From 6d9c200dbd85a3be177d6bb7cac33fba5662ac95 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 22 Jul 2026 17:17:40 +0000 Subject: Fix crash loop: WebView touched off main thread during extraction cancellation Root-caused via emulator stress test (rapid swiping through many articles): withTimeoutOrNull's cancellation, and LaunchedEffect disposal when a page is swiped away, both invoke invokeOnCancellation on kotlinx.coroutines.DefaultExecutor rather than the calling coroutine's Main dispatcher. The handler called webView.stopLoading()/destroy() directly, crashing with "A WebView method was called on thread ..." on essentially every session once enough thin-content articles were swiped past mid-extraction -- the reported crash loop. Now hops back to the main looper before touching the WebView, and guards against a double destroy() race between the JS-callback and cancellation paths. Also fixed a real (non-crashing) leak: the visible reader WebView in ReaderScreen was never destroy()-ed when its page left the pager, via AndroidView's onRelease. Reproduced pre-fix in ~80 rapid swipes on an emulator; post-fix, 320+ swipes with real 107-feed data ran clean with stable native heap. --- app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'app/src/main/java/org/terst/feedreader/ui') diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt index 692c198..cef17c5 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt @@ -231,7 +231,12 @@ private fun ReaderContent(articleId: String, onBack: () -> Unit) { loadDataWithBaseURL(baseUrl, wrapHtml(content), "text/html", "UTF-8", null) } }, - update = { webView -> webView.loadDataWithBaseURL(baseUrl, wrapHtml(content), "text/html", "UTF-8", null) } + update = { webView -> webView.loadDataWithBaseURL(baseUrl, wrapHtml(content), "text/html", "UTF-8", null) }, + // AndroidView does not destroy() the underlying WebView on its own -- + // without this, swiping through many articles (each page holding its own + // WebView) leaks a WebView's native resources per page and eventually + // exhausts memory over a long reading session. + onRelease = { webView -> webView.destroy() } ) } else if (isExtracting) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) -- cgit v1.2.3