Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autosave or update draft #11251

Merged
merged 17 commits into from
Feb 19, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
be3ad8b
A rough introduction of AutoSavePostIfNotDraftUseCase
oguzkocer Feb 6, 2020
32da8c4
Rough integration of AutoSavePostIfNotDraftUseCase
oguzkocer Feb 6, 2020
ab80870
Refactor AutoSavePostIfNotDraftUseCase so it's reusable
oguzkocer Feb 6, 2020
4cf02b7
Update FluxC hash which contains some fixes to fetching post status
oguzkocer Feb 6, 2020
738e71e
Throw IllegalArgumentException if autosave use case is already handli…
oguzkocer Feb 10, 2020
6767c91
Refactor AutoSavePostIfNotDraftUseCase
oguzkocer Feb 10, 2020
0b2a89e
Handle errors in AutoSavePostIfNotDraftUseCase
oguzkocer Feb 10, 2020
a70f33e
Merge remote-tracking branch 'origin/develop' into issue/autosave-or-…
oguzkocer Feb 11, 2020
d01c3f8
Handle all AutoSavePostIfNotDraftResult types
oguzkocer Feb 11, 2020
bb985b1
Observe AutoSavePostIfNotDraftUseCase.onPostChanged on MAIN thread wi…
oguzkocer Feb 13, 2020
63fd0fe
Don't show error notifications for post autosave fails
oguzkocer Feb 13, 2020
1268a78
Add documentation for auto-save use case
oguzkocer Feb 13, 2020
174b742
Sets up AutoSavePostIfNotDraftUseCaseTest and adds fetch post status …
oguzkocer Feb 13, 2020
19fad9d
Adds unit test for PostAutoSaveFailed
oguzkocer Feb 13, 2020
a3d06fb
Adds post is draft in remote unit test for AutoSavePostIfNotDraftUseCase
oguzkocer Feb 13, 2020
2c77170
Adds post auto-saved unit test for AutoSavePostIfNotDraftUseCase
oguzkocer Feb 13, 2020
26db1f9
Adds unit test for calling autoSavePostOrUpdateDraft with a local draft
oguzkocer Feb 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package org.wordpress.android.ui.uploads

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.BACKGROUND
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.generated.PostActionBuilder
import org.wordpress.android.fluxc.model.CauseOfOnPostChanged.RemoteAutoSavePost
import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId
import org.wordpress.android.fluxc.model.PostModel
import org.wordpress.android.fluxc.store.PostStore
import org.wordpress.android.fluxc.store.PostStore.OnPostChanged
import org.wordpress.android.fluxc.store.PostStore.OnPostStatusFetched
import org.wordpress.android.fluxc.store.PostStore.PostError
import org.wordpress.android.fluxc.store.PostStore.RemotePostPayload
import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.FetchPostStatusFailed
import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.PostAutoSaveFailed
import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.PostAutoSaved
import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.PostIsDraftInRemote
import java.lang.IllegalArgumentException
import javax.inject.Inject
import javax.inject.Named
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume

private const val DRAFT_POST_STATUS = "draft"

interface OnAutoSavePostIfNotDraftCallback {
fun handleAutoSavePostIfNotDraftResult(result: AutoSavePostIfNotDraftResult)
}

sealed class AutoSavePostIfNotDraftResult(open val post: PostModel) {
// Initial fetch post status request failed
data class FetchPostStatusFailed(override val post: PostModel, val error: PostError) :
AutoSavePostIfNotDraftResult(post)

// Post status is `DRAFT` in remote which means we'll want to update the draft directly
data class PostIsDraftInRemote(override val post: PostModel) : AutoSavePostIfNotDraftResult(post)

// Post status is not `DRAFT` in remote and the post was auto-saved successfully
data class PostAutoSaved(override val post: PostModel) : AutoSavePostIfNotDraftResult(post)

// Post status is not `DRAFT` in remote but the post auto-save failed
data class PostAutoSaveFailed(override val post: PostModel, val error: PostError) :
AutoSavePostIfNotDraftResult(post)
}

