Skip to content

fix: a link tap on a device with no browser shows a toast instead of crashing - #116

Open
GianniCarlo wants to merge 6 commits into
developfrom
fix/safe-uri-handler
Open

fix: a link tap on a device with no browser shows a toast instead of crashing#116
GianniCarlo wants to merge 6 commits into
developfrom
fix/safe-uri-handler

Conversation

@GianniCarlo

Copy link
Copy Markdown
Contributor

What

Sentry ANDROID-BOOKPLAYER-1P / -1R: tapping a contributor's GitHub link on a Pixel 3 (Android 12) whose browser was disabled crashed the app. Compose's platform UriHandler rethrows startActivity's ActivityNotFoundException as IllegalArgumentException("Can't open …"), and nine call sites used it bare — Tip Jar contributors, Settings → GitHub / Discord, Account → Terms / Privacy, Hardcover.

SafeUriHandler wraps the platform handler and turns exactly that failure into a toast (common_no_link_handler, English only per the localization convention); any other exception still propagates, so a malformed URI built by our own code is not hidden. BookPlayerTheme installs it as LocalUriHandler, so every call site is covered without changing any of them.

Behaviour

Unchanged for any device with a browser: same intent, same chooser. On a device with nothing handling https, the tap shows "No app on this device can open links." instead of killing the app.

Verification

  • SafeUriHandlerTest (4 cases) — delegation, bare ActivityNotFoundException, the platform's wrapped form, and that other IllegalArgumentExceptions are rethrown. Three mutations killed (swallow every IAE, drop the bare catch, rethrow the wrapped one).
  • ./gradlew assembleDevDebug testDevDebugUnitTest lintDevDebug green.
  • Emulator, bp-lowend-31, scripts/chaos/no-browser.sh (recipe in docs/crash-repro.md): beforeFATAL EXCEPTION … IllegalArgumentException: Can't open https://github.com/TortugaPower/bookplayer-android … Caused by: ActivityNotFoundException, process gone, from Settings → "View project on GitHub" (a different link from the report, same class); after — process alive, NotificationService logs the toast.

