From da072af8dfd61ea72ad2aafbc9958f2dc3456c87 Mon Sep 17 00:00:00 2001 From: Fandroid745 Date: Sat, 27 Dec 2025 00:06:14 +0530 Subject: [PATCH 1/3] feat: verify webview version and prevent loading blank screen --- .../java/com/ichi2/anki/pages/PageFragment.kt | 8 ++++ .../main/java/com/ichi2/utils/WebViewUtils.kt | 41 ++++++++++--------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt index d1f50338ea38..dbb237137c35 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt @@ -26,10 +26,12 @@ import androidx.core.net.toUri import androidx.fragment.app.Fragment import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.progressindicator.CircularProgressIndicator +import com.ichi2.anki.AnkiActivity import com.ichi2.anki.R import com.ichi2.anki.workarounds.OnWebViewRecreatedListener import com.ichi2.anki.workarounds.SafeWebViewLayout import com.ichi2.themes.Themes +import com.ichi2.utils.checkWebviewVersion import timber.log.Timber /** @@ -100,6 +102,12 @@ abstract class PageFragment( view: View, savedInstanceState: Bundle?, ) { + val ankiActivity = requireActivity() as AnkiActivity + if (checkWebviewVersion(ankiActivity)) { + Timber.w("Aborting PageFragement load: WebView is outdated") + ankiActivity.onBackPressedDispatcher.onBackPressed() + return + } server = AnkiServer(this).also { it.start() } webViewLayout = view.findViewById(R.id.webview_layout) diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt index 8d10924771df..61c70c71db11 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt @@ -31,14 +31,14 @@ import com.ichi2.anki.CrashReportService import com.ichi2.anki.R import timber.log.Timber -internal const val OLDEST_WORKING_WEBVIEW_VERSION_CODE = 418306960L -internal const val OLDEST_WORKING_WEBVIEW_VERSION = 85 +internal const val OLDEST_WORKING_WEBVIEW_VERSION_CODE = 443000000L +internal const val OLDEST_WORKING_WEBVIEW_VERSION = 90 /** * Shows a dialog if the current WebView version is older than the last supported version. */ -fun checkWebviewVersion(activity: AnkiActivity) { - val userVisibleCode = getChromeLikeWebViewVersionIfOutdated(activity) ?: return +fun checkWebviewVersion(activity: AnkiActivity): Boolean { + val userVisibleCode = getChromeLikeWebViewVersionIfOutdated(activity) ?: return false // Provide guidance to the user if the WebView is outdated val webviewPackageInfo = getAndroidSystemWebViewPackageInfo(activity.packageManager) @@ -51,6 +51,7 @@ fun checkWebviewVersion(activity: AnkiActivity) { Timber.w("WebView is outdated. %s: %s", webviewPackageInfo?.packageName, webviewPackageInfo?.versionName) showOutdatedWebViewDialog(activity, userVisibleCode, R.string.link_webview_update) } + return true } @MainThread @@ -93,17 +94,6 @@ fun checkWebViewVersionComponents( versionCode: Long, userAgent: String?, ): Int? { - // Checking the version code works for most webview packages - if (versionCode >= OLDEST_WORKING_WEBVIEW_VERSION_CODE) { - Timber.d( - "WebView is up to date. %s: %s(%s)", - packageName, - webviewVersion, - versionCode.toString(), - ) - return null - } - // Sometimes the webview version code appears too old, and the package name does as well, // but it's a webview that advertises modern capabilities via User-Agent in "Chrome" section // Our warning is purely advisory, so, let's let those through if User-Agent looks okay @@ -111,15 +101,26 @@ fun checkWebViewVersionComponents( val chromeRegex = """Chrome/(\d+)""".toRegex() val matchResult = chromeRegex.find(userAgent)?.groupValues?.get(1) matchResult?.toInt()?.let { - if (it < OLDEST_WORKING_WEBVIEW_VERSION) { - // If we got here, even the User-Agent says it's incompatible, return something - // potentially useful to the user as a browser version + if (it >= OLDEST_WORKING_WEBVIEW_VERSION) { + // If the User-Agent says we are modern, trust it and skip further checks. + return null + } else { + // If the User-Agent is explicitly below the floor, return it immediately. return it } } } - - return null + // Checking the version code works for most webview packages + if (versionCode >= OLDEST_WORKING_WEBVIEW_VERSION_CODE) { + Timber.d( + "WebView is up to date. %s: %s(%s)", + packageName, + webviewVersion, + versionCode.toString(), + ) + return null + } + return webviewVersion.split('.').firstOrNull()?.toIntOrNull() } private fun showOutdatedWebViewDialog( From 1a5f70b0d84119810b7873f1a799aa3747fc7034 Mon Sep 17 00:00:00 2001 From: Fandroid745 Date: Sat, 27 Dec 2025 15:59:01 +0530 Subject: [PATCH 2/3] Fix: Add Unit tests and implement selective blocking during importing for outdated webviews --- .../anki/pages/AnkiPackageImporterFragment.kt | 2 ++ .../java/com/ichi2/anki/pages/CsvImporter.kt | 2 ++ .../java/com/ichi2/anki/pages/PageFragment.kt | 8 +++++--- .../java/com/ichi2/utils/WebViewUtilsTest.kt | 20 +++++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiPackageImporterFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiPackageImporterFragment.kt index 76758ea2c427..e41c3b2e0637 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiPackageImporterFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiPackageImporterFragment.kt @@ -32,6 +32,8 @@ class AnkiPackageImporterFragment : PageFragment() { "import-anki-package$filePath" } + override fun requiresModernWebView() = true + override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient { // the back callback is only enabled when import is running and showing progress val backCallback = diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/CsvImporter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/CsvImporter.kt index 99f524d3ec0b..e3fa324b091c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/CsvImporter.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/CsvImporter.kt @@ -37,6 +37,8 @@ class CsvImporter : PageFragment() { "import-csv$filePath" } + override fun requiresModernWebView() = true + override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient { // the back callback is only enabled when import is running and showing progress val backCallback = diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt index dbb237137c35..cacdd6928dff 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt @@ -66,6 +66,8 @@ abstract class PageFragment( protected open fun onWebViewCreated() { } + protected open fun requiresModernWebView(): Boolean = false + /** * When the webview calls `BridgeCommand("foo")`, the PageFragment execute `bridgeCommands["foo"]`. * By default, only bridge command is allowed, subclasses must redefine it if they expect bridge commands. @@ -75,6 +77,7 @@ abstract class PageFragment( /** * Ensures that [pageWebViewClient] can receive `bridgeCommand` requests and execute the command from [bridgeCommands]. */ + private fun setupBridgeCommand(pageWebViewClient: PageWebViewClient) { if (bridgeCommands.isEmpty()) { return @@ -103,8 +106,8 @@ abstract class PageFragment( savedInstanceState: Bundle?, ) { val ankiActivity = requireActivity() as AnkiActivity - if (checkWebviewVersion(ankiActivity)) { - Timber.w("Aborting PageFragement load: WebView is outdated") + if (requiresModernWebView() && checkWebviewVersion(ankiActivity)) { + Timber.w("${this::class.simpleName} requires modern WebView (Chrome 90+), aborting load") ankiActivity.onBackPressedDispatcher.onBackPressed() return } @@ -114,7 +117,6 @@ abstract class PageFragment( view.findViewById(R.id.toolbar)?.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } - setupWebView(savedInstanceState) } diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt index 0e8788696ab6..694277f9f0d5 100644 --- a/AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt @@ -55,5 +55,25 @@ class WebViewUtilsTest { ), equalTo(null), ) + assertThat( + "Should catch old engine (78) in Huawei package even with valid versionCode", + checkWebViewVersionComponents( + "com.huawei.webview", + "12.1.2.322", + 450000000L, + "Mozilla/5.0 (Linux; Android 10; CDY-AN90 Build/HUAWEICDY-AN90; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.108 Mobile Safari/537.36", + ), + equalTo(78), + ) + assertThat( + "Huawei v15 with code 21311 should be allowed if UA indicates modern engine (114)", + checkWebViewVersionComponents( + "com.huawei.webview", + "15.0.4.326", + 21311L, + "Mozilla/5.0 (Linux; Android 12; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/114.0.5735.196 Mobile Safari/537.36", + ), + equalTo(null), + ) } } From 1b107a67a02c2c9736a3901f2fe5b6b07bbcef99 Mon Sep 17 00:00:00 2001 From: Fandroid745 Date: Fri, 2 Jan 2026 20:53:27 +0530 Subject: [PATCH 3/3] mend --- AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt index 694277f9f0d5..cd6bf52c2c6b 100644 --- a/AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt @@ -65,6 +65,8 @@ class WebViewUtilsTest { ), equalTo(78), ) + // Link: https://www.apkmirror.com/apk/huawei/huawei-webview-2/huawei-webview-15-0-4-326-release/ + // verified version code is 2113L for 15.0.4.326 by analyzing the manifest assertThat( "Huawei v15 with code 21311 should be allowed if UA indicates modern engine (114)", checkWebViewVersionComponents(