// TODO: Add documentation (add shortcode for the p2 discussion)
// TODO: Add unit tests
class AutoSavePostIfNotDraftUseCase @Inject constructor(
private val dispatcher: Dispatcher,
private val postStore: PostStore,
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher
) {
private val postStatusContinuations = HashMap<LocalId, Continuation<OnPostStatusFetched>>()
private val autoSaveContinuations = HashMap<LocalId, Continuation<OnPostChanged>>()

init {
dispatcher.register(this)
}

fun autoSavePostOrUpdateDraft(
remotePostPayload: RemotePostPayload,
callback: OnAutoSavePostIfNotDraftCallback
) {
val localPostId = LocalId(remotePostPayload.post.id)
if (remotePostPayload.post.isLocalDraft) {
throw IllegalArgumentException("Local drafts should not be auto-saved")
}
if (postStatusContinuations.containsKey(localPostId) ||
autoSaveContinuations.containsKey(localPostId)) {
throw IllegalArgumentException(
"This post is already being processed. Make sure not to start an autoSave " +
"or update draft action while another one is going on."
)
}
GlobalScope.launch(bgDispatcher) {
val onPostStatusFetched = fetchRemotePostStatus(remotePostPayload)
val result = when {
onPostStatusFetched.isError -> {
FetchPostStatusFailed(
post = remotePostPayload.post,
error = onPostStatusFetched.error
)
}
onPostStatusFetched.remotePostStatus == DRAFT_POST_STATUS -> {
PostIsDraftInRemote(remotePostPayload.post)
}
else -> {
autoSavePost(remotePostPayload)
}
}
callback.handleAutoSavePostIfNotDraftResult(result)
}
}

private suspend fun fetchRemotePostStatus(remotePostPayload: RemotePostPayload): OnPostStatusFetched {
val localPostId = LocalId(remotePostPayload.post.id)
return suspendCancellableCoroutine { cont ->
postStatusContinuations[localPostId] = cont
dispatcher.dispatch(PostActionBuilder.newFetchPostStatusAction(remotePostPayload))
}
}

private suspend fun autoSavePost(remotePostPayload: RemotePostPayload): AutoSavePostIfNotDraftResult {
val localPostId = LocalId(remotePostPayload.post.id)
val onPostChanged: OnPostChanged = suspendCancellableCoroutine { cont ->
autoSaveContinuations[localPostId] = cont
dispatcher.dispatch(PostActionBuilder.newRemoteAutoSavePostAction(remotePostPayload))
}
return if (onPostChanged.isError) {
PostAutoSaveFailed(remotePostPayload.post, onPostChanged.error)
} else {
val updatedPost = postStore.getPostByLocalPostId(localPostId.value)
PostAutoSaved(updatedPost)
}
}

@Subscribe(threadMode = BACKGROUND)
@Suppress("unused")
fun onPostStatusFetched(event: OnPostStatusFetched) {
val localPostId = LocalId(event.post.id)
postStatusContinuations[localPostId]?.let { continuation ->
continuation.resume(event)
postStatusContinuations.remove(localPostId)
}
}

@Subscribe(threadMode = BACKGROUND)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry I missed this during the initial review 😞

The previous implementation was subscribed on the MAIN thread and had priority set to 9. We wanted to ensure the PostUploadHandler receives the event before UploadService.
The UploadService invokes stopServiceIfUploadsComplete and checks mPostUploadHandler.hasInProgressUploads(). If we don't use the priority, we'll introduce a race condition as mPostUploadHandler.hasInProgressUploads() will return true or false depending on which class received the event first, UploadService vs PostUploadHandler.

Wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made this change in bb985b1, but I couldn't test it because I can't reproduce the issue even before the change. Also, as discussed on Slack, I think there is a better way to handle the priority issue, so I'll open an issue for that once this PR is merged.

@Suppress("unused")
fun onPostChanged(event: OnPostChanged) {
if (event.causeOfChange is RemoteAutoSavePost) {
val localPostId = LocalId((event.causeOfChange as RemoteAutoSavePost).localPostId)
autoSaveContinuations[localPostId]?.let { continuation ->
continuation.resume(event)
autoSaveContinuations.remove(localPostId)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.jetbrains.annotations.NotNull;
import org.wordpress.android.R;
import org.wordpress.android.WordPress;
import org.wordpress.android.analytics.AnalyticsTracker.Stat;
import org.wordpress.android.fluxc.Dispatcher;
import org.wordpress.android.fluxc.generated.MediaActionBuilder;
import org.wordpress.android.fluxc.generated.PostActionBuilder;
import org.wordpress.android.fluxc.model.CauseOfOnPostChanged;
import org.wordpress.android.fluxc.model.CauseOfOnPostChanged.RemoteAutoSavePost;
import org.wordpress.android.fluxc.model.MediaModel;
import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState;
import org.wordpress.android.fluxc.model.PostImmutableModel;
Expand All @@ -31,12 +30,15 @@
import org.wordpress.android.fluxc.store.MediaStore;
import org.wordpress.android.fluxc.store.MediaStore.UploadMediaPayload;
import org.wordpress.android.fluxc.store.PostStore;
import org.wordpress.android.fluxc.store.PostStore.OnPostChanged;
import org.wordpress.android.fluxc.store.PostStore.OnPostUploaded;
import org.wordpress.android.fluxc.store.PostStore.RemotePostPayload;
import org.wordpress.android.fluxc.store.SiteStore;
import org.wordpress.android.ui.posts.PostUtils;
import org.wordpress.android.ui.prefs.AppPrefs;
import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.FetchPostStatusFailed;
import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.PostAutoSaveFailed;
import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.PostAutoSaved;
import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.PostIsDraftInRemote;
import org.wordpress.android.ui.uploads.PostEvents.PostUploadStarted;
import org.wordpress.android.ui.utils.UiHelpers;
import org.wordpress.android.util.AppLog;
Expand All @@ -62,7 +64,7 @@

import javax.inject.Inject;

public class PostUploadHandler implements UploadHandler<PostModel> {
public class PostUploadHandler implements UploadHandler<PostModel>, OnAutoSavePostIfNotDraftCallback {
private static ArrayList<PostModel> sQueuedPostsList = new ArrayList<>();
private static Set<Integer> sFirstPublishPosts = new HashSet<>();
private static PostModel sCurrentUploadingPost = null;
Expand All @@ -79,6 +81,7 @@ public class PostUploadHandler implements UploadHandler<PostModel> {
@Inject MediaStore mMediaStore;
@Inject UiHelpers mUiHelpers;
@Inject UploadActionUseCase mUploadActionUseCase;
@Inject AutoSavePostIfNotDraftUseCase mAutoSavePostIfNotDraftUseCase;

PostUploadHandler(PostUploadNotifier postUploadNotifier) {
((WordPress) WordPress.getContext().getApplicationContext()).component().inject(this);
Expand Down Expand Up @@ -189,7 +192,7 @@ private void finishUpload() {
}

private enum UploadPostTaskResult {
PUSH_POST_DISPATCHED, ERROR, NOTHING_TO_UPLOAD
PUSH_POST_DISPATCHED, ERROR, NOTHING_TO_UPLOAD, AUTO_SAVE_OR_UPDATE_DRAFT
}

private class UploadPostTask extends AsyncTask<PostModel, Boolean, UploadPostTaskResult> {
Expand Down Expand Up @@ -286,8 +289,8 @@ protected UploadPostTaskResult doInBackground(PostModel... posts) {
break;
case REMOTE_AUTO_SAVE:
AppLog.d(T.POSTS, "PostUploadHandler - REMOTE_AUTO_SAVE. Post: " + mPost.getTitle());
mDispatcher.dispatch(PostActionBuilder.newRemoteAutoSavePostAction(payload));
break;
mAutoSavePostIfNotDraftUseCase.autoSavePostOrUpdateDraft(payload, PostUploadHandler.this);
malinajirka marked this conversation as resolved.
Show resolved Hide resolved
return UploadPostTaskResult.AUTO_SAVE_OR_UPDATE_DRAFT;
case DO_NOTHING:
AppLog.d(T.POSTS, "PostUploadHandler - DO_NOTHING. Post: " + mPost.getTitle());
// A single post might be enqueued twice for upload. It might cause some side-effects when the
Expand Down Expand Up @@ -604,6 +607,37 @@ private String uploadImageFile(MediaFile mediaFile, SiteModel site) {
}
}

// TODO: document?
@Override
public void handleAutoSavePostIfNotDraftResult(@NotNull AutoSavePostIfNotDraftResult result) {
PostModel post = result.getPost();
SiteModel site = mSiteStore.getSiteByLocalId(post.getLocalSiteId());
if (result instanceof FetchPostStatusFailed) {
mPostUploadNotifier.incrementUploadedPostCountFromForegroundNotification(post);
mPostUploadNotifier.updateNotificationErrorForPost(post, site, post.error.message, 0);
malinajirka marked this conversation as resolved.
Show resolved Hide resolved
finishUpload();
} else if (result instanceof PostAutoSaveFailed) {
mPostUploadNotifier.incrementUploadedPostCountFromForegroundNotification(post);
mPostUploadNotifier.updateNotificationErrorForPost(post, site, post.error.message, 0);
malinajirka marked this conversation as resolved.
Show resolved Hide resolved
finishUpload();
} else if (result instanceof PostAutoSaved) {
mPostUploadNotifier.incrementUploadedPostCountFromForegroundNotification(post);
finishUpload();
} else if (result instanceof PostIsDraftInRemote) {
/*
* If the post is a draft in remote, we'll update it directly instead of auto-saving it. Please see
* documentation of `AutoSavePostIfNotDraftUseCase` for details.
*
* We opted not to restore the current status after the post is uploaded to avoid its complexity and to
* replicate `UPLOAD_AS_DRAFT`. We may change this in the future.
*/
post.setStatus(PostStatus.DRAFT.toString());
mDispatcher.dispatch(PostActionBuilder.newPushPostAction(new RemotePostPayload(post, site)));
} else {
throw new IllegalStateException("All AutoSavePostIfNotDraftResult types must be handled");
}
}

/**
* Has priority 9 on OnPostUploaded events, which ensures that PostUploadHandler is the first to receive
* and process OnPostUploaded events, before they trickle down to other subscribers.
Expand Down Expand Up @@ -660,16 +694,4 @@ public void onPostUploaded(OnPostUploaded event) {

finishUpload();
}

@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN, priority = 9)
public void onPostChanged(OnPostChanged event) {
if (event.causeOfChange instanceof CauseOfOnPostChanged.RemoteAutoSavePost) {
int postLocalId = ((RemoteAutoSavePost) event.causeOfChange).getLocalPostId();
PostModel post = mPostStore.getPostByLocalPostId(postLocalId);
SiteModel site = mSiteStore.getSiteByLocalId(post.getLocalSiteId());
mPostUploadNotifier.incrementUploadedPostCountFromForegroundNotification(post);
finishUpload();
}
}
}