Skip to content

Commit 0036e12

Browse files
committed
feat: improve webview version verification
This fixes the verification of Huawei WebViews, which use a different version code (com.huawei.webview) In addition, this specifies a mechanism for a `PageFragment` to specify the minimum usable WebView version feat: verify webview version and prevent loading blank screen refactor: use WebViewVersion and WebViewVersionCode value classes and use finish() in PageFragement.kt refactor: rename checkWebviewVersion to showDialogIfWebViewOutdated and use Context instead of Activity feat: replace requiresModernWebView() with minimumWebViewVersion property test: update WebViewUtilsTest to use accurate webview version data Fix: Add Unit tests and implement selective blocking during importing for outdated webviews Fixes 19914
1 parent 91ad9ef commit 0036e12

6 files changed

Lines changed: 101 additions & 57 deletions

File tree

AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,14 @@ import com.ichi2.utils.ImportUtils
186186
import com.ichi2.utils.NetworkUtils
187187
import com.ichi2.utils.Permissions
188188
import com.ichi2.utils.VersionUtils
189-
import com.ichi2.utils.checkWebviewVersion
190189
import com.ichi2.utils.configureView
191190
import com.ichi2.utils.customView
192191
import com.ichi2.utils.dp
193192
import com.ichi2.utils.message
194193
import com.ichi2.utils.negativeButton
195194
import com.ichi2.utils.positiveButton
196195
import com.ichi2.utils.show
196+
import com.ichi2.utils.showDialogIfWebViewOutdated
197197
import com.ichi2.utils.title
198198
import kotlinx.coroutines.Dispatchers
199199
import kotlinx.coroutines.Job
@@ -556,7 +556,7 @@ open class DeckPicker :
556556

557557
shortAnimDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
558558

559-
checkWebviewVersion(this)
559+
with(this) { showDialogIfWebViewOutdated() }
560560

561561
// If a review reminder deserialization error has recently occurred
562562
// (ex. on app boot, when the app opened, etc.), inform the user via a dialog

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ import com.ichi2.anki.CollectionManager
2525
import com.ichi2.anki.R
2626
import com.ichi2.anki.SingleFragmentActivity
2727
import com.ichi2.anki.hideShowButtonCss
28+
import com.ichi2.utils.OLDEST_WORKING_WEBVIEW_VERSION
29+
import com.ichi2.utils.WebViewVersion
2830

