summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/org/terst/feedreader')
-rw-r--r--app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt29
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt7
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))