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

Mobile stories block - part 4: error handling #13103

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
8e415ba
wired in storySaveMediaListener.onStorySaveResult() correctly
mzorz Oct 11, 2020
477f231
cover the case for signaling temporal ids in media file save progress…
mzorz Oct 11, 2020
dc429da
updated gutenberg-mobile commit hash for error handling WIP
mzorz Oct 11, 2020
823404c
added upload retry and cancel bridge methods for mediaFiles collectio…
mzorz Oct 12, 2020
02defeb
updated commit hash for gutenberg-mobile, deep copy of mediaFiles whe…
mzorz Oct 12, 2020
f69413d
implemented requestMediaFilesSaveCancelDialog bridge method, just sho…
mzorz Oct 12, 2020
a7451cd
fixed lint errors, and added TODO for cancellation (not going to impl…
mzorz Oct 12, 2020
33bf90d
removed unnecessary toInt conversion call
mzorz Oct 12, 2020
e0f3640
broke comment lines to avoid exceeding 120 chars max length
mzorz Oct 12, 2020
1039293
removed unnecessary toInt() conversion
mzorz Oct 12, 2020
f8b3dea
clarified comment
mzorz Oct 12, 2020
471d7f4
updated bridge method names from mediaModelCreatedForFile to mediaIdC…
mzorz Oct 13, 2020
032a741
rolled back rename of event class StoryFrameMediaModelCreatedEvent, i…
mzorz Oct 13, 2020
1bcf38d
built logic to save temporary ids to prefs as well
mzorz Oct 13, 2020
246c4fe
accounting for Story re-creation out of temporary saved slides
mzorz Oct 13, 2020
3958007
updated gutenberg-mobile commit hash, removed commented out code
mzorz Oct 14, 2020
d7c646a
kotlinified / streamlined logic for recreating Story from prefs
mzorz Oct 14, 2020
d75a6f9
if story is unsuccessful, add id to failedMediaIds and remove from pr…
mzorz Oct 14, 2020
20e8f71
make sure not to cast id coming from Gutenberg mediaFiles as String, …
mzorz Oct 14, 2020
cd82b8d
only proceed with readding a saved slide if this is a final StorySave…
mzorz Oct 14, 2020
8b3e7e8
processing media slides with buildStoryMediaFileDataForTemporarySlide…
mzorz Oct 14, 2020
a499604
removed no longer used cleanTemporaryMediaFilesStructFoundInAnyStoryB…
mzorz Oct 14, 2020
dd312ef
fixed linter warnings
mzorz Oct 14, 2020
b8d6c4f
removed no longer needed check for anyMediaIdsInGutenbergStoryBlockAr…
mzorz Oct 14, 2020
de53b35
fixed bad indirection reference
mzorz Oct 14, 2020
5406c63
using cancel label for cancel button
mzorz Oct 14, 2020
b09a3c9
updated gutenberg-mobile hash
mzorz Oct 16, 2020
550f2e1
updates stories lib hash
mzorz Oct 16, 2020
bee838d
fixed merge conflicts
mzorz Oct 17, 2020
5edee85
Merge branch 'feature/jetpack-stories-block-base' into try/jetpack-st…
mzorz Oct 19, 2020
ef587b6
updated commit hash
mzorz Oct 20, 2020
76bc100
updated gutenberg-mobile commit hash
mzorz Oct 20, 2020
1e5220f
moved logic for Stories-related load/retry/cancel handling from EditP…
mzorz Oct 21, 2020
bcd2b1a
Merge branch 'feature/jetpack-stories-block-base' into try/jetpack-st…
mzorz Oct 21, 2020
102c634
Merge branch 'try/jetpack-stories-block-error-handling' into try/stor…
mzorz Oct 21, 2020
546b667
adding unit tests for LoadStoryFromStoriesPrefsUseCase class - partial
mzorz Oct 21, 2020
7ab26a5
added test cases for method areAllStorySlidesEditable
mzorz Oct 21, 2020
d5d5731
removed unused imports
mzorz Oct 21, 2020
5e55e16
removed commented line
mzorz Oct 21, 2020
4d1eaa1
giving parameter explicit name
mzorz Oct 21, 2020
6bb0e31
removed unnecessary val declaration
mzorz Oct 21, 2020
9b80af4
renamed _Phase1 and _Phase2 postfixed method names and added comments…
mzorz Oct 21, 2020
d7986d0
Update WordPress/src/main/java/org/wordpress/android/ui/stories/media…
mzorz Oct 21, 2020
695fed6
added const prefix
mzorz Oct 21, 2020
2bcc13e
added private scope modifier to private fun
mzorz Oct 21, 2020
c5e23b3
Update WordPress/src/main/java/org/wordpress/android/ui/stories/prefs…
mzorz Oct 21, 2020
dc19f99
Update WordPress/src/main/java/org/wordpress/android/ui/stories/prefs…
mzorz Oct 21, 2020
798da18
renamed parameters, simplified logic removing local val and modified …
mzorz Oct 21, 2020
1f36898
updated gutenberg-mobile commit hash
mzorz Oct 21, 2020
fa5e87a
removed unused var and import
mzorz Oct 21, 2020
74c62e7
renamed site name in test example
mzorz Oct 21, 2020
f53da47
added unit tests for SaveStoryGutenbergBlockUseCase class
mzorz Oct 21, 2020
83ee3c2
added more tests
mzorz Oct 21, 2020
c59fa66
added test case
mzorz Oct 21, 2020
773495a
added failing test for review
mzorz Oct 22, 2020
77cb234
fixed test, added needed test dependency
mzorz Oct 22, 2020
d9ed477
fixed lint warnings
mzorz Oct 22, 2020
cb26f56
Merge branch 'try/jetpack-stories-block-error-handling' into try/stor…
mzorz Oct 22, 2020
f8fb304
removed unused resources
mzorz Oct 22, 2020
875c8aa
removed unused resource in stories library
mzorz Oct 22, 2020
6b27610
mocking siteModel
mzorz Oct 22, 2020
0575171
applying naming convention for constants :)
mzorz Oct 22, 2020
aa5fe46
Update WordPress/src/test/java/org/wordpress/android/ui/stories/SaveS…
mzorz Oct 22, 2020
87d8f7b
dropped the one prefix
mzorz Oct 22, 2020
deca77d
Merge branch 'try/jetpack-stories-block-error-handling' of https://gi…
mzorz Oct 22, 2020
252b338
lifted return statement
mzorz Oct 22, 2020
828d5b9
renamed var
mzorz Oct 22, 2020
4d00ef2
mocking siteModel
mzorz Oct 22, 2020
204dc93
removed redundant assertion
mzorz Oct 22, 2020
f51842d
renamed var
mzorz Oct 22, 2020
efc7b2f
Update WordPress/src/test/java/org/wordpress/android/ui/stories/useca…
mzorz Oct 22, 2020
fea9d33
Update WordPress/src/test/java/org/wordpress/android/ui/stories/SaveS…
mzorz Oct 22, 2020
e8a1f55
beautified code
mzorz Oct 22, 2020
d9b260f
fixed typo
mzorz Oct 22, 2020
5a28768
updated stories library resources
mzorz Oct 22, 2020
10a8ee0
renamed test method
mzorz Oct 22, 2020
f324acd
renamed method
mzorz Oct 22, 2020
05ba52e
renamed parameter
mzorz Oct 22, 2020
621abbf
made the variable so it matches in setup and verification
mzorz Oct 22, 2020
ef2873c
don't set a default
mzorz Oct 22, 2020
8395bbe
removed extra parenthesis
mzorz Oct 22, 2020
27293f2
Merge branch 'try/jetpack-stories-block-error-handling' into try/stor…
mzorz Oct 22, 2020
47902d1
Merge pull request #13198 from wordpress-mobile/try/stories-listener-…
mzorz Oct 22, 2020
d358037
updated gutenberg-mobile commit hash with updated gutenberg and jetpa…
mzorz Oct 22, 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
1 change: 1 addition & 0 deletions WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ androidExtensions {

dependencies {
implementation project(path:':libs:stories-android:stories')
testImplementation project(path:':photoeditor')
implementation project(path:':libs:image-editor::ImageEditor')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@
import org.wordpress.android.ui.stories.StoryRepositoryWrapper;
import org.wordpress.android.ui.stories.prefs.StoriesPrefs;
import org.wordpress.android.ui.stories.usecase.LoadStoryFromStoriesPrefsUseCase;
import org.wordpress.android.ui.stories.usecase.LoadStoryFromStoriesPrefsUseCase.ReCreateStoryResult;
import org.wordpress.android.ui.uploads.PostEvents;
import org.wordpress.android.ui.uploads.UploadService;
import org.wordpress.android.ui.uploads.UploadUtils;
Expand Down Expand Up @@ -677,7 +676,7 @@ protected void onCreate(Bundle savedInstanceState) {

setupPrepublishingBottomSheetRunnable();

mStoriesEventListener.start(this.getLifecycle(), mSite);
mStoriesEventListener.start(this.getLifecycle(), mSite, mEditPostRepository);
setupPreviewUI();
}

Expand Down Expand Up @@ -3252,62 +3251,32 @@ public void onTrackableEvent(TrackableEvent event, Map<String, String> propertie
}

@Override public void onStoryComposerLoadRequested(ArrayList<Object> mediaFiles, String blockId) {
if (mLoadStoryFromStoriesPrefsUseCase.anyMediaIdsInGutenbergStoryBlockAreCorrupt(mediaFiles)) {
// unfortunately the medaiIds seem corrupt so, show a dialog and bail
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(getString(R.string.dialog_edit_story_unavailable_title));
builder.setMessage(getString(R.string.dialog_edit_story_corrupt_message));
builder.setPositiveButton(R.string.dialog_button_ok, (dialog, id) -> {
dialog.dismiss();
});
AlertDialog dialog = builder.create();
dialog.show();
return;
}
boolean noSlidesLoaded = mStoriesEventListener.onRequestMediaFilesEditorLoad(
this,
new LocalId(mEditPostRepository.getId()),
mNetworkErrorOnLastMediaFetchAttempt,
mediaFiles,
blockId
);

ReCreateStoryResult result = mLoadStoryFromStoriesPrefsUseCase
.loadStoryFromMemoryOrRecreateFromPrefs(mSite, mediaFiles);
if (!result.getNoSlidesLoaded()) {
// Story instance loaded or re-created! Load it onto the StoryComposer for editing now
ActivityLauncher.editStoryForResult(
this,
mSite,
new LocalId(mEditPostRepository.getId()),
result.getStoryIndex(),
result.getAllStorySlidesAreEditable(),
true,
blockId
);
} else {
// unfortunately we couldn't even load the remote media Ids indicated by the StoryBlock so we can't allow
// editing at this time :(
if (mNetworkErrorOnLastMediaFetchAttempt) {
// there was an error fetching media when we were loading the editor,
// we *may* still have a possibility, tell the user they may try refreshing the media again
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(getString(R.string.dialog_edit_story_unavailable_title));
builder.setMessage(getString(R.string.dialog_edit_story_unavailable_message));
builder.setPositiveButton(R.string.dialog_button_ok, (dialog, id) -> {
// try another fetchMedia request
fetchMediaList();
dialog.dismiss();
});
AlertDialog dialog = builder.create();
dialog.show();
} else {
// unrecoverable error, nothing we can do, inform the user :(.
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(getString(R.string.dialog_edit_story_unrecoverable_title));
builder.setMessage(getString(R.string.dialog_edit_story_unrecoverable_message));
builder.setPositiveButton(R.string.dialog_button_ok, (dialog, id) -> {
dialog.dismiss();
});
AlertDialog dialog = builder.create();
dialog.show();
}
if (mNetworkErrorOnLastMediaFetchAttempt && noSlidesLoaded) {
// try another fetchMedia request
fetchMediaList();
}
}

@Override public void onRetryUploadForMediaCollection(ArrayList<Object> mediaFiles) {
jd-alexander marked this conversation as resolved.
Show resolved Hide resolved
mStoriesEventListener.onRetryUploadForMediaCollection(this, mediaFiles, mEditorMediaUploadListener);
}

@Override public void onCancelUploadForMediaCollection(ArrayList<Object> mediaFiles) {
mStoriesEventListener.onCancelUploadForMediaCollection(mediaFiles);
}

@Override public void onCancelSaveForMediaCollection(ArrayList<Object> mediaFiles) {
mStoriesEventListener.onCancelSaveForMediaCollection(mediaFiles);
}

// FluxC events

@SuppressWarnings("unused")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,62 @@
package org.wordpress.android.ui.posts.editor

import android.app.Activity
import android.content.DialogInterface
import android.net.Uri
import androidx.appcompat.app.AlertDialog.Builder
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.State.CREATED
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.wordpress.stories.compose.frame.StorySaveEvents.FrameSaveCompleted
import com.wordpress.stories.compose.frame.StorySaveEvents.FrameSaveFailed
import com.wordpress.stories.compose.frame.StorySaveEvents.FrameSaveProgress
import com.wordpress.stories.compose.frame.StorySaveEvents.FrameSaveStart
import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveResult
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.wordpress.android.R
import org.wordpress.android.R.string
import org.wordpress.android.analytics.AnalyticsTracker
import org.wordpress.android.analytics.AnalyticsTracker.Stat.EDITOR_UPLOAD_MEDIA_RETRIED
import org.wordpress.android.editor.EditorMediaUploadListener
import org.wordpress.android.editor.gutenberg.StorySaveMediaListener
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId
import org.wordpress.android.fluxc.model.MediaModel
import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState.UPLOADED
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.store.MediaStore
import org.wordpress.android.ui.ActivityLauncher
import org.wordpress.android.ui.posts.EditPostRepository
import org.wordpress.android.ui.posts.editor.media.EditorMedia
import org.wordpress.android.ui.stories.SaveStoryGutenbergBlockUseCase.Companion.TEMPORARY_ID_PREFIX
import org.wordpress.android.ui.stories.StoryRepositoryWrapper
import org.wordpress.android.ui.stories.media.StoryMediaSaveUploadBridge.StoryFrameMediaModelCreatedEvent
import org.wordpress.android.ui.stories.usecase.LoadStoryFromStoriesPrefsUseCase
import org.wordpress.android.ui.uploads.UploadService
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T.MEDIA
import org.wordpress.android.util.EventBusWrapper
import org.wordpress.android.util.FluxCUtils
import org.wordpress.android.util.StringUtils
import org.wordpress.android.util.helpers.MediaFile
import java.util.ArrayList
import java.util.HashMap
import javax.inject.Inject

class StoriesEventListener @Inject constructor(
private val dispatcher: Dispatcher,
private val mediaStore: MediaStore,
private val eventBusWrapper: EventBusWrapper,
private val editorMedia: EditorMedia,
private val loadStoryFromStoriesPrefsUseCase: LoadStoryFromStoriesPrefsUseCase,
private val storyRepositoryWrapper: StoryRepositoryWrapper
) : LifecycleObserver {
private lateinit var lifecycle: Lifecycle
private lateinit var site: SiteModel
private lateinit var editPostRepository: EditPostRepository
private var storySaveMediaListener: StorySaveMediaListener? = null

@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
Expand All @@ -43,7 +67,7 @@ class StoriesEventListener @Inject constructor(

/**
* Handles the [Lifecycle.Event.ON_DESTROY] event to cleanup the registration for dispatcher and removing the
* observer for lifecycle.
* observer for lifecycle .
*/
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
private fun onDestroy() {
Expand All @@ -52,8 +76,9 @@ class StoriesEventListener @Inject constructor(
eventBusWrapper.unregister(this)
}

fun start(lifecycle: Lifecycle, site: SiteModel) {
fun start(lifecycle: Lifecycle, site: SiteModel, editPostRepository: EditPostRepository) {
this.site = site
this.editPostRepository = editPostRepository
this.lifecycle = lifecycle
this.lifecycle.addObserver(this)
}
Expand Down Expand Up @@ -121,11 +146,11 @@ class StoriesEventListener @Inject constructor(
}

@Subscribe(threadMode = ThreadMode.MAIN)
fun onStoryFrameMediaModelCreated(event: StoryFrameMediaModelCreatedEvent) {
fun onStoryFrameMediaIdChanged(event: StoryFrameMediaModelCreatedEvent) {
if (!lifecycle.currentState.isAtLeast(CREATED)) {
return
}
storySaveMediaListener?.onMediaModelCreatedForFile(event.oldId, event.newId, event.oldUrl)
storySaveMediaListener?.onMediaModelCreatedForFile(event.oldId, event.newId.toString(), event.oldUrl)
}

@Subscribe(threadMode = ThreadMode.MAIN)
Expand All @@ -135,9 +160,10 @@ class StoriesEventListener @Inject constructor(
}
val localMediaId = event.frameId.toString()
// just update progress, we may have still some other frames in this story that need be saved.
// we will send the Failed signal once all the Story frames have been processed
// we will send the Failed signal once all the Story frames have been processed (see onStorySaveProcessFinished)
val progress: Float = storyRepositoryWrapper.getCurrentStorySaveProgress(event.storyIndex, 0.0f)
storySaveMediaListener?.onMediaSaveReattached(localMediaId, progress)
// storySaveMediaListener?.onMediaSaveFailed(localMediaId)
}

@Subscribe(threadMode = ThreadMode.MAIN)
Expand All @@ -146,14 +172,135 @@ class StoriesEventListener @Inject constructor(
return
}
val story = storyRepositoryWrapper.getStoryAtIndex(event.storyIndex)
if (event.isSuccess() && event.frameSaveResult.size == story.frames.size) {
if (!event.isRetry && event.frameSaveResult.size == story.frames.size) {
// take the first frame IDs and mediaUri
val localMediaId = story.frames[0].id.toString()
val mediaUrl: String = Uri.fromFile(story.frames[0].composedFrameFile).toString()
storySaveMediaListener?.onMediaSaveSucceeded(localMediaId, mediaUrl)
storySaveMediaListener?.onStorySaveResult(localMediaId, event.isSuccess())
}
}

// Editor load / cancel events
fun onRequestMediaFilesEditorLoad(
activity: Activity,
postId: LocalId,
networkErrorOnLastMediaFetchAttempt: Boolean,
mediaFiles: ArrayList<Any>,
blockId: String
): Boolean {
val reCreateStoryResult = loadStoryFromStoriesPrefsUseCase
.loadStoryFromMemoryOrRecreateFromPrefs(site, mediaFiles)
if (!reCreateStoryResult.noSlidesLoaded) {
// Story instance loaded or re-created! Load it onto the StoryComposer for editing now
ActivityLauncher.editStoryForResult(
activity,
site,
postId,
reCreateStoryResult.storyIndex,
reCreateStoryResult.allStorySlidesAreEditable,
true,
blockId
)
} else {
val localMediaId = story.frames[0].id.toString()
storySaveMediaListener?.onMediaSaveFailed(localMediaId)
// unfortunately we couldn't even load the remote media Ids indicated by the StoryBlock so we can't allow
// editing at this time :(
if (networkErrorOnLastMediaFetchAttempt) {
// there was an error fetching media when we were loading the editor,
// we *may* still have a possibility, tell the user they may try refreshing the media again
val builder: Builder = MaterialAlertDialogBuilder(
activity
)
builder.setTitle(activity.getString(R.string.dialog_edit_story_unavailable_title))
builder.setMessage(activity.getString(R.string.dialog_edit_story_unavailable_message))
builder.setPositiveButton(R.string.dialog_button_ok) { dialog, id ->
dialog.dismiss()
}
val dialog = builder.create()
dialog.show()
} else {
// unrecoverable error, nothing we can do, inform the user :(.
val builder: Builder = MaterialAlertDialogBuilder(
activity
)
builder.setTitle(activity.getString(R.string.dialog_edit_story_unrecoverable_title))
builder.setMessage(activity.getString(R.string.dialog_edit_story_unrecoverable_message))
builder.setPositiveButton(R.string.dialog_button_ok) { dialog, id -> dialog.dismiss() }
val dialog = builder.create()
dialog.show()
}
}
return reCreateStoryResult.noSlidesLoaded
}

fun onCancelUploadForMediaCollection(mediaFiles: ArrayList<Any>) {
// just cancel upload for each media
for (mediaFile in mediaFiles) {
val localMediaId = StringUtils.stringToInt(
(mediaFile as HashMap<String?, Any?>)["id"].toString(), 0
)
if (localMediaId != 0) {
editorMedia.cancelMediaUploadAsync(localMediaId, false)
}
}
}

fun onRetryUploadForMediaCollection(
activity: Activity,
mediaFiles: ArrayList<Any>,
editorMediaUploadListener: EditorMediaUploadListener?
) {
val mediaIdsToRetry = ArrayList<Int>()
for (mediaFile in mediaFiles) {
val localMediaId = StringUtils.stringToInt(
(mediaFile as HashMap<String?, Any?>)["id"].toString(), 0
)
if (localMediaId != 0) {
val media: MediaModel = mediaStore.getMediaWithLocalId(localMediaId)
// if we find at least one item in the mediaFiles collection passed
// for which we don't have a local MediaModel, just tell the user and bail
if (media == null) {
AppLog.e(
MEDIA,
"Can't find media with local id: $localMediaId"
)
val builder: Builder = MaterialAlertDialogBuilder(
activity
)
builder.setTitle(activity.getString(string.cannot_retry_deleted_media_item_fatal))
builder.setPositiveButton(string.yes) { dialog, id -> dialog.dismiss() }
builder.setNegativeButton(activity.getString(string.no),
DialogInterface.OnClickListener { dialog: DialogInterface, id: Int -> dialog.dismiss() }
)
val dialog = builder.create()
dialog.show()
return
}
if (media.url != null && media.uploadState == UPLOADED.toString()) {
// Note: we should actually do this when the editor fragment starts instead of waiting for user
// input.
// Notify the editor fragment upload was successful and it should replace the local url by the
// remote url.
editorMediaUploadListener?.onMediaUploadSucceeded(
media.id.toString(),
FluxCUtils.mediaFileFromMediaModel(media)
)
} else {
UploadService.cancelFinalNotification(
activity,
editPostRepository.getPost()
)
UploadService.cancelFinalNotificationForMedia(activity, site)
mediaIdsToRetry.add(localMediaId)
}
}
}

if (!mediaIdsToRetry.isEmpty()) {
editorMedia.retryFailedMediaAsync(mediaIdsToRetry)
}
AnalyticsTracker.track(EDITOR_UPLOAD_MEDIA_RETRIED)
}

fun onCancelSaveForMediaCollection(mediaFiles: ArrayList<Any>) {
// TODO implement cancelling save process for media collection
}
}
Loading