package org.wordpress.android.ui.media.services;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.util.SparseArray;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.wordpress.android.WordPress;
import org.wordpress.android.analytics.AnalyticsTracker;
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.MediaModel.UploadState;
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.OnMediaUploaded;
import org.wordpress.android.models.MediaUploadState;
import org.wordpress.android.ui.posts.services.PostEvents;
import org.wordpress.android.util.AnalyticsUtils;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import de.greenrobot.event.EventBus;
/**
* Started with explicit list of media to upload.
*/
public class MediaUploadService extends Service {
private static final String MEDIA_LIST_KEY = "mediaList";
private SiteModel mSite;
private MediaModel mCurrentUpload;
private List<MediaModel> mUploadQueue = new ArrayList<>();
private SparseArray<Long> mUploadQueueTime = new SparseArray<>();
@Inject Dispatcher mDispatcher;
@Inject MediaStore mMediaStore;
public static void startService(Context context, SiteModel siteModel, ArrayList<MediaModel> mediaList) {
if (context == null) {
return;
}
Intent intent = new Intent(context, MediaUploadService.class);
intent.putExtra(WordPress.SITE, siteModel);
intent.putExtra(MediaUploadService.MEDIA_LIST_KEY, mediaList);
context.startService(intent);
}
@Override
public void onCreate() {
super.onCreate();
((WordPress) getApplication()).component().inject(this);
AppLog.i(AppLog.T.MEDIA, "Media Upload Service > created");
mDispatcher.register(this);
EventBus.getDefault().register(this);
mCurrentUpload = null;
}
@Override
public void onDestroy() {
cancelCurrentUpload();
mDispatcher.unregister(this);
EventBus.getDefault().unregister(this);
AppLog.i(AppLog.T.MEDIA, "Media Upload Service > destroyed");
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null || !intent.hasExtra(WordPress.SITE)) {
AppLog.e(AppLog.T.MEDIA, "MediaUploadService was killed and restarted with a null intent.");
stopServiceIfUploadsComplete();
return START_NOT_STICKY;
}
unpackIntent(intent);
uploadNextInQueue();
return START_REDELIVER_INTENT;
}
private void handleOnMediaUploadedSuccess(@NonNull OnMediaUploaded event) {
if (event.canceled) {
// Upload canceled
AppLog.i(AppLog.T.MEDIA, "Upload successfully canceled.");
trackUploadMediaEvents(AnalyticsTracker.Stat.MEDIA_UPLOAD_CANCELED, mCurrentUpload, null);
completeCurrentUpload();
uploadNextInQueue();
} else if (event.completed) {
// Upload completed
AppLog.i(AppLog.T.MEDIA, "Upload completed - localId=" + event.media.getId() + " title=" + event.media.getTitle());
trackUploadMediaEvents(AnalyticsTracker.Stat.MEDIA_UPLOAD_SUCCESS, mCurrentUpload, null);
mCurrentUpload.setMediaId(event.media.getMediaId());
completeCurrentUpload();
uploadNextInQueue();
} else {
// Upload Progress
// TODO check if we need to broadcast event.media, event.progress or we're just fine with
// listening to event.media, event.progress
}
}
private void handleOnMediaUploadedError(@NonNull OnMediaUploaded event) {
AppLog.w(AppLog.T.MEDIA, "Error uploading media: " + event.error.message);
// TODO: Don't update the state here, it needs to be done in FluxC
mCurrentUpload.setUploadState(UploadState.FAILED.name());
mDispatcher.dispatch(MediaActionBuilder.newUpdateMediaAction(mCurrentUpload));
// TODO: check whether we need to broadcast the error or maybe it is enough to register for FluxC events
// event.media, event.error
Map<String, Object> properties = new HashMap<>();
properties.put("error_type", event.error.type.name());
trackUploadMediaEvents(AnalyticsTracker.Stat.MEDIA_UPLOAD_ERROR, mCurrentUpload, properties);
completeCurrentUpload();
uploadNextInQueue();
}
private void uploadNextInQueue() {
// waiting for response to current upload request
if (mCurrentUpload != null) {
AppLog.i(AppLog.T.MEDIA, "Ignoring request to uploadNextInQueue, only one media item can be uploaded at a time.");
return;
}
// somehow lost our reference to the site, complete this action
if (mSite == null) {
AppLog.i(AppLog.T.MEDIA, "Unexpected state, site is null. Skipping this request - MediaUploadService.");
stopServiceIfUploadsComplete();
return;
}
mCurrentUpload = getNextMediaToUpload();
if (mCurrentUpload == null) {
AppLog.v(AppLog.T.MEDIA, "No more media items to upload. Skipping this request - MediaUploadService.");
stopServiceIfUploadsComplete();
return;
}
dispatchUploadAction(mCurrentUpload);
trackUploadMediaEvents(AnalyticsTracker.Stat.MEDIA_UPLOAD_STARTED, mCurrentUpload, null);
}
private void completeCurrentUpload() {
if (mCurrentUpload != null) {
mUploadQueue.remove(mCurrentUpload);
mCurrentUpload = null;
}
}
private MediaModel getNextMediaToUpload() {
if (!mUploadQueue.isEmpty()) {
return mUploadQueue.get(0);
}
return null;
}
private void addUniqueMediaToQueue(MediaModel media) {
for (MediaModel queuedMedia : mUploadQueue) {
if (queuedMedia.getLocalSiteId() == media.getLocalSiteId() &&
StringUtils.equals(queuedMedia.getFilePath(), media.getFilePath())) {
return;
}
}
// no match found in queue
mUploadQueue.add(media);
}
private void unpackIntent(@NonNull Intent intent) {
mSite = (SiteModel) intent.getSerializableExtra(WordPress.SITE);
// add local queued media from store
List<MediaModel> localMedia = mMediaStore.getLocalSiteMedia(mSite);
if (localMedia != null && !localMedia.isEmpty()) {
// uploading is updated to queued, queued media added to the queue, failed media added to completed list
for (MediaModel mediaItem : localMedia) {
if (MediaUploadState.UPLOADING.name().equals(mediaItem.getUploadState())) {
mediaItem.setUploadState(MediaUploadState.QUEUED.name());
mDispatcher.dispatch(MediaActionBuilder.newUpdateMediaAction(mediaItem));
}
if (MediaUploadState.QUEUED.name().equals(mediaItem.getUploadState())) {
addUniqueMediaToQueue(mediaItem);
}
}
}
// add new media
@SuppressWarnings("unchecked")
List<MediaModel> mediaList = (List<MediaModel>) intent.getSerializableExtra(MEDIA_LIST_KEY);
if (mediaList != null) {
for (MediaModel media : mediaList) {
addUniqueMediaToQueue(media);
}
}
}
private boolean matchesInProgressMedia(final @NonNull MediaModel media) {
return mCurrentUpload != null && media.getLocalSiteId() == mCurrentUpload.getLocalSiteId();
}
private void cancelCurrentUpload() {
if (mCurrentUpload != null) {
dispatchCancelAction(mCurrentUpload);
mCurrentUpload = null;
}
}
private void cancelAllUploads() {
mUploadQueue.clear();
mUploadQueueTime.clear();
cancelCurrentUpload();
}
private void cancelUpload(int localMediaId) {
// Cancel if it's currently uploading
if (mCurrentUpload != null && mCurrentUpload.getId() == localMediaId) {
cancelCurrentUpload();
}
// Remove from the queue
for(Iterator<MediaModel> i = mUploadQueue.iterator(); i.hasNext();) {
MediaModel mediaModel = i.next();
if (mediaModel.getId() == localMediaId) {
i.remove();
}
}
}
private void dispatchUploadAction(@NonNull final MediaModel media) {
AppLog.i(AppLog.T.MEDIA, "Dispatching upload action for media with local id: " + media.getId() +
" and path: " + media.getFilePath());
media.setUploadState(UploadState.UPLOADING.name());
mDispatcher.dispatch(MediaActionBuilder.newUpdateMediaAction(media));
MediaPayload payload = new MediaPayload(mSite, media);
mDispatcher.dispatch(MediaActionBuilder.newUploadMediaAction(payload));
}
private void dispatchCancelAction(@NonNull final MediaModel media) {
AppLog.i(AppLog.T.MEDIA, "Dispatching cancel upload action for media with local id: " + media.getId() +
" and path: " + media.getFilePath());
MediaPayload payload = new MediaPayload(mSite, mCurrentUpload);
mDispatcher.dispatch(MediaActionBuilder.newCancelMediaUploadAction(payload));
}
private void stopServiceIfUploadsComplete(){
AppLog.i(AppLog.T.MEDIA, "Media Upload Service > completed");
if (mUploadQueue.size() == 0) {
AppLog.i(AppLog.T.MEDIA, "No more items pending in queue. Stopping MediaUploadService.");
stopSelf();
}
}
// App events
@SuppressWarnings("unused")
public void onEventMainThread(PostEvents.PostMediaCanceled event) {
if (event.all) {
cancelAllUploads();
return;
}
cancelUpload(event.localMediaId);
}
// FluxC events
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMediaUploaded(OnMediaUploaded event) {
// event for unknown media, ignoring
if (event.media == null || !matchesInProgressMedia(event.media)) {
AppLog.w(AppLog.T.MEDIA, "Media event not recognized: " + event.media);
return;
}
if (event.isError()) {
handleOnMediaUploadedError(event);
} else {
handleOnMediaUploadedSuccess(event);
}
}
/**
* Analytics about media being uploaded
*
* @param media The media being uploaded
*/
private void trackUploadMediaEvents(AnalyticsTracker.Stat stat, MediaModel media, Map<String, Object> properties) {
if (media == null) {
AppLog.e(AppLog.T.MEDIA, "Cannot track media upload service events if the original media is null!!");
return;
}
Map<String, Object> mediaProperties = AnalyticsUtils.getMediaProperties(this, media.isVideo(), null, media.getFilePath());
if (properties != null) {
mediaProperties.putAll(properties);
}
long currentTime = System.currentTimeMillis();
if (stat == AnalyticsTracker.Stat.MEDIA_UPLOAD_STARTED) {
mUploadQueueTime.put(media.getId(), currentTime);
} else if(stat == AnalyticsTracker.Stat.MEDIA_UPLOAD_SUCCESS || stat == AnalyticsTracker.Stat.MEDIA_UPLOAD_ERROR) {
mediaProperties.put("upload_time_ms", currentTime - mUploadQueueTime.get(media.getId(), currentTime));
mUploadQueueTime.remove(media.getId());
}
AnalyticsTracker.track(stat, mediaProperties);
}
}