diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index fccb2a22ee52..ec7fb24f5e68 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,7 +2,7 @@ 26.8 ----- - +* [**] Resolved an issue where the editor could become impossible to exit when it failed to load. 26.7 ----- diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt index 98056cd9e4c5..7d9dd63c2409 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt @@ -1878,15 +1878,24 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene updateAndSavePostAsync(listener, isFinishing) } + @Suppress("ReturnCount") private fun updateFromEditor(oldContent: String, isFinishing: Boolean = false): UpdateFromEditor { editorFragment?.let { + // Don't read title/content from a stalled editor — doing so would + // overwrite the in-memory PostModel with empty strings, and the + // postChanged observer would then persist that empty state to + // SQLite. See issue #22878. + if (!it.isEditorReady()) { + AppLog.w(AppLog.T.EDITOR, "Skipping content update: Gutenberg editor not ready") + return UpdateFromEditor.Failed(java.lang.Exception("Gutenberg editor not ready")) + } return try { // To reduce redundant bridge events emitted to the Gutenberg editor, we get title and content at once val titleAndContent = it.getTitleAndContent(oldContent, isFinishing) val title = titleAndContent.first as String val content = titleAndContent.second as String PostFields(title, content) - } catch (e: EditorFragmentAbstract.EditorFragmentNotAddedException) { + } catch (e: GutenbergKitEditorFragmentBase.EditorFragmentNotAddedException) { AppLog.e(AppLog.T.EDITOR, "Impossible to save the post, we weren't able to update it.") UpdateFromEditor.Failed(e) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragment.kt index e1ba35ffae81..568a1e731d30 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragment.kt @@ -42,6 +42,7 @@ import org.wordpress.gutenberg.GutenbergView.TitleAndContentCallback import org.wordpress.gutenberg.Media import org.wordpress.gutenberg.model.EditorConfiguration import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import javax.inject.Inject class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { @@ -51,6 +52,9 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { private var gutenbergView: GutenbergView? = null private var isHtmlModeEnabled = false + @Volatile + private var editorReady = false + private val textWatcher = LiveTextWatcher() private var historyChangeListener: HistoryChangeListener? = null private var featuredImageChangeListener: FeaturedImageChangeListener? = null @@ -257,7 +261,10 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { } ) + editorReady = false + gutenbergView.setEditorDidBecomeAvailable { + editorReady = true mEditorFragmentListener.onEditorFragmentContentReady( ArrayList(), false ) @@ -434,8 +441,19 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { ) val finalResult = try { - latch.await() - result[0] + val completed = latch.await( + GET_TITLE_AND_CONTENT_TIMEOUT_SECONDS, TimeUnit.SECONDS + ) + if (!completed) { + AppLog.w( + AppLog.T.EDITOR, + "Timed out waiting for title and content from " + + "Gutenberg editor" + ) + null + } else { + result[0] + } } catch (e: InterruptedException) { AppLog.w( AppLog.T.EDITOR, @@ -446,7 +464,10 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { null } - return finalResult ?: Pair("", "") + // Surface failure to the caller as a checked exception so it can + // skip mutating the PostModel rather than persisting empty content + // over the user's existing draft. See issue #22878. + return finalResult ?: throw EditorFragmentNotAddedException() } override fun getEditorName(): String { @@ -517,6 +538,8 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { super.onDestroy() } + fun isEditorReady(): Boolean = editorReady + fun setXPostsEnabled(enabled: Boolean) { isXPostsEnabled = enabled } @@ -551,6 +574,8 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { private const val CAPTURE_PHOTO_PERMISSION_REQUEST_CODE = 101 private const val CAPTURE_VIDEO_PERMISSION_REQUEST_CODE = 102 + private const val GET_TITLE_AND_CONTENT_TIMEOUT_SECONDS = 5L + fun newInstance( configuration: EditorConfiguration, site: SiteModel