mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
fix(android): auto-recover from transient WebView init failures
- refactor(android): unify automatic and manual WebView relaunch paths - fix(android): add retry action to WebView init-failure screen (#7840) - refactor(android): extract shared WebView recovery helper - fix(android): auto-recover from transient WebView init failures
This commit is contained in:
parent
109f249011
commit
3c01d585ca
10 changed files with 295 additions and 16 deletions
|
|
@ -12,6 +12,25 @@
|
|||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!--
|
||||
Android 11+ package visibility: without these the PackageManager fallback in
|
||||
WebViewCompatibilityChecker can't read the WebView provider version, so a
|
||||
transient getCurrentWebViewPackage() miss leaves the block screen with no
|
||||
detected version. Listing the providers explicitly keeps us off
|
||||
QUERY_ALL_PACKAGES. → issue #7518.
|
||||
Keep this list in sync with KNOWN_WEBVIEW_PACKAGES in WebViewCompatibilityChecker.kt.
|
||||
-->
|
||||
<queries>
|
||||
<package android:name="com.google.android.webview" />
|
||||
<package android:name="com.android.webview" />
|
||||
<package android:name="com.android.chrome" />
|
||||
<package android:name="com.chrome.beta" />
|
||||
<package android:name="com.chrome.dev" />
|
||||
<package android:name="com.chrome.canary" />
|
||||
<package android:name="com.sec.android.app.sbrowser" />
|
||||
<package android:name="com.huawei.webview" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import com.superproductivity.superproductivity.webview.JavaScriptInterface
|
|||
import com.superproductivity.superproductivity.webview.WebHelper
|
||||
import com.superproductivity.superproductivity.webview.WebViewBlockActivity
|
||||
import com.superproductivity.superproductivity.webview.WebViewCompatibilityChecker
|
||||
import com.superproductivity.superproductivity.webview.WebViewRecovery
|
||||
import com.superproductivity.superproductivity.widget.ShareIntentQueue
|
||||
import com.superproductivity.superproductivity.widget.StartupOverlayManager
|
||||
import com.superproductivity.plugins.webdavhttp.WebDavHttpPlugin
|
||||
|
|
@ -41,6 +42,7 @@ class CapacitorMainActivity : BridgeActivity() {
|
|||
private lateinit var javaScriptInterface: JavaScriptInterface
|
||||
private var webViewCompatibility: WebViewCompatibilityChecker.Result? = null
|
||||
private var webViewBlocked = false
|
||||
private var webViewRecoveryScheduled = false
|
||||
private var pendingShareIntent: JSONObject? = null
|
||||
private var isFrontendReady = false
|
||||
private var startupOverlayManager: StartupOverlayManager? = null
|
||||
|
|
@ -108,7 +110,9 @@ class CapacitorMainActivity : BridgeActivity() {
|
|||
showWebViewInitFailureOrThrow("BridgeActivity.onCreate() failed to initialize WebView", e)
|
||||
return
|
||||
}
|
||||
if (webViewBlocked) {
|
||||
// A recovery relaunch was scheduled during super.onCreate()/load(); don't
|
||||
// fall through to the (now-doomed) bridge.webView null check and re-handle it.
|
||||
if (webViewBlocked || webViewRecoveryScheduled) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -236,10 +240,36 @@ class CapacitorMainActivity : BridgeActivity() {
|
|||
if (!WebViewCompatibilityChecker.isLikelyWebViewInitFailure(error)) {
|
||||
throw error
|
||||
}
|
||||
showWebViewInitFailure(message, error)
|
||||
recoverOrShowWebViewInitFailure(message, error)
|
||||
}
|
||||
|
||||
private fun showWebViewInitFailure(message: String, error: Throwable? = null) {
|
||||
recoverOrShowWebViewInitFailure(message, error)
|
||||
}
|
||||
|
||||
/**
|
||||
* WebView init failures are usually transient, and the user's own workaround is
|
||||
* to relaunch the app — so attempt that automatically once (via [WebViewRecovery])
|
||||
* before surfacing the terminal block screen. [WebViewCompatibilityChecker] caps
|
||||
* this at one relaunch per window so a genuinely broken provider still reaches
|
||||
* the block screen instead of boot-looping. The [webViewRecoveryScheduled] flag
|
||||
* stops Capacitor's multiple onCreate/load() failure checkpoints from each
|
||||
* scheduling a relaunch. → issue #7518.
|
||||
*/
|
||||
private fun recoverOrShowWebViewInitFailure(message: String, error: Throwable?) {
|
||||
if (webViewBlocked || webViewRecoveryScheduled) {
|
||||
return
|
||||
}
|
||||
if (WebViewCompatibilityChecker.canRetryInitFailure(this)) {
|
||||
webViewRecoveryScheduled = true
|
||||
Log.w("CapacitorMainActivity", "$message - scheduling one-shot WebView recovery relaunch")
|
||||
WebViewRecovery.scheduleRelaunch(this)
|
||||
return
|
||||
}
|
||||
blockForWebViewInitFailure(message, error)
|
||||
}
|
||||
|
||||
private fun blockForWebViewInitFailure(message: String, error: Throwable?) {
|
||||
if (error == null) {
|
||||
Log.e("CapacitorMainActivity", "$message - finishing activity")
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import com.superproductivity.superproductivity.webview.JavaScriptInterface
|
|||
import com.superproductivity.superproductivity.webview.WebHelper
|
||||
import com.superproductivity.superproductivity.webview.WebViewBlockActivity
|
||||
import com.superproductivity.superproductivity.webview.WebViewCompatibilityChecker
|
||||
import com.superproductivity.superproductivity.webview.WebViewRecovery
|
||||
import com.superproductivity.superproductivity.webview.WebViewRequestHandler
|
||||
import org.json.JSONObject
|
||||
|
||||
|
|
@ -101,8 +102,9 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
if (!initWebView()) {
|
||||
showWebViewInitFailure(
|
||||
recoverOrShowWebViewInitFailure(
|
||||
message = "Failed to instantiate WebView",
|
||||
error = null,
|
||||
compatibility = compatibility,
|
||||
)
|
||||
return
|
||||
|
|
@ -326,7 +328,30 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
if (!WebViewCompatibilityChecker.isLikelyWebViewInitFailure(error)) {
|
||||
throw error
|
||||
}
|
||||
showWebViewInitFailure(message, error)
|
||||
recoverOrShowWebViewInitFailure(message, error, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* WebView init failures are usually transient, and the user's own workaround is
|
||||
* to relaunch the app — so attempt that automatically once (via [WebViewRecovery])
|
||||
* before surfacing the terminal block screen. [WebViewCompatibilityChecker]
|
||||
* caps this at one relaunch per window so a genuinely broken provider still
|
||||
* reaches the block screen instead of boot-looping. → issue #7518.
|
||||
*
|
||||
* No re-entry guard is needed here (unlike CapacitorMainActivity): every call
|
||||
* site returns immediately, so this fires at most once per onCreate pass.
|
||||
*/
|
||||
private fun recoverOrShowWebViewInitFailure(
|
||||
message: String,
|
||||
error: Throwable?,
|
||||
compatibility: WebViewCompatibilityChecker.Result?,
|
||||
) {
|
||||
if (WebViewCompatibilityChecker.canRetryInitFailure(this)) {
|
||||
Log.w("SP-WebView", "$message - scheduling one-shot WebView recovery relaunch")
|
||||
WebViewRecovery.scheduleRelaunch(this)
|
||||
return
|
||||
}
|
||||
showWebViewInitFailure(message, error, compatibility)
|
||||
}
|
||||
|
||||
private fun showWebViewInitFailure(
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ package com.superproductivity.superproductivity.webview
|
|||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.superproductivity.superproductivity.FullscreenActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.superproductivity.superproductivity.R
|
||||
|
||||
class WebViewBlockActivity : AppCompatActivity() {
|
||||
|
|
@ -64,9 +65,22 @@ class WebViewBlockActivity : AppCompatActivity() {
|
|||
}.trim()
|
||||
findViewById<TextView>(R.id.webview_block_details).text = details
|
||||
|
||||
val retryButton = findViewById<Button>(R.id.webview_block_retry)
|
||||
if (config.showRetry) {
|
||||
retryButton.visibility = View.VISIBLE
|
||||
retryButton.setOnClickListener { relaunchApp() }
|
||||
} else {
|
||||
retryButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
val updateButton = findViewById<Button>(R.id.webview_block_update)
|
||||
if (config.action == WebViewCompatibilityChecker.BlockScreenAction.OPEN_WEBVIEW_SETTINGS_WITH_WARNING) {
|
||||
updateButton.setText(R.string.webview_block_open_settings)
|
||||
// This branch is only reached for init failures, where Retry is the
|
||||
// primary recovery, so render the settings action as secondary to
|
||||
// keep a single clear primary action.
|
||||
updateButton.backgroundTintList =
|
||||
ColorStateList.valueOf(ContextCompat.getColor(this, R.color.webview_block_secondary))
|
||||
updateButton.setOnClickListener {
|
||||
showOpenWebViewConfirmation(provider, useUpdatePage = false)
|
||||
}
|
||||
|
|
@ -129,15 +143,7 @@ class WebViewBlockActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun relaunchApp() {
|
||||
// Always launch FullscreenActivity (the manifest's MAIN/LAUNCHER) explicitly
|
||||
// rather than via PackageManager.getLaunchIntentForPackage, which can return
|
||||
// null in stripped Android variants and would leave the user with an empty
|
||||
// screen after tapping confirm. FullscreenActivity itself routes via
|
||||
// LaunchDecider to CapacitorMainActivity when appropriate.
|
||||
val launchIntent = Intent(this, FullscreenActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
startActivity(launchIntent)
|
||||
finish()
|
||||
WebViewRecovery.relaunchNow(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -21,8 +21,17 @@ object WebViewCompatibilityChecker {
|
|||
private const val PREFS_NAME = "webview_compatibility"
|
||||
private const val KEY_LAST_GOOD_VERSION = "last_known_good_major_version"
|
||||
private const val KEY_BLOCK_OVERRIDE = "block_override"
|
||||
private const val KEY_INIT_RETRY_AT = "init_failure_retry_at"
|
||||
private const val DEFAULT_WEBVIEW_PACKAGE = "com.google.android.webview"
|
||||
|
||||
// A WebView init failure is frequently transient: the OS WebView provider can be
|
||||
// mid-update or not yet resolved at the instant the activity starts (this is the
|
||||
// INIT_FAILURE the user sees, with no readable version). We allow exactly ONE
|
||||
// automatic recovery relaunch per this window; a failure that persists past it
|
||||
// falls through to the block screen instead of boot-looping. Reset on the next
|
||||
// successful load. → issue #7518.
|
||||
private const val INIT_FAILURE_RETRY_WINDOW_MS = 60_000L
|
||||
|
||||
const val MIN_CHROMIUM_VERSION = 107
|
||||
const val RECOMMENDED_CHROMIUM_VERSION = 110
|
||||
|
||||
|
|
@ -30,6 +39,9 @@ object WebViewCompatibilityChecker {
|
|||
// reasonable proxy when WebView.getCurrentWebViewPackage() is unavailable.
|
||||
// The match is treated as non-authoritative (canBlockBasedOnVersion = false),
|
||||
// so a wrong match here can only ever produce WARN, never BLOCK.
|
||||
// NOTE: keep this list in sync with the <queries> block in AndroidManifest.xml —
|
||||
// on Android 11+ a package missing there is invisible to getPackageInfo() and the
|
||||
// fallback silently can't read its version. → issue #7518.
|
||||
private val KNOWN_WEBVIEW_PACKAGES = listOf(
|
||||
"com.google.android.webview",
|
||||
"com.android.webview",
|
||||
|
|
@ -69,6 +81,11 @@ object WebViewCompatibilityChecker {
|
|||
val action: BlockScreenAction,
|
||||
val showTryAnyway: Boolean,
|
||||
val showSource: Boolean,
|
||||
// Offered only for INIT_FAILURE: these failures are frequently transient
|
||||
// (e.g. the WebView provider not being ready on the first launch after a
|
||||
// cold boot), so a relaunch usually succeeds. Harmless for a genuinely
|
||||
// broken provider — it just returns to this screen.
|
||||
val showRetry: Boolean,
|
||||
)
|
||||
|
||||
data class Result(
|
||||
|
|
@ -159,8 +176,13 @@ object WebViewCompatibilityChecker {
|
|||
* version, so a future genuine block (e.g. WebView downgrade) is not silently bypassed.
|
||||
*/
|
||||
fun recordSuccessfulLoad(context: Context, majorVersion: Int?) {
|
||||
if (majorVersion == null || majorVersion <= 0) return
|
||||
val prefs = preferences(context)
|
||||
// A successful load proves any prior transient init failure is resolved, so
|
||||
// clear the recovery guard regardless of whether the version is readable.
|
||||
if (prefs.getLong(KEY_INIT_RETRY_AT, 0L) != 0L) {
|
||||
prefs.edit().remove(KEY_INIT_RETRY_AT).apply()
|
||||
}
|
||||
if (majorVersion == null || majorVersion <= 0) return
|
||||
val needsVersionUpdate = majorVersion > prefs.getInt(KEY_LAST_GOOD_VERSION, -1)
|
||||
val needsOverrideClear = majorVersion >= MIN_CHROMIUM_VERSION &&
|
||||
prefs.getBoolean(KEY_BLOCK_OVERRIDE, false)
|
||||
|
|
@ -181,6 +203,42 @@ object WebViewCompatibilityChecker {
|
|||
preferences(context).edit().putBoolean(KEY_BLOCK_OVERRIDE, enabled).commit()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the single automatic recovery relaunch allowed per transient WebView
|
||||
* init failure is still available. Read-only: returns false once a relaunch was
|
||||
* recorded within [INIT_FAILURE_RETRY_WINDOW_MS], so the caller shows the block
|
||||
* screen instead of looping. The budget is spent separately by
|
||||
* [recordInitFailureRetry] (only when a relaunch actually happens) and reset by
|
||||
* [recordSuccessfulLoad] on the next healthy load.
|
||||
*/
|
||||
fun canRetryInitFailure(context: Context, now: Long = System.currentTimeMillis()): Boolean =
|
||||
shouldRetryInitFailure(
|
||||
preferences(context).getLong(KEY_INIT_RETRY_AT, 0L),
|
||||
now,
|
||||
INIT_FAILURE_RETRY_WINDOW_MS,
|
||||
)
|
||||
|
||||
/**
|
||||
* Spends the recovery-relaunch budget. Called at the moment of an actual
|
||||
* relaunch (not when merely deciding to schedule one) so a user who kills the
|
||||
* app during the settle delay doesn't burn the one shot.
|
||||
*
|
||||
* Uses [SharedPreferences.Editor.commit] (sync) because the caller relaunches
|
||||
* immediately — an async write could be lost to the relaunch and re-arm the loop.
|
||||
*/
|
||||
fun recordInitFailureRetry(context: Context, now: Long = System.currentTimeMillis()) {
|
||||
preferences(context).edit().putLong(KEY_INIT_RETRY_AT, now).commit()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun shouldRetryInitFailure(lastRetryAt: Long, now: Long, windowMs: Long): Boolean {
|
||||
if (lastRetryAt <= 0L) return true
|
||||
val elapsed = now - lastRetryAt
|
||||
// elapsed < 0 means the wall clock moved backwards since the last attempt;
|
||||
// treat it as stale and allow a retry rather than wedging the user.
|
||||
return elapsed < 0L || elapsed >= windowMs
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun sourceFromName(sourceName: String?): VersionSource {
|
||||
if (sourceName.isNullOrBlank()) return VersionSource.UNKNOWN
|
||||
|
|
@ -224,6 +282,7 @@ object WebViewCompatibilityChecker {
|
|||
},
|
||||
showTryAnyway = canBypassBlock(source),
|
||||
showSource = source != VersionSource.UNKNOWN,
|
||||
showRetry = isInitFailure,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
package com.superproductivity.superproductivity.webview
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.superproductivity.superproductivity.FullscreenActivity
|
||||
|
||||
/**
|
||||
* Recovers from a *transient* WebView init failure (#7518) by relaunching the app
|
||||
* once, after a short settle delay, before the caller surfaces the terminal block
|
||||
* screen. The OS WebView provider is frequently just mid-update / not-yet-resolved
|
||||
* at the instant the activity starts; the user's own workaround is to fully close
|
||||
* and reopen the app, and this does that automatically.
|
||||
*
|
||||
* Scope/limitation: this is a *same-process* relaunch (it routes through the
|
||||
* MAIN/LAUNCHER activity with a fresh task, mirroring [WebViewBlockActivity]'s
|
||||
* existing relaunch). It recovers the "provider not yet resolved" race but NOT a
|
||||
* process whose native WebView load has already hard-failed and cached — that
|
||||
* would need a true fresh process (e.g. a ProcessPhoenix-style trampoline), which
|
||||
* we deliberately avoid for now to keep this dependency-free and within Android's
|
||||
* background-activity-launch rules. A genuinely broken provider therefore still
|
||||
* reaches the block screen (no regression).
|
||||
*
|
||||
* Loop prevention lives in [WebViewCompatibilityChecker]: the retry budget is only
|
||||
* spent at the moment we actually relaunch ([WebViewCompatibilityChecker.recordInitFailureRetry]),
|
||||
* and a relaunch that re-fails within the window is denied by
|
||||
* [WebViewCompatibilityChecker.canRetryInitFailure], falling through to the block screen.
|
||||
*/
|
||||
object WebViewRecovery {
|
||||
private const val TAG = "WebViewRecovery"
|
||||
|
||||
// Delay before the relaunch, giving a mid-update / not-yet-resolved provider a
|
||||
// moment to settle. ~2.5s roughly matches the "close the app and reopen"
|
||||
// interval that already works for users. → issue #7518.
|
||||
private const val RELAUNCH_DELAY_MS = 2500L
|
||||
|
||||
/**
|
||||
* Schedules the one-shot *automatic* recovery relaunch (after a settle delay).
|
||||
* The caller must have already confirmed
|
||||
* [WebViewCompatibilityChecker.canRetryInitFailure] and must return immediately
|
||||
* afterwards without continuing to use the (doomed) WebView.
|
||||
*/
|
||||
fun scheduleRelaunch(activity: Activity) {
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (activity.isFinishing || activity.isDestroyed) {
|
||||
return@postDelayed
|
||||
}
|
||||
// Spend the retry budget only now that we're actually relaunching, so a
|
||||
// user who kills the app during the delay doesn't waste the one shot and
|
||||
// gets a fresh auto-retry on their next manual launch.
|
||||
WebViewCompatibilityChecker.recordInitFailureRetry(activity)
|
||||
Log.w(TAG, "Auto-relaunching to recover from transient WebView init failure")
|
||||
relaunchNow(activity)
|
||||
}, RELAUNCH_DELAY_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Relaunches the app immediately from the MAIN/LAUNCHER activity. Used for
|
||||
* user-initiated recovery (the block screen's "Retry" / "Try anyway"), where no
|
||||
* settle delay or retry-budget accounting applies.
|
||||
*
|
||||
* Always targets [FullscreenActivity] explicitly rather than
|
||||
* PackageManager.getLaunchIntentForPackage, which can return null in stripped
|
||||
* Android variants and would leave the user on an empty screen. FullscreenActivity
|
||||
* itself routes via LaunchDecider to CapacitorMainActivity when appropriate.
|
||||
*/
|
||||
fun relaunchNow(activity: Activity) {
|
||||
activity.startActivity(
|
||||
Intent(activity, FullscreenActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
|
||||
)
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,17 @@
|
|||
android:textColor="#cccccc"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/webview_block_retry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:backgroundTint="#4285F4"
|
||||
android:text="@string/webview_block_retry"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/webview_block_update"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -44,7 +55,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:backgroundTint="#424242"
|
||||
android:backgroundTint="@color/webview_block_secondary"
|
||||
android:text="@string/webview_block_close"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF" />
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@
|
|||
<color name="statusBar">#f8f8f7</color>
|
||||
<color name="navigationBar">#f8f8f7</color>
|
||||
<color name="windowBackground">#f8f8f7</color>
|
||||
<color name="webview_block_secondary">#424242</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<string name="webview_block_provider">Provider package: %1$s</string>
|
||||
<string name="webview_block_source">Detection source: %1$s</string>
|
||||
<string name="webview_block_update">Update WebView</string>
|
||||
<string name="webview_block_retry">Retry</string>
|
||||
<string name="webview_block_open_settings">Open WebView settings</string>
|
||||
<string name="webview_block_close">Close app</string>
|
||||
<string name="webview_block_try_anyway">Try anyway (not recommended)</string>
|
||||
|
|
|
|||
|
|
@ -244,6 +244,7 @@ class WebViewCompatibilityCheckerTest {
|
|||
assertEquals(BlockScreenAction.OPEN_WEBVIEW_SETTINGS_WITH_WARNING, config.action)
|
||||
assertFalse(config.showTryAnyway)
|
||||
assertTrue(config.showSource)
|
||||
assertTrue(config.showRetry)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -255,6 +256,7 @@ class WebViewCompatibilityCheckerTest {
|
|||
|
||||
assertEquals(R.string.webview_init_failure_details_without_provider, config.detailsIntroResId)
|
||||
assertFalse(config.showTryAnyway)
|
||||
assertTrue(config.showRetry)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -269,6 +271,7 @@ class WebViewCompatibilityCheckerTest {
|
|||
assertEquals(BlockScreenAction.UPDATE_WEBVIEW, config.action)
|
||||
assertTrue(config.showTryAnyway)
|
||||
assertFalse(config.showSource)
|
||||
assertFalse(config.showRetry)
|
||||
}
|
||||
|
||||
// Intent helper data ------------------------------------------------------
|
||||
|
|
@ -404,6 +407,54 @@ class WebViewCompatibilityCheckerTest {
|
|||
assertFalse(WebViewCompatibilityChecker.isLikelyWebViewInitFailure(error))
|
||||
}
|
||||
|
||||
// init-failure recovery guard --------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `shouldRetryInitFailure allows the first recovery attempt`() {
|
||||
assertTrue(
|
||||
WebViewCompatibilityChecker.shouldRetryInitFailure(
|
||||
lastRetryAt = 0L,
|
||||
now = 1_000L,
|
||||
windowMs = 60_000L,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldRetryInitFailure blocks a second attempt within the window`() {
|
||||
// The relaunch we just triggered failed again immediately → don't boot-loop.
|
||||
assertFalse(
|
||||
WebViewCompatibilityChecker.shouldRetryInitFailure(
|
||||
lastRetryAt = 1_000L,
|
||||
now = 1_000L + 59_000L,
|
||||
windowMs = 60_000L,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldRetryInitFailure allows a fresh attempt once the window elapses`() {
|
||||
assertTrue(
|
||||
WebViewCompatibilityChecker.shouldRetryInitFailure(
|
||||
lastRetryAt = 1_000L,
|
||||
now = 1_000L + 60_000L,
|
||||
windowMs = 60_000L,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldRetryInitFailure allows retry when the wall clock moves backwards`() {
|
||||
// A clock change must not wedge the user on the block screen forever.
|
||||
assertTrue(
|
||||
WebViewCompatibilityChecker.shouldRetryInitFailure(
|
||||
lastRetryAt = 10_000L,
|
||||
now = 5_000L,
|
||||
windowMs = 60_000L,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// parseMajorVersion ------------------------------------------------------
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue