Skip to content

fix(browser): explain instead of crashing when the system WebView cannot start - #414

Merged
yuga-hashimoto merged 2 commits into
mainfrom
fix-guest-browser-webview-unavailable
Oct 30, 2026
Merged

yuga-hashimoto merged 2 commits into
mainfrom
fix-guest-browser-webview-unavailable

Conversation

@yuga-hashimoto

Copy link
Copy Markdown
Owner

Summary

Fixes a fatal crash that Crashlytics reports for v1.2.25–v1.2.27 (2 events / 2 users): opening the Guest Browser on a vivo V2538 (Android 16) killed the app with

AndroidRuntimeException: Resources$NotFoundException: failed to redirect ResourcesImpl
  at ResourcesManager.registerResourcePaths
  at WebViewFactory.getProviderClass
  at WebView.setWebContentsDebuggingEnabled
  at GuestBrowserScreen (AndroidView factory)

setWebContentsDebuggingEnabled is the first WebView call, so it is what starts the system WebView provider, and on that device the provider's resource registration threw. The exception escaped the Compose view factory and took the whole app down. Nothing in the app can repair a provider that will not start.

What changed

  • viewOrNull (new, feature/browser/PlatformView.kt) builds a view the platform may refuse to build and returns null instead of throwing. It catches Exception and LinkageError (a provider whose classes or native libraries fail to load), and nothing else: an OutOfMemoryError is not a missing provider.
  • GuestBrowserScreen guards only starting the provider (the debugging flag and the WebView constructor). Everything after it (configure, loadUrl, …) still runs on a WebView that exists, so a failure there stays a real bug and no half-built view is left behind.
  • When the provider cannot start, the screen shows a short explanation instead of the page and hides the address bar, which would do nothing. The text is added in all eight locales and uses each locale's own name for the screen.

What this does not claim

  • This is a mitigation, not a fix: the root cause is the device's WebView provider. The user can still not browse; they get an explanation instead of a crash.
  • While the fallback is shown the CDP debugging flag is not set either, so the in-guest agent cannot drive the browser.
  • It was not reproduced or verified on a device. The provider failure only occurs on the affected phone, and the Compose wiring and fallback text have no unit test.

Not in this PR (known follow-ups)

From the review, none blocking:

  • The reload button in the top bar stays enabled in the fallback and does nothing; a reloadEnabled argument would match hiding the address bar.
  • PlatformViewTest's link-error case asserts only the count of reported throwables; assertSame(error, reported.single()) would match the other test.
  • If configure() or loadUrl() throws on a half-broken provider the app still crashes; destroying the view and falling back would cover it, once a crash report shows it happens.
  • An injectable WebView factory would let the existing instrumented GuestBrowserScreenInstrumentedTest cover the fallback.
  • A fix hides this from Crashlytics, so the number of affected devices is no longer visible; an opted-in non-fatal could record it.

Test plan

  • ./gradlew spotlessCheck detekt :app:testGithubDebugUnitTest — 1620 tests, 0 failures. New: PlatformViewTest (4). It did not compile before the change, and the link-error case failed before the second commit.
  • Locale key parity (same rule as i18n-check.yml) and xmllint on all eight strings.xml
  • ./gradlew :app:assembleGithubDebug :app:lintGithubDebug — 0 errors
  • Not verified on a device
  • CI green

Pre-PR review

The repo-reviewer subagent is defined for OpenCode (.opencode/agents/repo-reviewer.md) and is not available in the Claude Code session that produced this change. The reviews below were done by a read-only Claude Code subagent given that definition as its role and output format. It is a stand-in, not the repo-reviewer subagent itself.

The first commit (2024b17b) was approved with non-blocking suggestions; the second commit (eec26bf0) applies four of them (narrower guard, LinkageError, hidden address bar, wording). The report below is the re-review of HEAD (eec26bf0).

Pre-PR review

判定: APPROVE

対象: コミット2件(2024b17b、eec26bf0)。origin/main の先頭を祖先に持つ。今回の差分は git show HEAD。

ブロッカー

なし

提案(非ブロッキング)

  • GuestBrowserScreen.kt(TopBar の onReload = { webView?.reload() })— フォールバック表示中も、更新ボタンは有効なままで、押しても何も起きない(履歴ボタンは canGoBack/canGoForward が false のため無効)。GuestBrowserTopBar に reloadEnabled = !webViewUnavailable のような引数を足して無効にすると、URL バーを隠した判断と揃う。
  • GuestBrowserScreen.kt(if (!webViewUnavailable) で URL バーを隠す)— 失敗する factory は、AndroidView が挿入される composition の apply フェーズで webViewUnavailable = true を書く。そのため最初の1フレームは URL バー付きで描画され、次のフレームで消える可能性がある。実害はなく、許容範囲。
  • PlatformViewTest.kt(a provider whose native code fails to load ...)— assertEquals(1, reported.size) だけでは、報告された Throwable が UnsatisfiedLinkError そのものかを確認できない。assertSame(error, reported.single()) にすると、既存の RuntimeException 側のテストと揃う。
  • 作りかけの WebView を残さないために、ガード範囲を縮めたことで、configure() や loadUrl() が投げた場合はクラッシュに戻る(生成後の失敗は実バグという割り切り)。プロバイダが半壊状態の端末では、WebView は作れても settings や loadUrl が投げる場合が理論上ありうる。その場合は destroy() してフォールバックに回す選択肢もあるが、クラッシュ報告で実例が出てから検討すれば十分。

