diff options
Diffstat (limited to 'app/src/main/java/org')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt | 29 | ||||
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt | 7 |
2 files changed, 34 insertions, 2 deletions
diff --git a/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt b/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt index 9be08b0..286588a 100644 --- a/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt +++ b/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt @@ -2,6 +2,8 @@ package org.terst.feedreader.readability import android.annotation.SuppressLint import android.content.Context +import android.os.Handler +import android.os.Looper import android.webkit.WebView import android.webkit.WebViewClient import kotlinx.coroutines.Dispatchers @@ -46,7 +48,16 @@ object ReaderExtractor { val webView = WebView(context) webView.settings.javaScriptEnabled = true + // A slow-loading page's evaluateJavascript callback can arrive right as the + // outer withTimeoutOrNull cancels this coroutine, racing finish() (JS-callback + // path) against invokeOnCancellation (timeout path). Both used to call + // webView.destroy() unconditionally -- a double destroy() crashes natively. + // This flag makes teardown happen exactly once no matter which path wins. + var finished = false + fun finish(result: ExtractedArticle?) { + if (finished) return + finished = true if (continuation.isActive) continuation.resume(result) webView.stopLoading() webView.destroy() @@ -89,7 +100,23 @@ object ReaderExtractor { } } - continuation.invokeOnCancellation { webView.destroy() } + // withTimeoutOrNull's cancellation is driven by a delayed task on + // kotlinx.coroutines.DefaultExecutor, NOT the calling coroutine's dispatcher -- + // so this handler can run on a background thread despite the whole extraction + // being wrapped in withContext(Dispatchers.Main). Touching the WebView there + // crashes ("A WebView method was called on thread ..."), which is exactly what + // a slow/never-finishing article load (timing out after TIMEOUT_MS) hit every + // time -- reproduced here as the real "crash loop" root cause. Hop back to the + // main looper before tearing down. + continuation.invokeOnCancellation { + Handler(Looper.getMainLooper()).post { + if (!finished) { + finished = true + webView.stopLoading() + webView.destroy() + } + } + } webView.loadUrl(url) } } 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)) |
