From 38237591a7b8ec30112a56e5d13ed71c32abc6b6 Mon Sep 17 00:00:00 2001 From: Nero Nguyen Date: Tue, 15 Sep 2026 17:59:41 +0700 Subject: [PATCH] improvement(startup): WebView startup helper Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com> Assisted-by: Claude Fable 5.1 --- .../main/java/com/ichi2/anki/AnkiDroidApp.kt | 20 +++++++++-- .../main/java/com/ichi2/utils/WebViewUtils.kt | 36 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt index fe2eb7eb6160..643afd53a117 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt @@ -67,6 +67,7 @@ import com.ichi2.utils.ExceptionUtil import com.ichi2.utils.LanguageUtil import com.ichi2.utils.measureTime import com.ichi2.utils.setWebContentsDebuggingEnabled +import com.ichi2.utils.startUpWebView import com.ichi2.widget.DayRolloverAlarm import com.ichi2.widget.WidgetNotificationScheduler import com.ichi2.widget.cardanalysis.CardAnalysisWidget @@ -172,8 +173,6 @@ open class AnkiDroidApp : showThemedToast(this.applicationContext, getString(R.string.user_is_a_robot), false) } - setWebContentsDebuggingEnabled(Prefs.isWebDebugEnabled) - setupContextMenus() setup("makeBackendUsable") { makeBackendUsable(this) } @@ -185,6 +184,8 @@ open class AnkiDroidApp : if (!checkWebViewAvailable()) { return } + // after the probe: startUpWebView throws on its executor if the WebView provider is missing (5794) + setupWebView() // Forget the last deck that was used in the CardBrowser CardBrowser.clearLastDeckId() @@ -447,6 +448,21 @@ open class AnkiDroidApp : } } + /** + * Starts asynchronously loading the WebView on a background thread. + * + * TODO: This only handles a subset of WebView init, and will not produce a performance + * improvement until this pattern is used for all WebView init. + */ + private fun setupWebView() = + setup("setupWebView") { + startUpWebView( + context = this, + onSuccess = { setWebContentsDebuggingEnabled(Prefs.isWebDebugEnabled) }, + onFailure = { e -> Timber.w(e, "startUpWebView") }, + ) + } + /** * @return the app version, OS version and device model, provided when syncing. */ diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt index 127606d36378..1adf12200e27 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt @@ -27,12 +27,17 @@ import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.core.content.pm.PackageInfoCompat import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewOutcomeReceiver +import androidx.webkit.WebViewStartUpConfig +import androidx.webkit.WebViewStartUpResult +import androidx.webkit.WebViewStartupException import com.ichi2.anki.R import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.utils.openUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber +import java.util.concurrent.Executors @JvmInline value class WebViewVersion( @@ -231,3 +236,34 @@ fun setWebContentsDebuggingEnabled(enabled: Boolean) = // android.util.AndroidRuntimeException: android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed Timber.w(e, "setWebContentsDebuggingEnabled") } + +/** + * Starts the WebView on a background thread, so later WebView usage is faster. + * + * Exactly one of [onSuccess] or [onFailure] is called, on the main thread, once startup completes. + * + * Background work runs on a dedicated thread, released once startup completes. + * + * @see WebViewCompat.startUpWebView + */ +fun startUpWebView( + context: Context, + onSuccess: () -> Unit, + onFailure: (Throwable) -> Unit, +) { + val executor = Executors.newSingleThreadExecutor() + val config = WebViewStartUpConfig.Builder(executor).build() + val callback = + object : WebViewOutcomeReceiver { + override fun onResult(result: WebViewStartUpResult?) { + executor.shutdown() + onSuccess() + } + + override fun onError(e: WebViewStartupException) { + executor.shutdown() + onFailure(e) + } + } + WebViewCompat.startUpWebView(context, config, callback) +}