Skip to content

Commit 5f2258e

Browse files
BrayanDSOlukstbit
authored andcommitted
fix(anki pages): don't crash after onRenderProcessGone
tested with the following patch: ```patch Subject: [PATCH] testing --- Index: AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt (revision a3e86d739bf9049cd6673076a1ec5bb2beeac4ee) +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt (date 1762546566117) @@ -37,6 +37,7 @@ import com.ichi2.anki.R import com.ichi2.anki.ViewerResourceHandler import com.ichi2.anki.dialogs.TtsVoicesDialogFragment +import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.localizedErrorMessage import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.utils.ext.collectIn @@ -48,6 +49,7 @@ import com.ichi2.compat.CompatHelper.Companion.resolveActivityCompat import com.ichi2.themes.Themes import com.ichi2.utils.show +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -69,6 +71,13 @@ viewModel.eval.collectIn(lifecycleScope) { eval -> webViewLayout.evaluateJavascript(eval) } + + launchCatchingTask { + (0..2).forEach { _ -> + delay(3000) + webViewLayout.loadUrl("chrome://crash") + } + } } override fun onStart() { Index: AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt (revision a3e86d739bf9049cd6673076a1ec5bb2beeac4ee) +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt (date 1762546610210) @@ -31,9 +31,11 @@ import com.google.android.material.progressindicator.CircularProgressIndicator import com.ichi2.anki.R import com.ichi2.anki.SingleFragmentActivity +import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.workarounds.OnWebViewRecreatedListener import com.ichi2.anki.workarounds.SafeWebViewLayout import com.ichi2.themes.Themes +import kotlinx.coroutines.delay import timber.log.Timber import kotlin.reflect.KClass @@ -118,6 +120,13 @@ } setupWebView(savedInstanceState) + + launchCatchingTask { + (0..2).forEach { _ -> + delay(3000) + webViewLayout.loadUrl("chrome://crash") + } + } } private fun setupWebView(savedInstanceState: Bundle?) { ```
1 parent 35ea10d commit 5f2258e

12 files changed

Lines changed: 90 additions & 65 deletions

File tree

AnkiDroid/src/main/java/com/ichi2/anki/pages/CongratsPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class CongratsPage :
7676
// typically due to 'day rollover'
7777
if (changes.studyQueues) {
7878
Timber.i("refreshing: study queues updated")
79-
webView.reload()
79+
webViewLayout.reload()
8080
}
8181
}
8282

