From 4986da71baae1d77c7e03b8e60bd21c81f2a875a Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 27 Oct 2026 15:25:04 -0400 Subject: [PATCH 1/7] fix(android): recover the editor when its renderer process dies Unhandled, a WebView renderer death (often out-of-memory) kills the host app. Android can't reuse the WebView afterward, so replace it and load the editor again, failing pending reads. A renderer that dies before the editor loads shows the crash notice instead, avoiding a reload loop. Co-Authored-By: Claude Opus 5.5 --- .../org/wordpress/gutenberg/GutenbergView.kt | 72 +++++++++- .../GutenbergViewRenderProcessGoneTest.kt | 132 ++++++++++++++++++ 2 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index 7b70bf221..6462845f1 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -19,6 +19,7 @@ import android.view.inputmethod.InputMethodManager import android.webkit.ConsoleMessage import android.webkit.CookieManager import android.webkit.JavascriptInterface +import android.webkit.RenderProcessGoneDetail import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebResourceError @@ -96,7 +97,10 @@ const val ASSET_PATH_INDEX = "/assets/index.html" * - If `dependencies` is null, dependencies are fetched asynchronously before loading */ class GutenbergView : FrameLayout { - private val webView: WebView + private var webView: WebView + + /** Whether [webView] replaced one whose renderer died and has yet to load the editor. */ + private var isWebViewBlank = false @Volatile private var isEditorLoaded = false private var didFireEditorLoaded = false @@ -254,6 +258,9 @@ class GutenbergView : FrameLayout { /** * Provides access to the internal WebView for tests and advanced use cases. + * + * The instance is replaced when its renderer process dies, so read this + * each time rather than holding on to it. */ val editorWebView: WebView get() = webView @@ -349,10 +356,7 @@ class GutenbergView : FrameLayout { .build() // Create the internal WebView as first child (behind overlays) - webView = WebView(context).apply { - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - alpha = 0f - } + webView = createWebView() addView(webView) // Create loading overlay views @@ -389,6 +393,11 @@ class GutenbergView : FrameLayout { } } + private fun createWebView(): WebView = WebView(context).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + alpha = 0f + } + /** * Transitions to the progress bar phase (dependency fetching). */ @@ -514,6 +523,12 @@ class GutenbergView : FrameLayout { onEditorPageStarted(url) } + override fun onRenderProcessGone(view: WebView?, detail: RenderProcessGoneDetail?): Boolean { + if (view === webView) onEditorRenderProcessGone(detail?.didCrash() == true) + // Unhandled, Android kills the app along with the renderer. + return true + } + override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest @@ -695,6 +710,7 @@ class GutenbergView : FrameLayout { */ private fun loadEditor(dependencies: EditorDependencies) { this.dependencies = dependencies + isWebViewBlank = false // Derive the asset loader authority from the site URL so that the editor // document shares the site's origin, making REST API and AJAX requests @@ -1183,11 +1199,53 @@ class GutenbergView : FrameLayout { // may never arrive. failPendingTitleAndContentReads() handler.post { - showSpinnerPhase() - webView.reload() + if (isWebViewBlank) { + loadEditor(dependencies) + } else { + showSpinnerPhase() + webView.reload() + } + } + } + + /** + * The web view's renderer process died, taking the editor with it. + * + * Android can't use a web view again once its renderer dies, so it is + * replaced, and the editor loads in the new one from the content + * [LatestContentProvider] returns. A renderer that died before the editor + * loaded may die again on every load, so the crash notice offers a reload + * instead of looping. + */ + private fun onEditorRenderProcessGone(didCrash: Boolean) { + Log.e(TAG, if (didCrash) "The editor's renderer crashed" else "The system killed the editor's renderer") + val wasEditorLoaded = isEditorLoaded + isEditorLoaded = false + // The dead renderer can never answer these reads. + failPendingTitleAndContentReads() + replaceWebView() + blockInserterDialog?.dismiss() + editorDidBecomeUnavailableListener?.onEditorUnavailable(this) + if (wasEditorLoaded) { + loadEditor(dependencies) + } else { + showEditorCrashPhase() } } + private fun replaceWebView() { + val deadWebView = webView + deadWebView.animate().cancel() + removeView(deadWebView) + deadWebView.destroy() + // A file pick still open would answer the dead web view. + filePathCallback = null + + webView = createWebView() + addView(webView, 0) + isWebViewBlank = true + } + @JavascriptInterface fun onEditorContentChanged() { contentChangeListener?.onContentChanged() diff --git a/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt b/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt new file mode 100644 index 000000000..b305b3aea --- /dev/null +++ b/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt @@ -0,0 +1,132 @@ +package org.wordpress.gutenberg + +import android.os.Looper +import android.view.View +import android.webkit.RenderProcessGoneDetail +import kotlinx.coroutines.test.TestScope +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf +import org.wordpress.gutenberg.model.EditorConfiguration +import org.wordpress.gutenberg.model.EditorDependencies + +/** How the editor survives its web view's renderer process dying. */ +@RunWith(RobolectricTestRunner::class) +class GutenbergViewRenderProcessGoneTest { + + private val testScope = TestScope() + + private fun siteView() = GutenbergView( + EditorConfiguration.builder("https://example.com", "https://example.com/wp-json/").build(), + EditorDependencies.empty, + testScope, + RuntimeEnvironment.getApplication() + ).also { shadowOf(Looper.getMainLooper()).idle() } + + /** The editor document, mirroring the fallback in `loadEditor`. */ + private val editorUrl = BuildConfig.GUTENBERG_EDITOR_URL + .ifEmpty { "https://example.com/assets/index.html" } + + private fun loadedSiteView() = siteView().also { view -> + view.onEditorLoaded() + shadowOf(Looper.getMainLooper()).idle() + } + + private fun killRenderer(view: GutenbergView): Boolean { + val detail = mock(RenderProcessGoneDetail::class.java) + `when`(detail.didCrash()).thenReturn(true) + val webView = view.editorWebView + val handled = webView.webViewClient.onRenderProcessGone(webView, detail) + shadowOf(Looper.getMainLooper()).idle() + return handled + } + + @Test + fun `a renderer death is handled so the app survives it`() { + val view = loadedSiteView() + + assertTrue("an unhandled renderer death kills the app", killRenderer(view)) + } + + @Test + fun `a renderer death replaces the web view`() { + val view = loadedSiteView() + val deadWebView = view.editorWebView + + killRenderer(view) + + assertNotSame("a web view can't be used once its renderer dies", deadWebView, view.editorWebView) + assertNull("the dead web view leaves the hierarchy", deadWebView.parent) + assertSame(view, view.editorWebView.parent) + assertEquals("the new web view stays behind the overlays", 0, view.indexOfChild(view.editorWebView)) + } + + @Test + fun `a renderer death fails reads waiting on the dead renderer`() { + val view = loadedSiteView() + val callback = RecordingTitleAndContentCallback() + // Robolectric's shadow WebView never invokes the ValueCallback, like a + // renderer that dies before it answers. + view.getTitleAndContent("original content", callback) + shadowOf(Looper.getMainLooper()).idle() + + killRenderer(view) + + assertTrue("the host is told the editor is not ready", callback.errors.single() is EditorNotReadyException) + } + + @Test + fun `a renderer death tells the host the editor is unavailable`() { + val view = loadedSiteView() + var unavailable = false + view.setEditorDidBecomeUnavailable { unavailable = true } + + killRenderer(view) + + assertTrue(unavailable) + } + + @Test + fun `a renderer death after the editor loaded loads it again`() { + val view = loadedSiteView() + + killRenderer(view) + + assertEquals(editorUrl, shadowOf(view.editorWebView).lastLoadedUrl) + assertEquals(View.VISIBLE, view.editorWebView.visibility) + } + + @Test + fun `a renderer death before the editor loaded offers a reload instead of looping`() { + val view = siteView() + + killRenderer(view) + + assertNull("loading again may kill the renderer again", shadowOf(view.editorWebView).lastLoadedUrl) + assertEquals("the crash notice covers the editor", View.INVISIBLE, view.editorWebView.visibility) + + view.reloadEditor() + shadowOf(Looper.getMainLooper()).idle() + + assertEquals("the blank web view has no page to reload", editorUrl, shadowOf(view.editorWebView).lastLoadedUrl) + } + + private class RecordingTitleAndContentCallback : GutenbergView.TitleAndContentCallback { + val errors = mutableListOf() + + override fun onResult(title: CharSequence, content: CharSequence) = Unit + + override fun onError(error: Throwable) { + errors += error + } + } +} From 2b8d66465b03f64a6fed8ba91b5490cc37b9fecd Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 27 Oct 2026 16:10:03 -0400 Subject: [PATCH 2/7] fix(android): mark the renderer-gone handler as requiring API 26 RenderProcessGoneDetail.didCrash() is API 26, above the library's minSdk of 24, so Android Lint reported a NewApi error. Android only calls the handler from API 26. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuj1TpMKFBGz2r4RHx --- .../src/main/java/org/wordpress/gutenberg/GutenbergView.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index 6462845f1..928b8aec2 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -9,6 +9,7 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper @@ -31,6 +32,7 @@ import android.webkit.WebViewClient import android.widget.FrameLayout import android.widget.ProgressBar import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewAssetLoader.AssetsPathHandler @@ -523,6 +525,8 @@ class GutenbergView : FrameLayout { onEditorPageStarted(url) } + // Android only calls this from API 26, where it was added. + @RequiresApi(Build.VERSION_CODES.O) override fun onRenderProcessGone(view: WebView?, detail: RenderProcessGoneDetail?): Boolean { if (view === webView) onEditorRenderProcessGone(detail?.didCrash() == true) // Unhandled, Android kills the app along with the renderer. From 9fa6aa95a76db4eb3b1d4c68f94d5a4d607ead49 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 27 Oct 2026 16:10:59 -0400 Subject: [PATCH 3/7] fix(android): recover automatically from one renderer death at a time The editor reports itself loaded on first paint, before images and embeds finish rendering, so content that runs the renderer out of memory after load reloaded forever. A second death after an automatic recovery now shows the crash notice; reloading from it re-arms recovery. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuj1TpMKFBGz2r4RHx --- .../org/wordpress/gutenberg/GutenbergView.kt | 18 ++++++++--- .../GutenbergViewRenderProcessGoneTest.kt | 30 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index 928b8aec2..7716fbe90 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -103,6 +103,14 @@ class GutenbergView : FrameLayout { /** Whether [webView] replaced one whose renderer died and has yet to load the editor. */ private var isWebViewBlank = false + + /** + * Whether the editor reloaded on its own after a renderer death, since + * the user last chose to reload it. The editor reports itself loaded + * before its content finishes rendering, so content that kills the + * renderer would otherwise reload forever. + */ + private var hasRecoveredAutomatically = false @Volatile private var isEditorLoaded = false private var didFireEditorLoaded = false @@ -1199,6 +1207,7 @@ class GutenbergView : FrameLayout { */ internal fun reloadEditor() { isEditorLoaded = false + hasRecoveredAutomatically = false // The reload replaces the page these reads were sent to, so their results // may never arrive. failPendingTitleAndContentReads() @@ -1217,9 +1226,9 @@ class GutenbergView : FrameLayout { * * Android can't use a web view again once its renderer dies, so it is * replaced, and the editor loads in the new one from the content - * [LatestContentProvider] returns. A renderer that died before the editor - * loaded may die again on every load, so the crash notice offers a reload - * instead of looping. + * [LatestContentProvider] returns. That happens once: a renderer that dies + * before the editor loads, or again after an automatic recovery, may die + * on every load, so the crash notice offers a reload instead of looping. */ private fun onEditorRenderProcessGone(didCrash: Boolean) { Log.e(TAG, if (didCrash) "The editor's renderer crashed" else "The system killed the editor's renderer") @@ -1230,7 +1239,8 @@ class GutenbergView : FrameLayout { replaceWebView() blockInserterDialog?.dismiss() editorDidBecomeUnavailableListener?.onEditorUnavailable(this) - if (wasEditorLoaded) { + if (wasEditorLoaded && !hasRecoveredAutomatically) { + hasRecoveredAutomatically = true loadEditor(dependencies) } else { showEditorCrashPhase() diff --git a/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt b/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt index b305b3aea..4911186aa 100644 --- a/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt +++ b/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt @@ -120,6 +120,36 @@ class GutenbergViewRenderProcessGoneTest { assertEquals("the blank web view has no page to reload", editorUrl, shadowOf(view.editorWebView).lastLoadedUrl) } + @Test + fun `a renderer death after an automatic recovery offers a reload instead of looping`() { + val view = loadedSiteView() + killRenderer(view) + view.onEditorLoaded() + shadowOf(Looper.getMainLooper()).idle() + + killRenderer(view) + + assertNull("content that kills the renderer once loaded would reload forever", shadowOf(view.editorWebView).lastLoadedUrl) + assertEquals("the crash notice covers the editor", View.INVISIBLE, view.editorWebView.visibility) + } + + @Test + fun `reloading from the crash notice allows another automatic recovery`() { + val view = loadedSiteView() + killRenderer(view) + view.onEditorLoaded() + shadowOf(Looper.getMainLooper()).idle() + killRenderer(view) + + view.reloadEditor() + shadowOf(Looper.getMainLooper()).idle() + view.onEditorLoaded() + shadowOf(Looper.getMainLooper()).idle() + killRenderer(view) + + assertEquals(editorUrl, shadowOf(view.editorWebView).lastLoadedUrl) + } + private class RecordingTitleAndContentCallback : GutenbergView.TitleAndContentCallback { val errors = mutableListOf() From 1c10e3952f5e1fd83f48657c90063696801f53f1 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 27 Oct 2026 16:11:09 -0400 Subject: [PATCH 4/7] docs(android): note hosts must handle renderer deaths in their WebViews An app's WebViews share one renderer, so GutenbergView recovering alone doesn't keep the app alive while a host WebView leaves the death unhandled. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuj1TpMKFBGz2r4RHx --- .../main/java/org/wordpress/gutenberg/GutenbergView.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index 7716fbe90..9666b100e 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -97,6 +97,14 @@ const val ASSET_PATH_INDEX = "/assets/index.html" * * - If `dependencies` is provided, the editor loads immediately (fast path) * - If `dependencies` is null, dependencies are fetched asynchronously before loading + * + * ## Renderer Crashes + * + * The view recovers when its WebView's renderer process dies, rather than + * letting Android kill the app. An app's WebViews usually share one renderer, + * though, and Android still kills the app if any WebView alive at the time + * leaves `WebViewClient.onRenderProcessGone` unhandled. Hosts must handle it + * in their own WebViews for the app to survive. */ class GutenbergView : FrameLayout { private var webView: WebView From 0693946cbeb62b8bad7470c878265242e418b3d7 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 30 Oct 2026 07:41:49 -0400 Subject: [PATCH 5/7] docs(android): note renderer deaths also make the editor unavailable The listener's KDoc named the error boundary as the only cause and promised a reload notice, but a renderer death now notifies it too and may reload automatically. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuj1TpMKFBGz2r4RHx --- .../org/wordpress/gutenberg/GutenbergView.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index 9666b100e..cea3c6b94 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -1016,16 +1016,17 @@ class GutenbergView : FrameLayout { * [EditorAvailableListener.onEditorAvailable] when the editor crashes before * it finishes loading. * - * The editor's error boundary caught an error and replaced the editor with an - * error message. React unmounted the editor, which deleted every JavaScript - * `editor` API, so calls to them are refused from this point until the editor - * reloads. + * Either the editor's error boundary caught an error and React unmounted the + * editor, or the web view's renderer process died. Both remove every + * JavaScript `editor` API, so calls to them are refused from this point until + * the editor reloads. * - * GutenbergKit covers the editor with a notice offering to reload it. Until it - * reloads, hosts should disable the controls that depend on the editor — - * history, editor mode — while leaving those that read from their own - * persisted copy, such as saving and closing, available. Re-enable them the - * next time [EditorAvailableListener.onEditorAvailable] is called. + * GutenbergKit either reloads the editor automatically after a renderer death + * or covers it with a notice offering to reload it. Until it reloads, hosts + * should disable the controls that depend on the editor — history, editor + * mode — while leaving those that read from their own persisted copy, such as + * saving and closing, available. Re-enable them the next time + * [EditorAvailableListener.onEditorAvailable] is called. */ fun interface EditorUnavailableListener { fun onEditorUnavailable(view: GutenbergView?) From 5555d6cfcbddcad83e9bbf2a75cad9513e6187b7 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 30 Oct 2026 08:06:08 -0400 Subject: [PATCH 6/7] fix(android): report open dialogs closed when the renderer dies A dead renderer can't unmount the editor's dialogs to report them closed, so hosts kept navigation disabled and Back was refused on the crash notice. This mirrors iOS, which reports them closed when its web content process terminates. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuj1TpMKFBGz2r4RHx --- .../org/wordpress/gutenberg/GutenbergView.kt | 9 ++++++++ .../GutenbergViewRenderProcessGoneTest.kt | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index cea3c6b94..c68d6fad5 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -257,6 +257,9 @@ class GutenbergView : FrameLayout { private var logJsExceptionListener: LogJsExceptionListener? = null private var autocompleterTriggeredListener: AutocompleterTriggeredListener? = null private var modalDialogStateListener: ModalDialogStateListener? = null + + /** Modal dialogs the editor reports open, so a renderer death can report them closed. */ + private val openModalDialogs = mutableSetOf() private var networkRequestListener: NetworkRequestListener? = null private var latestContentProvider: LatestContentProvider? = null private var blockInserterDialog: BlockPickerDialog? = null @@ -1247,6 +1250,10 @@ class GutenbergView : FrameLayout { failPendingTitleAndContentReads() replaceWebView() blockInserterDialog?.dismiss() + // The dead renderer can't unmount the editor's open dialogs to report + // them closed, so report them here rather than leave navigation blocked. + openModalDialogs.forEach { modalDialogStateListener?.onModalDialogClosed(it) } + openModalDialogs.clear() editorDidBecomeUnavailableListener?.onEditorUnavailable(this) if (wasEditorLoaded && !hasRecoveredAutomatically) { hasRecoveredAutomatically = true @@ -1418,6 +1425,7 @@ class GutenbergView : FrameLayout { @JavascriptInterface fun onModalDialogOpened(dialogType: String) { handler.post { + openModalDialogs.add(dialogType) modalDialogStateListener?.onModalDialogOpened(dialogType) } } @@ -1425,6 +1433,7 @@ class GutenbergView : FrameLayout { @JavascriptInterface fun onModalDialogClosed(dialogType: String) { handler.post { + openModalDialogs.remove(dialogType) modalDialogStateListener?.onModalDialogClosed(dialogType) } } diff --git a/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt b/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt index 4911186aa..4dde7561d 100644 --- a/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt +++ b/android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewRenderProcessGoneTest.kt @@ -95,6 +95,28 @@ class GutenbergViewRenderProcessGoneTest { assertTrue(unavailable) } + @Test + fun `a renderer death reports the editor's open dialogs closed`() { + val view = loadedSiteView() + val closed = mutableListOf() + view.setModalDialogStateListener(object : GutenbergView.ModalDialogStateListener { + override fun onModalDialogOpened(dialogType: String) = Unit + + override fun onModalDialogClosed(dialogType: String) { + closed += dialogType + } + }) + view.onModalDialogOpened("block-inspector") + view.onModalDialogOpened("block-inserter") + view.onModalDialogClosed("block-inserter") + shadowOf(Looper.getMainLooper()).idle() + closed.clear() + + killRenderer(view) + + assertEquals("the host would keep its navigation disabled", listOf("block-inspector"), closed) + } + @Test fun `a renderer death after the editor loaded loads it again`() { val view = loadedSiteView() From 8ad4c4fdac7e74b3e55a0e319485aaf2b532ad35 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 30 Oct 2026 08:34:12 -0400 Subject: [PATCH 7/7] docs(android): remove a redundant API level comment The @RequiresApi annotation already states the API level. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuj1TpMKFBGz2r4RHx --- .../src/main/java/org/wordpress/gutenberg/GutenbergView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index c68d6fad5..de18073e5 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -544,7 +544,6 @@ class GutenbergView : FrameLayout { onEditorPageStarted(url) } - // Android only calls this from API 26, where it was added. @RequiresApi(Build.VERSION_CODES.O) override fun onRenderProcessGone(view: WebView?, detail: RenderProcessGoneDetail?): Boolean { if (view === webView) onEditorRenderProcessGone(detail?.didCrash() == true)