チェック済み項目

  • ガード縮小後の同値性: 成功時の挙動は、最初の PR 前のコードと同じ。created.apply { ... } の中の順序は、layoutParams の設定、configure(...)、webView = this、loadUrl(initialUrl が空でないとき)で、create の中の順序(setWebContentsDebuggingEnabled → WebView(context))も元のとおり。apply の this は WebView、webView = this の webView は外側の remember 状態で、書き込み先と書き込み時点(AndroidView の factory、composition の apply フェーズ)も元のコードと同じ。created == null の分岐では webView に何も代入されないので、作りかけの WebView が状態に残らない(前回の指摘の解消を確認)。
  • 型推論と Compose 上の安全性:
    • val created = viewOrNull(...) は WebView?。else 枝では val なので smart cast が効く。factory の最後の式が if-else で、枝の型は View(View(context))と WebView なので共通上位型は View、AndroidView<View> に推論される。update や onRelease を使っていないので影響はない。
    • webViewUnavailable = true は枝の先頭で書いてから View(context) を返す。factory は apply フェーズで走り、前例の webView = this と同じ位置から状態を書くだけなので安全。
    • URL バーを隠しても、urlInput(rememberSaveable)や BackHandler(webView が null なら onBack())には影響しない。
  • LinkageError の捕捉範囲: ガードは setWebContentsDebuggingEnabled と WebView(context) の2呼び出しだけに縮小されているので、過剰ではない。UnsatisfiedLinkError、NoClassDefFoundError、NoSuchMethodError はこの範囲ではプロバイダの不整合の表れで、アプリ側では直せないため、フォールバックが適切。OutOfMemoryError や StackOverflowError は LinkageError ではないので伝播し、テストもそれを確認している。ごくまれに ExceptionInInitializerError(LinkageError の一種)が OOM を原因に持つ場合は、握られる可能性があるが、実害の見込みは極めて低い。
  • 8ロケールの訳文:
    • キー guest_browser_webview_unavailable が8ファイル(values、ar、es、fr、ja、pt-rBR、ru、zh-rCN)に各1つあり、xmllint --noout でエラーなし。
    • 画面名は各ロケールの guest_browser_title に揃っている(Guest Browser、ゲストブラウザ、Navegador invitado、Navigateur invité、Navegador convidado、Гостевой браузер、访客浏览器、المتصفح الضيف)。
    • ar は「يتعذّر تشغيل المتصفح الضيف لأن WebView لم يُحمَّل ...」で語順は自然。Latin の WebView と Android System WebView は文中にあり、文末はアラビア語(المشكلة.)なので RTL の表示は崩れない。
    • fr の n\'a は、同ファイル既存の l\'autorisation と同じエスケープ形式。他の特殊文字はない。
  • ハードルール: i18n は8ロケール揃い、UI 文言のハードコードなし、ログは英語、シークレットなし、テストあり。spotless・detekt・単体テスト(1620件)・i18n キー一致は依頼文の報告に依拠しており、再実行はしていない(読み取り専用のため)。assembleGithubDebug と lintGithubDebug は実行予定とのことなので、マージ前に緑であることを確認してほしい。

🤖 Generated with Claude Code

yuga-hashimoto and others added 2 commits October 30, 2026 23:56
…not start

Crashlytics (v1.2.25-v1.2.27, 2 events / 2 users) has a fatal
AndroidRuntimeException caused by
Resources$NotFoundException "failed to redirect ResourcesImpl" on a vivo
phone (Android 16). It is thrown by WebView.setWebContentsDebuggingEnabled in
GuestBrowserScreen's AndroidView factory: the first WebView call starts the
system WebView provider, whose resource registration failed. The exception
escaped the Compose view factory and took the whole app down.

Nothing in the app can repair a provider that will not start, so creation is
now guarded. viewOrNull builds the WebView (including the debugging flag and
its configuration) and returns null on any Exception; the screen then shows a
short explanation, translated into all eight locales, instead of the page.
Only Exception is caught, so an OutOfMemoryError is not reported as a missing
WebView.

PlatformViewTest covers the three outcomes; it did not compile before the
change. The Compose wiring and the fallback text are not unit-testable here,
and the provider failure cannot be reproduced without the affected device.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pre-PR review of the previous commit raised four points, all addressed here:

- The guard wrapped the whole WebView setup, so an exception thrown by
  configure() or loadUrl() after the view existed returned null and left a
  half-built WebView that was never attached or destroyed. Only starting the
  system provider (the debugging flag and the WebView constructor) can fail
  for reasons the app cannot fix, so only that is guarded now; the rest runs
  on a WebView that exists, where a failure is a real bug.
- A provider whose classes or native libraries fail to load surfaces as a
  LinkageError rather than an Exception. viewOrNull now treats it as a missing
  view as well. Other Errors, an OutOfMemoryError above all, still propagate.
- With no WebView the address bar, Open and history buttons did nothing, so
  the address bar is hidden while the explanation is shown.
- The explanation called the screen the "built-in browser" while its title is
  "Guest Browser" in each locale; it now uses the screen's own name.

PlatformViewTest gains the LinkageError case, which failed before this change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@yuga-hashimoto
yuga-hashimoto merged commit c791e35 into main Oct 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant