Skip to content

Commit

Permalink
Merge pull request #13103 from wordpress-mobile/try/jetpack-stories-b…
Browse files Browse the repository at this point in the history
…lock-error-handling

Mobile stories block - part 4: error handling
  • Loading branch information
mzorz authored Oct 23, 2020
2 parents 6d01bff + d358037 commit f1fe044
Show file tree
Hide file tree
Showing 17 changed files with 1,049 additions and 200 deletions.
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) {
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

0 comments on commit f1fe044

Please sign in to comment.