package org.wordpress.android.ui.media.services; import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import android.support.annotation.NonNull; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.WordPress; import org.wordpress.android.fluxc.Dispatcher; import org.wordpress.android.fluxc.generated.MediaActionBuilder; import org.wordpress.android.fluxc.model.MediaModel; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.store.MediaStore; import org.wordpress.android.fluxc.store.MediaStore.MediaPayload; import org.wordpress.android.fluxc.store.MediaStore.OnMediaChanged; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; /** * A service for deleting media. Only one media item is deleted at a time. */ public class MediaDeleteService extends Service { public static final String SITE_KEY = "mediaSite"; public static final String MEDIA_LIST_KEY = "mediaList"; public class MediaDeleteBinder extends Binder { public MediaDeleteService getService() { return MediaDeleteService.this; } public void addMediaToDeleteQueue(@NonNull MediaModel media) { getDeleteQueue().add(media); deleteNextInQueue(); } public void removeMediaFromDeleteQueue(@NonNull MediaModel media) { getDeleteQueue().remove(media); deleteNextInQueue(); } } private final IBinder mBinder = new MediaDeleteBinder(); private SiteModel mSite; // required for payloads @Inject Dispatcher mDispatcher; @Inject MediaStore mMediaStore; private MediaModel mCurrentDelete; private List<MediaModel> mDeleteQueue; private List<MediaModel> mCompletedItems; @Override public void onCreate() { super.onCreate(); ((WordPress) getApplication()).component().inject(this); mDispatcher.register(this); mCurrentDelete = null; } @Override public void onDestroy() { mDispatcher.unregister(this); // TODO: if event not dispatched for ongoing delete cancel it and dispatch cancel event super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public int onStartCommand(Intent intent, int flags, int startId) { // stop service if no site is given if (intent == null || !intent.hasExtra(SITE_KEY)) { stopSelf(); return START_NOT_STICKY; } mSite = (SiteModel) intent.getSerializableExtra(SITE_KEY); mDeleteQueue = (List<MediaModel>) intent.getSerializableExtra(MEDIA_LIST_KEY); // start deleting queued media deleteNextInQueue(); // only run while app process is running, allows service to be stopped by user force closing the app return START_NOT_STICKY; } @SuppressWarnings("unused") @Subscribe(threadMode = ThreadMode.MAIN) public void onMediaChanged(OnMediaChanged event) { // event for unknown media, ignoring if (event.mediaList == null || event.mediaList.isEmpty() || !matchesInProgressMedia(event.mediaList.get(0))) { AppLog.w(T.MEDIA, "Media event not recognized: " + event.mediaList); return; } if (event.isError()) { handleOnMediaChangedError(event); } else { handleMediaChangedSuccess(event); } deleteNextInQueue(); } public @NonNull List<MediaModel> getDeleteQueue() { if (mDeleteQueue == null) { mDeleteQueue = new ArrayList<>(); } return mDeleteQueue; } public @NonNull List<MediaModel> getCompletedItems() { if (mCompletedItems == null) { mCompletedItems = new ArrayList<>(); } return mCompletedItems; } private void handleMediaChangedSuccess(@NonNull OnMediaChanged event) { switch (event.cause) { case DELETE_MEDIA: if (mCurrentDelete != null) { AppLog.d(T.MEDIA, mCurrentDelete.getTitle() + " successfully deleted!"); completeCurrentDelete(); } break; case REMOVE_MEDIA: if (mCurrentDelete != null) { AppLog.d(T.MEDIA, "Successfully deleted " + mCurrentDelete.getTitle()); completeCurrentDelete(); } break; } } private void handleOnMediaChangedError(@NonNull OnMediaChanged event) { MediaModel media = event.mediaList.get(0); switch (event.error.type) { case AUTHORIZATION_REQUIRED: AppLog.v(T.MEDIA, "Authorization required. Stopping MediaDeleteService."); // stop delete service until authorized to perform actions on site stopSelf(); break; case NULL_MEDIA_ARG: // shouldn't happen, get back to deleting the queue AppLog.d(T.MEDIA, "Null media argument supplied, skipping current delete."); completeCurrentDelete(); break; case NOT_FOUND: if (media == null) { break; } AppLog.d(T.MEDIA, "Could not find media (id=" + media.getMediaId() + "). on remote"); // remove media from local database mDispatcher.dispatch(MediaActionBuilder.newRemoveMediaAction(mCurrentDelete)); break; case PARSE_ERROR: AppLog.d(T.MEDIA, "Error parsing reponse to " + event.cause.toString() + "."); completeCurrentDelete(); break; default: completeCurrentDelete(); break; } } /** * Delete next media item in queue. Only one media item is deleted at a time. */ private void deleteNextInQueue() { // waiting for response to current delete request if (mCurrentDelete != null) { AppLog.i(T.MEDIA, "Ignoring request to deleteNextInQueue, only one media item can be deleted at a time."); return; } // somehow lost our reference to the site, stop service if (mSite == null) { AppLog.i(T.MEDIA, "Unexpected state, site is null. Stopping MediaDeleteService."); stopSelf(); return; } mCurrentDelete = nextMediaToDelete(); // no more items to delete, stop service if (mCurrentDelete == null) { AppLog.v(T.MEDIA, "No more media items to delete. Stopping MediaDeleteService."); stopSelf(); return; } dispatchDeleteAction(mCurrentDelete); } private void dispatchDeleteAction(@NonNull MediaModel media) { AppLog.v(T.MEDIA, "Deleting " + media.getTitle() + " (id=" + media.getMediaId() + ")"); MediaPayload payload = new MediaPayload(mSite, media); mDispatcher.dispatch(MediaActionBuilder.newDeleteMediaAction(payload)); } /** * Compares site ID and media ID to determine if a given media item matches the current media item being deleted. */ private boolean matchesInProgressMedia(final @NonNull MediaModel media) { return mCurrentDelete != null && media.getLocalSiteId() == mCurrentDelete.getLocalSiteId() && media.getMediaId() == mCurrentDelete.getMediaId(); } /** * @return the next item in the queue to delete, null if queue is empty */ private MediaModel nextMediaToDelete() { if (!getDeleteQueue().isEmpty()) { return getDeleteQueue().get(0); } return null; } /** * Moves current delete from the queue into the completed list. */ private void completeCurrentDelete() { if (mCurrentDelete != null) { getCompletedItems().add(mCurrentDelete); getDeleteQueue().remove(mCurrentDelete); mCurrentDelete = null; } } }