fix: a link tap on a device with no browser shows a toast instead of crashing - #116
fix: a link tap on a device with no browser shows a toast instead of crashing#116GianniCarlo wants to merge 6 commits into
Conversation
…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>
✅ Claude PR Review —
|
| 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>
- 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>
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>
- 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>
- 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>
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
UriHandlerrethrowsstartActivity'sActivityNotFoundExceptionasIllegalArgumentException("Can't open …"), and nine call sites used it bare — Tip Jar contributors, Settings → GitHub / Discord, Account → Terms / Privacy, Hardcover.SafeUriHandlerwraps 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.BookPlayerThemeinstalls it asLocalUriHandler, 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, bareActivityNotFoundException, the platform's wrapped form, and that otherIllegalArgumentExceptions are rethrown. Three mutations killed (swallow every IAE, drop the bare catch, rethrow the wrapped one)../gradlew assembleDevDebug testDevDebugUnitTest lintDevDebuggreen.bp-lowend-31,scripts/chaos/no-browser.sh(recipe indocs/crash-repro.md): before —FATAL 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,NotificationServicelogs the toast.Triage notes (no code)
ForegroundServiceStartNotAllowedExceptionfrom media3's async notification path on 1.1.3+20): media3 1.11.0, already ondevelopsince 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 toMediaSessionService.Listener— so the crash is gone in 1.2.0. Without a listener the failure branch is silent; posting a fallback notification would needPOST_NOTIFICATIONSon Android 13+, which the app does not request → a product decision, left open.🤖 Generated with Claude Code