2931
class AnkiPackageImporterFragment : PageFragment() {
3032
override val pagePath: String by lazy {
3133
val filePath = requireArguments().getString(KEY_FILE_PATH)
3234
"import-anki-package$filePath"
3335
}
3436

37+
override val minimumWebViewVersion: WebViewVersion = OLDEST_WORKING_WEBVIEW_VERSION
38+
3539
override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient {
3640
// the back callback is only enabled when import is running and showing progress
3741
val backCallback =

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import com.ichi2.anki.CollectionManager
2727
import com.ichi2.anki.R
2828
import com.ichi2.anki.SingleFragmentActivity
2929
import com.ichi2.anki.hideShowButtonCss
30+
import com.ichi2.utils.OLDEST_WORKING_WEBVIEW_VERSION
31+
import com.ichi2.utils.WebViewVersion
3032

3133
/**
3234
* Anki page used to import text/csv files
@@ -37,6 +39,8 @@ class CsvImporter : PageFragment() {
3739
"import-csv$filePath"
3840
}
3941

42+
override val minimumWebViewVersion: WebViewVersion = OLDEST_WORKING_WEBVIEW_VERSION
43+
4044
override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient {
4145
// the back callback is only enabled when import is running and showing progress
4246
val backCallback =

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import com.ichi2.anki.R
3030
import com.ichi2.anki.workarounds.OnWebViewRecreatedListener
3131
import com.ichi2.anki.workarounds.SafeWebViewLayout
3232
import com.ichi2.themes.Themes
33+
import com.ichi2.utils.WebViewVersion
34+
import com.ichi2.utils.showDialogIfWebViewOutdated
3335
import timber.log.Timber
3436

3537
/**
@@ -64,6 +66,8 @@ abstract class PageFragment(
6466

6567
protected open fun onWebViewCreated() { }
6668

69+
protected open val minimumWebViewVersion: WebViewVersion? = null
70+
6771
/**
6872
* When the webview calls `BridgeCommand("foo")`, the PageFragment execute `bridgeCommands["foo"]`.
6973
* By default, only bridge command is allowed, subclasses must redefine it if they expect bridge commands.
@@ -103,10 +107,21 @@ abstract class PageFragment(
103107
server = AnkiServer(this).also { it.start() }
104108
webViewLayout = view.findViewById(R.id.webview_layout)
105109

110+
minimumWebViewVersion?.let { minVersion ->
111+
val isOutdated =
112+
with(requireContext()) {
113+
showDialogIfWebViewOutdated(minVersion) {
114+
requireActivity().finish()
115+
}
116+
}
117+
if (isOutdated) {
118+
Timber.w("${this::class.simpleName} requires modern WebView (Chrome 90+), aborting load")
119+
return
120+
}
121+
}
106122
view.findViewById<MaterialToolbar>(R.id.toolbar)?.setNavigationOnClickListener {
107123
requireActivity().onBackPressedDispatcher.onBackPressed()
108124
}
109-
110125
setupWebView(savedInstanceState)
111126
}
112127

AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt

Lines changed: 63 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,52 @@ import androidx.annotation.VisibleForTesting
2727
import androidx.appcompat.app.AlertDialog
2828
import androidx.core.content.pm.PackageInfoCompat
2929
import androidx.webkit.WebViewCompat
30-
import com.ichi2.anki.AnkiActivity
3130
import com.ichi2.anki.R
3231
import com.ichi2.anki.common.crashreporting.CrashReportService
32+
import com.ichi2.anki.utils.openUrl
3333
import kotlinx.coroutines.Dispatchers
3434
import kotlinx.coroutines.withContext
3535
import timber.log.Timber
3636

37-
internal const val OLDEST_WORKING_WEBVIEW_VERSION_CODE = 418306960L
38-
internal const val OLDEST_WORKING_WEBVIEW_VERSION = 85
37+
@JvmInline
38+
value class WebViewVersion(
39+
val value: Int,
40+
)
41+
42+
@JvmInline
43+
value class WebViewVersionCode(
44+
val value: Long,
45+
)
46+
47+
/** The Android package version code which corresponds to the Webview version. */
48+
internal val OLDEST_WORKING_WEBVIEW_VERSION_CODE = WebViewVersionCode(418306960L)
49+
50+
/** The minimum supported Webview version(Human readable). */
51+
internal val OLDEST_WORKING_WEBVIEW_VERSION = WebViewVersion(85)
3952

4053
/**
4154
* Shows a dialog if the current WebView version is older than the last supported version.
4255
*/
43-
fun checkWebviewVersion(activity: AnkiActivity) {
44-
val userVisibleCode = getChromeLikeWebViewVersionIfOutdated(activity) ?: return
56+
57+
context(context: Context)
58+
fun showDialogIfWebViewOutdated(
59+
minimumWebViewVersion: WebViewVersion = OLDEST_WORKING_WEBVIEW_VERSION,
60+
onOutdated: () -> Unit = {},
61+
): Boolean {
62+
val userVisibleCode = getChromeLikeWebViewVersionIfOutdated(context, minimumWebViewVersion) ?: return false
4563

4664
// Provide guidance to the user if the WebView is outdated
47-
val webviewPackageInfo = getAndroidSystemWebViewPackageInfo(activity.packageManager)
48-
val legacyWebViewPackageInfo = getLegacyWebViewPackageInfo(activity.packageManager)
65+
val webviewPackageInfo = WebViewCompat.getCurrentWebViewPackage(context)
66+
val legacyWebViewPackageInfo = getLegacyWebViewPackageInfo(context.packageManager)
4967
// TODO modify the alert dialog text to handle the usage of developer builds for system WebView
5068
if (legacyWebViewPackageInfo != null) {
5169
Timber.w("WebView is outdated. %s: %s", legacyWebViewPackageInfo.packageName, legacyWebViewPackageInfo.versionName)
52-
showOutdatedWebViewDialog(activity, userVisibleCode, R.string.link_legacy_webview_update)
70+
showOutdatedWebViewDialog(context, userVisibleCode, R.string.link_legacy_webview_update, onOutdated)
5371
} else {
5472
Timber.w("WebView is outdated. %s: %s", webviewPackageInfo?.packageName, webviewPackageInfo?.versionName)
55-
showOutdatedWebViewDialog(activity, userVisibleCode, R.string.link_webview_update)
73+
showOutdatedWebViewDialog(context, userVisibleCode, R.string.link_webview_update, onOutdated)
5674
}
75+
return true
5776
}
5877

5978
@MainThread
@@ -77,16 +96,25 @@ fun getWebviewUserAgent(context: Context): String? {
7796
* Returns a Chrome-like WebView version if it is outdated, otherwise null if
7897
* cannot be determined at all or if okay
7998
*/
80-
private fun getChromeLikeWebViewVersionIfOutdated(activity: AnkiActivity): Int? {
99+
private fun getChromeLikeWebViewVersionIfOutdated(
100+
context: Context,
101+
minimumWebViewVersion: WebViewVersion,
102+
): Int? {
81103
// If we cannot get the package information at all, return null
82-
val webviewPackageInfo = getAndroidSystemWebViewPackageInfo(activity.packageManager) ?: return null
104+
val webviewPackageInfo = WebViewCompat.getCurrentWebViewPackage(context) ?: return null
83105
val webviewVersion =
84106
webviewPackageInfo.versionName ?: run {
85107
Timber.w("Failed to obtain WebView version")
86108
return null
87109
}
88110
val versionCode = PackageInfoCompat.getLongVersionCode(webviewPackageInfo)
89-
return checkWebViewVersionComponents(webviewPackageInfo.packageName, webviewVersion, versionCode, getWebviewUserAgent(activity))
111+
return checkWebViewVersionComponents(
112+
webviewPackageInfo.packageName,
113+
webviewVersion,
114+
versionCode,
115+
getWebviewUserAgent(context),
116+
minimumWebViewVersion,
117+
)
90118
}
91119

92120
@VisibleForTesting
@@ -95,34 +123,35 @@ fun checkWebViewVersionComponents(
95123
webviewVersion: String,
96124
versionCode: Long,
97125
userAgent: String?,
126+
minimumWebViewVersion: WebViewVersion = OLDEST_WORKING_WEBVIEW_VERSION,
98127
): Int? {
99-
// Checking the version code works for most webview packages
100-
if (versionCode >= OLDEST_WORKING_WEBVIEW_VERSION_CODE) {
101-
Timber.d(
102-
"WebView is up to date. %s: %s(%s)",
103-
packageName,
104-
webviewVersion,
105-
versionCode.toString(),
106-
)
107-
return null
108-
}
109-
110128
// Sometimes the webview version code appears too old, and the package name does as well,
111129
// but it's a webview that advertises modern capabilities via User-Agent in "Chrome" section
112130
// Our warning is purely advisory, so, let's let those through if User-Agent looks okay
113131
userAgent?.let {
114132
val chromeRegex = """Chrome/(\d+)""".toRegex()
115133
val matchResult = chromeRegex.find(userAgent)?.groupValues?.get(1)
116134
matchResult?.toInt()?.let {
117-
if (it < OLDEST_WORKING_WEBVIEW_VERSION) {
118-
// If we got here, even the User-Agent says it's incompatible, return something
119-
// potentially useful to the user as a browser version
135+
if (it >= minimumWebViewVersion.value) {
136+
// If the User-Agent says we are modern, trust it and skip further checks.
137+
return null
138+
} else {
139+
// If the User-Agent is explicitly below the floor, return it immediately.
120140
return it
121141
}
122142
}
123143
}
124-
125-
return null
144+
// Checking the version code works for most webview packages
145+
if (versionCode >= OLDEST_WORKING_WEBVIEW_VERSION_CODE.value) {
146+
Timber.d(
147+
"WebView is up to date. %s: %s(%s)",
148+
packageName,
149+
webviewVersion,
150+
versionCode.toString(),
151+
)
152+
return null
153+
}
154+
return webviewVersion.split('.').firstOrNull()?.toIntOrNull()
126155
}
127156

128157
data class WebViewInfo(
@@ -153,15 +182,17 @@ suspend fun getWebViewInfo(context: Context): WebViewInfo =
153182
}
154183

155184
private fun showOutdatedWebViewDialog(
156-
activity: AnkiActivity,
185+
context: Context,
157186
installedVersion: Int,
158187
@StringRes learnMoreUrl: Int,
188+
onDismiss: () -> Unit = {},
159189
) {
160-
AlertDialog.Builder(activity).show {
161-
setMessage(activity.getString(R.string.webview_update_message, installedVersion, OLDEST_WORKING_WEBVIEW_VERSION))
190+
AlertDialog.Builder(context).show {
191+
setMessage(context.getString(R.string.webview_update_message, installedVersion, OLDEST_WORKING_WEBVIEW_VERSION.value))
162192
setPositiveButton(R.string.scoped_storage_learn_more) { _, _ ->
163-
activity.openUrl(learnMoreUrl)
193+
context.openUrl(learnMoreUrl)
164194
}
195+
setOnDismissListener { onDismiss() }
165196
}
166197
}
167198

@@ -172,26 +203,6 @@ private fun getLegacyWebViewPackageInfo(packageManager: PackageManager): Package
172203
null
173204
}
174205

175-
/**
176-
* Returns a [PackageInfo] from the current system WebView, or `null` if unavailable
177-
*/
178-
private fun getAndroidSystemWebViewPackageInfo(packageManager: PackageManager): PackageInfo? {
179-
fun getPackage(packageName: String): PackageInfo? =
180-
try {
181-
packageManager.getPackageInfo(packageName, 0)
182-
} catch (_: PackageManager.NameNotFoundException) {
183-
null
184-
}
185-
186-
// The WebView is called com.android.webview by default.
187-
// Partner devices which ship with Google applications ship the Google-specific version
188-
// of the WebView called com.google.android.webview.
189-
// https://issues.chromium.org/issues/40419837#comment10
190-
191-
return getPackage("com.google.android.webview")
192-
?: getPackage("com.android.webview") // com.android.webview is used on API 24
193-
}
194-
195206
/**
196207
* Enables debugging of web contents (HTML / CSS / JavaScript)
197208
* loaded into any WebViews of this application. This flag can be enabled

AnkiDroid/src/test/java/com/ichi2/utils/WebViewUtilsTest.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class WebViewUtilsTest {
2828
checkWebViewVersionComponents(
2929
"com.google.android.webview",
3030
"53.0.2785.124",
31-
OLDEST_WORKING_WEBVIEW_VERSION_CODE - 1,
31+
OLDEST_WORKING_WEBVIEW_VERSION_CODE.value - 1,
3232
"Mozilla/5.0 (Linux; Android 7.0; Android SDK built for arm64 Build/NYC; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.124 Mobile Safari/537.36",
3333
),
3434
equalTo(53),
@@ -50,10 +50,20 @@ class WebViewUtilsTest {
5050
checkWebViewVersionComponents(
5151
"com.google.android.webview",
5252
"74.0.0.0",
53-
OLDEST_WORKING_WEBVIEW_VERSION_CODE - 1,
53+
OLDEST_WORKING_WEBVIEW_VERSION_CODE.value - 1,
5454
"Mozilla/5.0 (Linux; Android 9; SM-A730F Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.102 Mobile Safari/537.36",
5555
),
5656
equalTo(null),
5757
)
58+
assertThat(
59+
"Known old huawei webview determined correctly",
60+
checkWebViewVersionComponents(
61+
"com.huawei.webview",
62+
"unknown",
63+
356L,
64+
"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",
65+
),
66+
equalTo(78),
67+
)
5868
}
5969
}

0 commit comments

Comments
 (0)