fix(android): avoid false WebView version lockout

This commit is contained in:
Johannes Millan 2026-04-28 11:43:37 +02:00
parent f8f405c623
commit 10257fc21a
2 changed files with 108 additions and 28 deletions

View file

@ -53,17 +53,10 @@ object WebViewCompatibilityChecker {
}
fun evaluate(context: Context): Result {
val packageInfo = resolvePackageInfo(context)
val resolvedPackageInfo = resolvePackageInfo(context)
val packageInfo = resolvedPackageInfo?.packageInfo
val packageMajor =
packageInfo?.let { parseMajorVersion(it.versionName) ?: parseMajorVersion(it.versionCode) }
if (packageMajor != null) {
return buildResult(
majorVersion = packageMajor,
packageInfo = packageInfo,
source = VersionSource.PACKAGE,
)
}
packageInfo?.let { parseMajorVersion(it.versionName) ?: parseMajorVersion(it.longVersionCode) }
val userAgentMajor = resolveFromUserAgent(context)
if (userAgentMajor != null) {
@ -71,6 +64,25 @@ object WebViewCompatibilityChecker {
majorVersion = userAgentMajor,
packageInfo = packageInfo,
source = VersionSource.USER_AGENT,
canBlockBasedOnVersion = true,
)
}
if (packageMajor != null && resolvedPackageInfo.canBlockBasedOnVersion) {
return buildResult(
majorVersion = packageMajor,
packageInfo = packageInfo,
source = VersionSource.PACKAGE,
canBlockBasedOnVersion = true,
)
}
if (packageMajor != null) {
return buildResult(
majorVersion = packageMajor,
packageInfo = packageInfo,
source = VersionSource.PACKAGE,
canBlockBasedOnVersion = false,
)
}
@ -78,6 +90,7 @@ object WebViewCompatibilityChecker {
majorVersion = null,
packageInfo = packageInfo,
source = VersionSource.UNKNOWN,
canBlockBasedOnVersion = false,
)
}
@ -85,21 +98,13 @@ object WebViewCompatibilityChecker {
majorVersion: Int?,
packageInfo: PackageInfo?,
source: VersionSource,
canBlockBasedOnVersion: Boolean,
): Result {
// Check if this is a third-party WebView with non-standard versioning
val isThirdPartyWebView = packageInfo?.packageName?.let { pkg ->
pkg !in listOf("com.google.android.webview", "com.android.webview", "com.android.chrome")
} ?: false
val status = when {
majorVersion == null -> Status.WARN
// For third-party WebViews with suspiciously low version numbers,
// be lenient and just warn instead of blocking (they may use different versioning)
isThirdPartyWebView && majorVersion < 50 -> Status.WARN
majorVersion < MIN_CHROMIUM_VERSION -> Status.BLOCK
majorVersion < RECOMMENDED_CHROMIUM_VERSION -> Status.WARN
else -> Status.OK
}
val status = statusForVersion(
majorVersion = majorVersion,
packageName = packageInfo?.packageName,
canBlockBasedOnVersion = canBlockBasedOnVersion,
)
return Result(
status = status,
majorVersion = majorVersion,
@ -109,7 +114,32 @@ object WebViewCompatibilityChecker {
)
}
private fun resolvePackageInfo(context: Context): PackageInfo? {
@VisibleForTesting
internal fun statusForVersion(
majorVersion: Int?,
packageName: String?,
canBlockBasedOnVersion: Boolean,
): Status {
// Check if this is a third-party WebView with non-standard versioning
val isThirdPartyWebView = packageName?.let { pkg ->
pkg !in listOf("com.google.android.webview", "com.android.webview", "com.android.chrome")
} ?: false
return when {
majorVersion == null -> Status.WARN
// PackageManager fallback scans installed packages, not necessarily the
// active WebView provider. Use it for diagnostics only, never lockout.
!canBlockBasedOnVersion && majorVersion < RECOMMENDED_CHROMIUM_VERSION -> Status.WARN
// For third-party WebViews with suspiciously low version numbers,
// be lenient and just warn instead of blocking (they may use different versioning)
isThirdPartyWebView && majorVersion < 50 -> Status.WARN
majorVersion < MIN_CHROMIUM_VERSION -> Status.BLOCK
majorVersion < RECOMMENDED_CHROMIUM_VERSION -> Status.WARN
else -> Status.OK
}
}
private fun resolvePackageInfo(context: Context): ResolvedPackageInfo? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Why: on devices with a broken WebView provider getCurrentWebViewPackage()
// can throw (AndroidRuntimeException / MissingWebViewPackageException /
@ -117,7 +147,9 @@ object WebViewCompatibilityChecker {
// FullscreenActivity's recovery. All three extend RuntimeException;
// keep the catch narrow so non-runtime failures still surface.
try {
WebView.getCurrentWebViewPackage()?.let { return it }
WebView.getCurrentWebViewPackage()?.let {
return ResolvedPackageInfo(it, canBlockBasedOnVersion = true)
}
} catch (e: RuntimeException) {
Log.d(TAG, "getCurrentWebViewPackage() threw; falling back to PackageManager", e)
}
@ -128,7 +160,7 @@ object WebViewCompatibilityChecker {
try {
val info = packageInfo(pm, packageName)
if (info != null) {
return info
return ResolvedPackageInfo(info, canBlockBasedOnVersion = false)
}
} catch (_: PackageManager.NameNotFoundException) {
// Ignore and continue
@ -199,9 +231,14 @@ object WebViewCompatibilityChecker {
context.startActivity(updateIntent)
}
private val PackageInfo.versionCode: Long
private val PackageInfo.longVersionCode: Long
get() = PackageInfoCompat.getLongVersionCode(this)
private val CHROME_REGEX = Regex("Chrom(?:e|ium)/(\\d+)")
private val VERSION_REGEX = Regex("Version/(\\d+)")
private data class ResolvedPackageInfo(
val packageInfo: PackageInfo,
val canBlockBasedOnVersion: Boolean,
)
}

View file

@ -0,0 +1,43 @@
package com.superproductivity.superproductivity.webview
import com.superproductivity.superproductivity.webview.WebViewCompatibilityChecker.Status
import org.junit.Assert.assertEquals
import org.junit.Test
class WebViewCompatibilityCheckerTest {
@Test
fun `blocks old authoritative WebView version`() {
assertEquals(
Status.BLOCK,
WebViewCompatibilityChecker.statusForVersion(
majorVersion = WebViewCompatibilityChecker.MIN_CHROMIUM_VERSION - 1,
packageName = "com.google.android.webview",
canBlockBasedOnVersion = true,
),
)
}
@Test
fun `warns for old package-manager fallback version`() {
assertEquals(
Status.WARN,
WebViewCompatibilityChecker.statusForVersion(
majorVersion = WebViewCompatibilityChecker.MIN_CHROMIUM_VERSION - 1,
packageName = "com.google.android.webview",
canBlockBasedOnVersion = false,
),
)
}
@Test
fun `accepts recommended package-manager fallback version`() {
assertEquals(
Status.OK,
WebViewCompatibilityChecker.statusForVersion(
majorVersion = WebViewCompatibilityChecker.RECOMMENDED_CHROMIUM_VERSION,
packageName = "com.google.android.webview",
canBlockBasedOnVersion = false,
),
)
}
}