Triage notes (no code)

  • -21 (ForegroundServiceStartNotAllowedException from media3's async notification path on 1.1.3+20): media3 1.11.0, already on develop since media3 1.11.0 (fixes the mergePlayerInfo crash, -Q); watch crown volume via AudioManager #101, catches it on both the sync and the bitmap-callback paths and routes it to MediaSessionService.Listener — so the crash is gone in 1.2.0. Without a listener the failure branch is silent; posting a fallback notification would need POST_NOTIFICATIONS on Android 13+, which the app does not request → a product decision, left open.
  • Five "16–168 byte allocation with <1% heap free" OOMs (-1T/-1Y/-1Z/-1V/-22) on 128 MB-heap devices are one class (heap exhausted; the crashing frame is whoever allocated last) and need a heap dump, not a patch. Watch whether 1.2.0's bounded reads thin them.

🤖 Generated with Claude Code

…crashing

Sentry ANDROID-BOOKPLAYER-1P / -1R: tapping a contributor's GitHub link on
a Pixel 3 whose browser was disabled killed the app. Compose's platform
UriHandler rethrows startActivity's ActivityNotFoundException as an
IllegalArgumentException("Can't open …"), and nine call sites (Tip Jar,
Settings, Account, Hardcover) called it bare.

SafeUriHandler wraps the platform handler and turns exactly that failure
into a toast (common_no_link_handler); any other exception still propagates.
BookPlayerTheme installs it as LocalUriHandler, so every call site is covered
without touching any of them, and none can be the one that forgot.

Reproduced on bp-lowend-31 with scripts/chaos/no-browser.sh: before, FATAL
IllegalArgumentException from Settings → "View project on GitHub" and the
process gone; after, alive, NotificationService logs the toast. Recipe in
docs/crash-repro.md. Unit test SafeUriHandlerTest (4 cases); three mutations
killed (swallow every IAE, drop the bare ANFE catch, rethrow the wrapped one).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread app/src/main/java/com/tortugapower/audiobookplayer/ui/SafeUriHandler.kt Outdated
Comment thread scripts/chaos/no-browser.sh Outdated
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

✅ Claude PR Review — PASS

Adds SafeUriHandler, a narrow UriHandler decorator that converts the missing-browser failure (ActivityNotFoundException, bare or wrapped by Compose as IllegalArgumentException("Can't open …")) into a toast plus a scheme-only Sentry breadcrumb, and installs it as LocalUriHandler in BookPlayerTheme so all five LocalUriHandler.current call sites are covered without touching them. I verified BookPlayerTheme wraps both MainActivity.setContent blocks and every link call site, that rememberSafeUriHandler() reads the outer platform handler (no self-referential CompositionLocal) and is properly remembered, that only the cause-chain-matching failure is swallowed, and that the breadcrumb leaks no URL or PII. No :core/:app boundary, Media3, billing or Room impact; the new string is in values/strings.xml and behaviour is unchanged on any device with a browser. Both findings from the earlier push appear addressed: the cause walk is now bounded at depth 8 with a cyclic-chain test, and the chaos script fails loudly when the coordinate fallback lands on the wrong screen. Remaining nits are pre-existing and outside the diff: a few direct startActivity sites (StorageFull.kt:54, SettingsScreen.kt:110) still bypass this wrapper.

Findings: no findings

Previously raised

Finding Status
app/src/main/java/com/tortugapower/audiobookplayer/ui/SafeUriHandler.kt:48 (info) ⚠︎ moved ✅ verified fixed in bdb89f7
scripts/chaos/no-browser.sh:42 (info) ⚠︎ moved ✅ verified fixed in bdb89f7

Converged: nothing new this round, and every earlier finding is settled.

Model claude-opus-5 · run log · 0 new · 0 carried over · 2 verified closed · 0 resolved · advisory (a human should still review). Findings are de-duplicated across pushes; an earlier finding closes only when the verification pass judges it against the current code — fixed, no longer applicable, accepted by a maintainer, or a duplicate of a finding reported on this push.

- The handled no-browser case leaves a trail: a Sentry breadcrumb with the
  link's scheme only, so a later report says how common the population is
  without putting a user's URL in it. One catch with a predicate instead of
  two catches and an unused binding.
- scripts/chaos/no-browser.sh: `pidof` exits non-zero when the process is
  gone, which under `set -euo pipefail` aborted the recipe in exactly the
  crashed case it demonstrates; `|| true` there and on the Chrome disable.
  Verified: before-fix build prints `fatal=1 alive=0`, fixed build
  `fatal=0 alive=1 toast=1`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread app/src/main/java/com/tortugapower/audiobookplayer/ui/theme/Theme.kt Outdated
Comment thread scripts/chaos/no-browser.sh Outdated
- The breadcrumb's scheme is `substringBefore(':', missingDelimiterValue =
  "unknown")`: a scheme-less URI would otherwise have put the whole link in
  the report the adjacent comment promises to keep it out of.
- scripts/chaos/no-browser.sh: the final logcat grep matched nothing on a
  PASSING run and, under `set -euo pipefail`, made the script's exit status
  non-zero exactly when the fix works; it now says so instead. Verified exit
  0 on the fixed build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread scripts/chaos/no-browser.sh
scripts/chaos/no-browser.sh: `tap()`'s `c=$(… | grep … | awk …)` inherited
the pipeline's exit 1 when no node matched, so under `set -euo pipefail` the
script died before the "no node matching" line that exists for that case.
`|| true` on the substitution; the explicit check stays the error path.
Verified: the miss prints the message and returns 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread app/src/main/java/com/tortugapower/audiobookplayer/ui/theme/Theme.kt Outdated
- The missing-handler check walks the causal chain rather than the
  immediate cause, so a Compose release that adds a wrapping level cannot
  bring the crash back silently; a causeless IllegalArgumentException is
  still rethrown. Test added; the immediate-cause-only mutation fails it.
- rememberSafeUriHandler() next to SafeUriHandler owns the toast and the
  breadcrumb; BookPlayerTheme only installs it, and stays about theming.
- scripts/chaos/no-browser.sh re-enables Chrome on any exit (trap), so an
  aborted run does not leave the emulator with no https handler.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread app/src/main/java/com/tortugapower/audiobookplayer/ui/SafeUriHandler.kt Outdated
Comment thread scripts/chaos/no-browser.sh Outdated
- The cause-chain walk is bounded (MAX_CAUSE_DEPTH = 8): a cyclic cause
  graph is constructible with initCause and this runs on the main thread.
  Test with a 2-second timeout; removing the bound fails it.
- scripts/chaos/no-browser.sh: the Settings tab is resolved through the
  accessibility helper first, the fixed coordinate is the fallback, and the
  Settings screen is verified either way — on another AVD the run fails
  loudly instead of reading as a pass on the wrong screen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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