AnkiDroid/src/main/java/com/ichi2/anki/pages/DeckOptions.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class DeckOptions : PageFragment() {
5757
object : OnBackPressedCallback(false) {
5858
override fun handleOnBackPressed() {
5959
Timber.v("webView: navigating back")
60-
webView.goBack()
60+
webViewLayout.goBack()
6161
}
6262
}
6363

@@ -71,7 +71,7 @@ class DeckOptions : PageFragment() {
7171
override fun handleOnBackPressed() {
7272
Timber.v("DeckOptions: requesting the webview to handle the user close request.")
7373
if (webViewIsReady) {
74-
webView.evaluateJavascript("anki.deckOptionsPendingChanges()") {
74+
webViewLayout.evaluateJavascript("anki.deckOptionsPendingChanges()") {
7575
// Callback is handled in the WebView:
7676
// * A 'discard changes' dialog may be shown, using confirm()
7777
// * if no changes, or changes discarded, `deckOptionsRequireClose` is called
@@ -115,7 +115,7 @@ class DeckOptions : PageFragment() {
115115
override fun handleOnBackPressed() {
116116
Timber.i("back button: closing displayed modal")
117117
try {
118-
webView.evaluateJavascript(
118+
webViewLayout.evaluateJavascript(
119119
"""
120120
document.getElementsByClassName("modal show")[0]
121121
.getElementsByClassName("btn-close")[0].click()
@@ -160,18 +160,18 @@ class DeckOptions : PageFragment() {
160160
super.onViewCreated(view, savedInstanceState)
161161
}
162162

163-
override fun onWebViewCreated(webView: WebView) {
163+
override fun onWebViewCreated() {
164164
// addJavascriptInterface needs to happen before loadUrl
165-
webView.addJavascriptInterface(ModalJavaScriptInterfaceListener(), "ankidroid")
165+
webViewLayout.addJavascriptInterface(ModalJavaScriptInterfaceListener(), "ankidroid")
166166
Timber.d("Added JS Interface: 'ankidroid")
167167
}
168168

169169
@NeedsTest("going back on a manual page takes priority over closing a modal")
170170
override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient {
171-
requireActivity().onBackPressedDispatcher.addCallback(this, onBackFromDeckOptions)
172-
requireActivity().onBackPressedDispatcher.addCallback(this, onBackFromModal)
171+
activity?.onBackPressedDispatcher?.addCallback(this, onBackFromDeckOptions)
172+
activity?.onBackPressedDispatcher?.addCallback(this, onBackFromModal)
173173
// going back on a manual page takes priority over closing a modal
174-
requireActivity().onBackPressedDispatcher.addCallback(this, onBackFromManual)
174+
activity?.onBackPressedDispatcher?.addCallback(this, onBackFromManual)
175175

176176
return object : PageWebViewClient() {
177177
private val ankiManualHostRegex = Regex("^docs\\.ankiweb\\.net\$")
@@ -228,14 +228,14 @@ class DeckOptions : PageFragment() {
228228
val openJs = getListenerJs("shown.bs.modal", "open")
229229
val closeJs = getListenerJs("hidden.bs.modal", "close")
230230

231-
webView.evaluateJavascript(openJs, {})
232-
webView.evaluateJavascript(closeJs, {})
231+
webViewLayout.evaluateJavascript(openJs, {})
232+
webViewLayout.evaluateJavascript(closeJs, {})
233233
}
234234

235235
fun onWebViewReady() {
236236
Timber.d("WebView ready to receive input")
237237
webViewIsReady = true
238-
webView.isVisible = true
238+
webViewLayout.isVisible = true
239239
pageLoadingIndicator.isVisible = false
240240
}
241241

AnkiDroid/src/main/java/com/ichi2/anki/pages/ImageOcclusion.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class ImageOcclusion :
7575
view.findViewById<MaterialToolbar>(R.id.toolbar).setOnMenuItemClickListener {
7676
if (it.itemId == R.id.action_save) {
7777
Timber.i("save item selected")
78-
webView.evaluateJavascript("anki.imageOcclusion.save()") {
78+
webViewLayout.evaluateJavascript("anki.imageOcclusion.save()") {
7979
// reset to the previous deck that the backend "saw" as selected, this
8080
// avoids other screens unexpectedly having their working decks modified(
8181
// most important being the Reviewer where the user would find itself

AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import com.google.android.material.appbar.MaterialToolbar
3131
import com.google.android.material.progressindicator.CircularProgressIndicator
3232
import com.ichi2.anki.R
3333
import com.ichi2.anki.SingleFragmentActivity
34+
import com.ichi2.anki.workarounds.OnWebViewRecreatedListener
35+
import com.ichi2.anki.workarounds.SafeWebViewLayout
3436
import com.ichi2.themes.Themes
3537
import timber.log.Timber
3638
import kotlin.reflect.KClass
@@ -41,8 +43,9 @@ import kotlin.reflect.KClass
4143
open class PageFragment(
4244
@LayoutRes contentLayoutId: Int = R.layout.page_fragment,
4345
) : Fragment(contentLayoutId),
44-
PostRequestHandler {
45-
lateinit var webView: WebView
46+
PostRequestHandler,
47+
OnWebViewRecreatedListener {
48+
lateinit var webViewLayout: SafeWebViewLayout
4649
private lateinit var server: AnkiServer
4750

4851
/**
@@ -63,7 +66,7 @@ open class PageFragment(
6366
*/
6467
protected open fun onCreateWebViewClient(savedInstanceState: Bundle?) = PageWebViewClient()
6568

66-
protected open fun onWebViewCreated(webView: WebView) { }
69+
protected open fun onWebViewCreated() { }
6770

6871
/**
6972
* When the webview calls `BridgeCommand("foo")`, the PageFragment execute `bridgeCommands["foo"]`.
@@ -78,7 +81,7 @@ open class PageFragment(
7881
if (bridgeCommands.isEmpty()) {
7982
return
8083
}
81-
webView.addJavascriptInterface(
84+
webViewLayout.addJavascriptInterface(
8285
object : Any() {
8386
@JavascriptInterface
8487
fun bridgeCommandImpl(request: String) {
@@ -101,39 +104,42 @@ open class PageFragment(
101104
view: View,
102105
savedInstanceState: Bundle?,
103106
) {
104-
val pageWebViewClient = onCreateWebViewClient(savedInstanceState)
105107
server = AnkiServer(this).also { it.start() }
106-
webView =
107-
view.findViewById<WebView>(R.id.webview).apply {
108-
with(settings) {
109-
javaScriptEnabled = true
110-
displayZoomControls = false
111-
builtInZoomControls = true
112-
setSupportZoom(true)
113-
}
114-
webViewClient = pageWebViewClient
115-
webChromeClient = PageChromeClient()
116-
}
117-
setupBridgeCommand(pageWebViewClient)
118-
onWebViewCreated(webView)
119-
120-
val arguments = requireArguments()
121-
val path = requireNotNull(arguments.getString(PATH_ARG_KEY)) { "'$PATH_ARG_KEY' missing" }
122-
val title = arguments.getString(TITLE_ARG_KEY)
123-
124-
val nightMode = if (Themes.currentTheme.isNightMode) "#night" else ""
125-
val url = "${server.baseUrl()}$path$nightMode".toUri()
126-
Timber.i("Loading $url")
127-
webView.loadUrl(url.toString())
108+
webViewLayout = view.findViewById(R.id.webview_layout)
128109

129-
view.findViewById<MaterialToolbar>(R.id.toolbar).apply {
110+
val title = requireArguments().getString(TITLE_ARG_KEY)
111+
view.findViewById<MaterialToolbar>(R.id.toolbar)?.apply {
130112
if (title != null) {
131113
setTitle(title)
132114
}
133115
setNavigationOnClickListener {
134116
requireActivity().onBackPressedDispatcher.onBackPressed()
135117
}
136118
}
119+
120+
setupWebView(savedInstanceState)
121+
}
122+
123+
private fun setupWebView(savedInstanceState: Bundle?) {
124+
val pageWebViewClient = onCreateWebViewClient(savedInstanceState)
125+
webViewLayout.apply {
126+
setAcceptThirdPartyCookies(true)
127+
with(settings) {
128+
javaScriptEnabled = true
129+
displayZoomControls = false
130+
builtInZoomControls = true
131+
setSupportZoom(true)
132+
}
133+
setWebViewClient(pageWebViewClient)
134+
setWebChromeClient(PageChromeClient())
135+
setupBridgeCommand(pageWebViewClient)
136+
onWebViewCreated()
137+
}
138+
val path = requireNotNull(requireArguments().getString(PATH_ARG_KEY)) { "'$PATH_ARG_KEY' missing" }
139+
val nightMode = if (Themes.currentTheme.isNightMode) "#night" else ""
140+
val url = "${server.baseUrl()}$path$nightMode".toUri()
141+
Timber.i("Loading $url")
142+
webViewLayout.loadUrl(url.toString())
137143
}
138144

139145
override suspend fun handlePostRequest(
@@ -156,6 +162,10 @@ open class PageFragment(
156162
super.onDestroyView()
157163
}
158164

165+
override fun onWebViewRecreated(webView: WebView) {
166+
setupWebView(null)
167+
}
168+
159169
companion object {
160170
const val PATH_ARG_KEY = "path"
161171
const val TITLE_ARG_KEY = "title"

AnkiDroid/src/main/java/com/ichi2/anki/pages/PageWebViewClient.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ import android.webkit.ValueCallback
2020
import android.webkit.WebResourceRequest
2121
import android.webkit.WebResourceResponse
2222
import android.webkit.WebView
23-
import android.webkit.WebViewClient
2423
import androidx.core.view.isVisible
2524
import com.google.android.material.color.MaterialColors
2625
import com.ichi2.anki.OnPageFinishedCallback
26+
import com.ichi2.anki.workarounds.SafeWebViewClient
27+
import com.ichi2.anki.workarounds.SafeWebViewLayout
2728
import com.ichi2.utils.AssetHelper.guessMimeType
2829
import com.ichi2.utils.toRGBHex
2930
import timber.log.Timber
@@ -33,7 +34,7 @@ import java.io.IOException
3334
/**
3435
* Base WebViewClient to be used on [PageFragment]
3536
*/
36-
open class PageWebViewClient : WebViewClient() {
37+
open class PageWebViewClient : SafeWebViewClient() {
3738
val onPageFinishedCallbacks: MutableList<OnPageFinishedCallback> = mutableListOf()
3839

3940
override fun shouldInterceptRequest(
@@ -93,6 +94,7 @@ open class PageWebViewClient : WebViewClient() {
9394
open fun onShowWebView(webView: WebView) {
9495
Timber.v("Displaying WebView")
9596
webView.isVisible = true
97+
(webView.parent as? SafeWebViewLayout)?.isVisible = true
9698
}
9799

98100
override fun onPageFinished(

AnkiDroid/src/main/java/com/ichi2/anki/pages/RemoveAccountFragment.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import android.os.Bundle
2020
import android.view.View
2121
import android.webkit.WebResourceRequest
2222
import android.webkit.WebView
23-
import android.webkit.WebViewClient
2423
import androidx.annotation.CallSuper
2524
import androidx.core.view.isVisible
2625
import androidx.fragment.app.Fragment
2726
import com.google.android.material.appbar.MaterialToolbar
2827
import com.ichi2.anki.R
2928
import com.ichi2.anki.common.annotations.NeedsTest
29+
import com.ichi2.anki.workarounds.SafeWebViewClient
30+
import com.ichi2.anki.workarounds.SafeWebViewLayout
3031
import timber.log.Timber
3132

3233
/**
@@ -48,7 +49,7 @@ import timber.log.Timber
4849
*/
4950
@NeedsTest("pressing 'back' on this screen closes it")
5051
class RemoveAccountFragment : Fragment(R.layout.page_fragment) {
51-
private lateinit var webView: WebView
52+
private lateinit var webViewLayout: SafeWebViewLayout
5253

5354
/**
5455
* A count of the redirects performed, to ensure we don't get into an infinite loop
@@ -70,7 +71,7 @@ class RemoveAccountFragment : Fragment(R.layout.page_fragment) {
7071
}
7172

7273
Timber.i("redirecting to 'remove account'")
73-
webView.loadUrl(getString(R.string.remove_account_url))
74+
webViewLayout.loadUrl(getString(R.string.remove_account_url))
7475
return true
7576
}
7677

@@ -79,17 +80,17 @@ class RemoveAccountFragment : Fragment(R.layout.page_fragment) {
7980
view: View,
8081
savedInstanceState: Bundle?,
8182
) {
82-
webView =
83-
view.findViewById<WebView>(R.id.webview).apply {
83+
webViewLayout =
84+
view.findViewById<SafeWebViewLayout>(R.id.webview_layout).apply {
8485
isVisible = true
8586
with(settings) {
8687
javaScriptEnabled = true
8788
displayZoomControls = false
8889
builtInZoomControls = true
8990
setSupportZoom(true)
9091
}
91-
webViewClient =
92-
object : WebViewClient() {
92+
val webViewClient =
93+
object : SafeWebViewClient() {
9394
override fun shouldOverrideUrlLoading(
9495
view: WebView?,
9596
request: WebResourceRequest?,
@@ -119,12 +120,13 @@ class RemoveAccountFragment : Fragment(R.layout.page_fragment) {
119120
maybeRedirectToRemoveAccount(url)
120121
}
121122
}
123+
setWebViewClient(webViewClient)
122124
}
123125

124126
// BUG: custom sync server doesn't use this URL
125127
val url = getString(R.string.remove_account_url)
126128
Timber.i("Loading '$url'")
127-
webView.loadUrl(url)
129+
webViewLayout.loadUrl(url)
128130

129131
view.findViewById<MaterialToolbar?>(R.id.toolbar)?.apply {
130132
title = getString(R.string.remove_account)

AnkiDroid/src/main/java/com/ichi2/anki/pages/Statistics.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ class Statistics :
4646
savedInstanceState: Bundle?,
4747
) {
4848
super.onViewCreated(view, savedInstanceState)
49-
webView.isNestedScrollingEnabled = true
5049

5150
binding.deckName.setOnClickListener { startDeckSelection(all = false, filtered = false) }
5251
binding.appBar
@@ -79,7 +78,7 @@ class Statistics :
7978
val printManager = getSystemService(requireContext(), PrintManager::class.java)
8079
val currentDateTime = getTimestamp(TimeManager.time)
8180
val jobName = "${getString(R.string.app_name)}-stats-$currentDateTime"
82-
val printAdapter = webView.createPrintDocumentAdapter(jobName)
81+
val printAdapter = webViewLayout.createPrintDocumentAdapter(jobName)
8382
printManager?.print(
8483
jobName,
8584
printAdapter,
@@ -114,7 +113,7 @@ class Statistics :
114113
textBox.dispatchEvent(new Event("input", { bubbles: true }));
115114
textBox.dispatchEvent(new Event("change"));
116115
""".trimIndent()
117-
webView.evaluateJavascript(javascriptCode, null)
116+
webViewLayout.evaluateJavascript(javascriptCode, null)
118117
}
119118

120119
companion object {

AnkiDroid/src/main/java/com/ichi2/anki/workarounds/NestedScrollingWebView.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,14 @@ class NestedScrollingWebView
191191
velocityY: Float,
192192
) = childHelper.dispatchNestedPreFling(velocityX, velocityY)
193193
}
194+
195+
class NestedScrollingSafeWebViewLayout : SafeWebViewLayout {
196+
constructor(context: Context) : this(context, null)
197+
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
198+
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
199+
200+
override fun createWebView(): NestedScrollingWebView =
201+
NestedScrollingWebView(context).also {
202+
it.isNestedScrollingEnabled = true
203+
}
204+
}

AnkiDroid/src/main/java/com/ichi2/anki/workarounds/SafeWebViewLayout.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import androidx.fragment.app.findFragment
2828
import com.ichi2.anki.BuildConfig
2929
import timber.log.Timber
3030

31-
class SafeWebViewLayout :
31+
open class SafeWebViewLayout :
3232
FrameLayout,
3333
OnRenderProcessGoneListener {
3434
constructor(context: Context) : this(context, null)
@@ -43,7 +43,7 @@ class SafeWebViewLayout :
4343
field = value
4444
}
4545

46-
private fun createWebView() = WebView(context)
46+
protected open fun createWebView() = WebView(context)
4747

4848
init {
4949
addView(webView, webViewLayoutParams)
@@ -99,6 +99,8 @@ class SafeWebViewLayout :
9999

100100
fun destroy() = webView.destroy()
101101

102+
fun createPrintDocumentAdapter(documentName: String) = webView.createPrintDocumentAdapter(documentName)
103+
102104
override fun setOnScrollChangeListener(l: OnScrollChangeListener?) = webView.setOnScrollChangeListener(l)
103105

104106
override fun onRenderProcessGone(webView: WebView) {

AnkiDroid/src/main/res/layout/image_occlusion.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
android:layout_width="match_parent"
2828
android:layout_height="wrap_content"
2929
app:layout_constraintTop_toTopOf="parent"
30-
app:layout_constraintBottom_toTopOf="@id/webview">
30+
app:layout_constraintBottom_toTopOf="@id/webview_layout">
3131

3232
<com.google.android.material.appbar.MaterialToolbar
3333
android:id="@+id/toolbar"
@@ -50,8 +50,8 @@
5050
</com.google.android.material.appbar.MaterialToolbar>
5151
</com.google.android.material.appbar.AppBarLayout>
5252

53-
<WebView
54-
android:id="@+id/webview"
53+
<com.ichi2.anki.workarounds.SafeWebViewLayout
54+
android:id="@+id/webview_layout"
5555
android:layout_width="match_parent"
5656
android:layout_height="0dp"
5757
app:layout_constraintTop_toBottomOf="@id/app_bar"

0 commit comments

Comments
 (0)