From ec683acd64ddb9d2fb810d8feacbafa6d0a96785 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 18 Aug 2026 16:37:25 +0200 Subject: [PATCH 1/2] Do not let a page that was navigated away from fail the one that replaced it `testDOCXEditMode` and `testODTEditMode` have been failing an api level at a time all week, always with "webview did not answer in 10000ms document=no result" - and `no result` is the tell: `lastDocument` is only null once something called `unload`, so the document had not merely failed to become editable, it had been given up on. Logcat from a failing run says what did it. A page that never commits leaves a reload scheduled 2.5s out - the workaround for a webview reporting progress 100 over a blank page - and nothing cancelled it when the next page was asked for. The test before had deliberately loaded a page that 404s, so its retry fired inside the next test, went back to a server that had gone with the document that owned it, and got a 404. `onReceivedHttpError` reported that against whatever document was on screen by then, which was the docx being opened for edit mode. So `loadUrl` cancels a retry the page before it left waiting, the way `destroy` already does when the whole view is replaced. And `failPage` answers only for the page it was last asked to load: a request made for a document already closed can still be answered long after, and the document on screen is not the one that failed. Nothing is loosened. `LandingTests.aDocumentThatFailsToOpenComesBackToTheList` holds the case that matters - a document whose page really is a 404 - and the whole instrumented suite, 80 tests, passes locally. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Kd9KchUHN41DQ3DUreK8ML --- .../opendocument/droid/ui/widget/PageView.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt index 0000a585c450..c7abaeffefa1 100644 --- a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt +++ b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt @@ -53,6 +53,9 @@ constructor(context: Context, attributeSet: AttributeSet?) : private var wasCommitCalled = false + /** What [loadUrl] was last given: the only page whose failure is this document's. */ + private var loadedUrl: String? = null + private var isBridgeAttached = false init { @@ -316,6 +319,14 @@ constructor(context: Context, attributeSet: AttributeSet?) : // the third party viewers an ONLINE result loads here. takes effect on the next load if (!url.startsWith(JAVASCRIPT_SCHEME)) { attachBridge(isOwnContent(url)) + + // a page that never committed left a retry waiting in onPageFinished. Now that another + // page has been asked for, that retry would load the old one back over it - and the + // document it belonged to has taken its server with it, so what it would find there is + // a 404 this page is then given up on for + buggyWebViewHandler.removeCallbacksAndMessages(null) + + loadedUrl = url } super.loadUrl(url) @@ -339,6 +350,13 @@ constructor(context: Context, attributeSet: AttributeSet?) : return } + // and only the page being shown. A request made for a document already closed can still be + // answered here, long after the page moved on, and the document on screen is not the one + // that failed + if (loadedUrl != null && url.toString() != loadedUrl) { + return + } + documentFragment.onPageFailed() } From d3df3994cb34f521c7127d2deea536018d431cac Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 18 Aug 2026 16:47:26 +0200 Subject: [PATCH 2/2] Check the retry against the page still wanted, not only the flag Cancelling on `loadUrl` clears the retries queued until then, and not the one queued after: page A can finish - and schedule its retry - once B has already been asked for. `wasCommitCalled` is about whichever page is being waited on, so if B has not committed within the 2.5s, A's retry reads B's flag, believes it is about itself, and loads A back over B. Which is the navigation this set out to stop, arrived at from the other side. So the retry asks whether the page it names is still the page wanted. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Kd9KchUHN41DQ3DUreK8ML --- .../main/java/app/opendocument/droid/ui/widget/PageView.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt index c7abaeffefa1..eabdd06b4c52 100644 --- a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt +++ b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt @@ -82,7 +82,12 @@ constructor(context: Context, attributeSet: AttributeSet?) : buggyWebViewHandler.postDelayed( { - if (!wasCommitCalled) { + // [url] and not whatever is loaded now: this callback can arrive after + // another page was asked for, which cancels the retries queued until + // then but not the one queued here. wasCommitCalled is about the page + // being waited on, so on its own it would answer for that other page + // and put this one back over it + if (!wasCommitCalled && url == loadedUrl) { crashManager.log(RuntimeException("commit was not called")) loadUrl(url)