Skip to content

Crash on Adapty.activate: WebView and UserAgent #52

Description

@Monabr

Summary

UserAgentRetriever calls WebSettings.getDefaultUserAgent() from a background thread on every
Adapty.activate. When the WebView engine fails to start on that device, the resulting exception
is delivered on the main thread and kills the process. The try/catch already present in
retrieveUserAgent() does not cover this path, so there is nothing an integrator can do about it.

Environment

  • Adapty Android SDK 3.17.2 (io.adapty:adapty-bom)
  • Reproduces on various Android versions and devices, not tied to one OS version
  • Reported via Firebase Crashlytics from production

What happens

  1. We call Adapty.activate(applicationContext, AdaptyConfig.Builder(apiKey).build()) in
    Application.onCreate().
  2. UserAgentRetriever is created and calls WebSettings.getDefaultUserAgent(appContext) inside a
    plain thread { }.
  3. Because the call is off the main thread, WebView posts the chromium startup as a task to the
    main thread, and the background thread blocks on a CountDownLatch (visible as Thread-5 in
    the dump below).
  4. On the main thread, chromium startup fails to resolve its own resource:
    Resources$NotFoundException: String array resource ID #0x1070005 in
    AwResource.getConfigKeySystemUuidMapping.
  5. Chromium wraps it into org.chromium.base.JniAndroid$UncaughtExceptionException, which escapes
    MessageQueue.nativePollOnce inside Looper.loop and kills the process.

The underlying WebView bug is known: https://issues.chromium.org/issues/40205792. It fires when the
WebView package is unavailable at that moment (being updated, disabled, or the process resources
went stale after an apk update). Other libraries hit the same thing and moved away from
getDefaultUserAgent (for example react-native-device-info#272).

Why the current try/catch does not help

private fun retrieveUserAgent() {
    thread {
        try {
            lock.writeLock().lock()
            userAgent = WebSettings.getDefaultUserAgent(appContext)
        } catch (_: Throwable) {
        } finally {
            lock.writeLock().unlock()
        }
    }
}

The exception never reaches this catch. It is thrown on the main thread, inside a chromium task
that nothing wraps, and goes straight to the default uncaught handler.

Impact

Our app does not use WebView anywhere. The WebView engine is started in our process only because
of this user agent call, and that is the sole reason the app crashes. There is also no way to opt
out: AdaptyConfig.Builder has no option related to user agent collection.

Stack traces

Fatal exception (main thread)
Fatal Exception: org.chromium.base.JniAndroid$UncaughtExceptionException
       at org.chromium.base.JniAndroid.handleException(chromium-SystemWebViewGoogle6432.aab-stable-787118103:21)
       at android.os.MessageQueue.nativePollOnce(MessageQueue.java)
       at android.os.MessageQueue.next(MessageQueue.java:381)
       at android.os.Looper.loopOnce(Looper.java:191)
       at android.os.Looper.loop(Looper.java:325)
       at android.app.ActivityThread.main(ActivityThread.java:10404)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:635)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:970)

Caused by android.content.res.Resources$NotFoundException: String array resource ID #0x1070005
       at android.content.res.Resources.getStringArray(Resources.java:900)
       at org.chromium.android_webview.common.AwResource.getConfigKeySystemUuidMapping(chromium-SystemWebViewGoogle6432.aab-stable-787118103:5)
       at android.os.MessageQueue.nativePollOnce(MessageQueue.java)
       at android.os.MessageQueue.next(MessageQueue.java:381)
       at android.os.Looper.loopOnce(Looper.java:191)
       at android.os.Looper.loop(Looper.java:325)
       at android.app.ActivityThread.main(ActivityThread.java:10404)
Blocked Adapty thread at the same moment
Thread-5:
       at jdk.internal.misc.Unsafe.park(Unsafe.java)
       at java.util.concurrent.locks.LockSupport.park(LockSupport.java:221)
       at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:230)
       at com.android.webview.chromium.q0.n(chromium-SystemWebViewGoogle6432.aab-stable-787118103:21)
       at com.android.webview.chromium.WebViewChromiumFactoryProvider$StaticsAdapter.getDefaultUserAgent(chromium-SystemWebViewGoogle6432.aab-stable-787118103:44)
       at android.webkit.WebSettings.getDefaultUserAgent(WebSettings.java:1408)
       at com.adapty.internal.utils.UserAgentRetriever$retrieveUserAgent$1.invoke(UserAgentRetriever.kt:35)
       at com.adapty.internal.utils.UserAgentRetriever$retrieveUserAgent$1.invoke(UserAgentRetriever.kt:32)
       at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

Possible fixes

  1. Fall back to System.getProperty("http.agent") and never touch WebView. This is what other
    libraries switched to after the same bug, and it needs no WebView process at all.
  2. Add an option to AdaptyConfig.Builder to disable user agent collection or to supply a custom
    user agent string.
  3. At minimum, make the call lazy (only when a request actually needs the user agent) and run it on
    the main thread, so the failure lands inside your own try/catch instead of escaping to the
    looper.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions