Skip to content

fix(trim): short-clip coerceIn crash + friendly 'clip too short' messaging#97

Open
stozo04 wants to merge 4 commits into
mainfrom
claude/github-issue-95-fix-xv25e3
Open

fix(trim): short-clip coerceIn crash + friendly 'clip too short' messaging#97
stozo04 wants to merge 4 commits into
mainfrom
claude/github-issue-95-fix-xv25e3

Conversation

@stozo04

@stozo04 stozo04 commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Description

Fixes the Crashlytics fatal IllegalArgumentException: Cannot coerce value to an empty range: maximum 335 is less than minimum 400 in FilmstripTrimSelector (Crashlytics 7169b499fee6554684dba69b1ae1a8f0, first seen v1.0.27), and replaces the silent short-clip dead-end with friendly user messaging.

Part 1 — crash fix (fddfb26 and earlier)

Root cause (verified in source, not just from the auto-triage):

  • Neither the capture-finalize path (OpenLoopViewModel.kt:388) nor the gallery-import path (:567) floors a clip's duration before routing to Trim — both set trimEndMs = durationMs as-is. A tap-and-release capture of 335 ms lands on the Trim screen.
  • FilmstripTrimSelector (TrimFilmstripControls.kt) clamped handle positions at four call sites (drag + accessibility setProgress, per handle) with bounds derived from runtime state, e.g. targetMs.coerceIn(curStartMs + minGapMs, durationMs) with minGapMs = 400. Kotlin's coerceIn throws on an inverted range instead of clamping, so the first handle touch (or TalkBack interaction) crashed with exactly the reported message.
  • The bug is latent since the filmstrip selector shipped; 1.0.27 is just when a user first hit it.

Fix: extracted the clamp math into pure, import-free ui/components/TrimHandleMath.kt (same JVM-testable split as media/BoomerangSequence.kt) and use it at all four call sites; each derived bound is clamped so the range is valid by construction. Added lesson 025. Release bump to versionCode 28 / versionName 1.0.28.

Note: supersedes draft PR #96 (same diagnosis, guard duplicated inline 4× with no tests), now closed.

Part 2 — "clip too short" UX (6ed52f4, owner-requested follow-up)

The clamp fix stopped the crash, but a sub-MIN_TRIM_DURATION (400 ms) clip still landed on the Trim screen with pinned handles and a no-op SAVE — a confusing silent dead-end. Now both entry points gate at the existing legal minimum (400 ms, unchanged) and tell the user:

  • Import (onVideoPicked): a picked clip under 400 ms is rejected before any copy (re-checked post-copy, mirroring the too-long double-check) with a friendly "That clip's a bit short" dialog — the sibling of the existing "a bit long" dialog, using the same recreation-safe StateFlow pattern. The user stays on the Gallery.
  • Capture (Finalize handler): a capture that finalizes under 400 ms — or with zero encoded frames (ERROR_NO_VALID_DATA, a tap-and-release) — discards the scratch, returns to the viewfinder, and shows a "That was quick! Record a little longer to make a loop." snackbar instead of a silent return. (Copy corrected after owner review: the shutter is tap-to-start / tap-to-stop, not press-and-hold.)
  • The Lesson-025 clamp math and the trimValid gate stay in place as the backstop for anything that slips through.
Import: "a bit short" dialog Capture: too-quick snackbar
Import too short dialog Capture too short snackbar

Related Issue

Fixes #95

Type of Change

  • 🐛 Bug Fix: A bug fix (non-breaking change which fixes an issue)
  • Feature: Friendly "clip too short" messaging on both entry points (owner-requested follow-up)
  • 🔧 Chore: Version bump to 28 / 1.0.28 for the Play rollout

How Has This Been Tested?

Full local gate run on 2026-07-06 (this replaces the original remote session's "could not run Gradle" caveat — everything below is now actually verified):

  • Builds: :app:assembleDebug and :app:assembleReleaseBUILD SUCCESSFUL, exit 0.
  • Unit tests: :app:testDebugUnitTest green, including the 12 TrimHandleMathTest tests and 5 new OpenLoopViewModelTest tests (pre-copy import reject, post-copy import reject, exactly-400 ms accepted, sub-400 ms finalize discard + CaptureTooShort event, ERROR_NO_VALID_DATACaptureTooShort).
  • Lint: :app:lintDebug — 0 errors, no new warnings.
  • Pre-merge E2E gate (crash fix): full capture → trim → speed → reverse loop → B&W → save → gallery-playback run on the Pixel 8 AVD, plus the exact issue-[Crashlytics] io.github.stozo04.openloop.ui.components.TrimFilmstripControlsKt$FilmstripTrimSelector$1$14$1.invoke$lambda$3 #95 repro — a 335 ms clip on the Trim screen, both handles dragged: no crash, handles pin, zero IllegalArgumentException/FATAL lines. Report: docs/e2e/2026-07-06_104410.md.
  • Live UX verification (part 2): tap-tap capture → snackbar shown on the viewfinder; 335 ms import → "a bit short" dialog, never enters Trim, "Got it" returns to Gallery; normal 4 s capture still routes to Trim. Report + screenshots: docs/e2e/2026-07-06_111500-too-short-ux.md.

Manual QA checklist:

  • gradlew :app:assembleDebug :app:assembleRelease :app:testDebugUnitTest :app:lintDebug — all green (2026-07-06, local)
  • Sub-400 ms clip on the Trim screen → dragging both handles neither crashes nor moves them (driven via a 335 ms import; with part 2 a too-short clip can no longer reach Trim through the normal entry points)
  • Record a normal clip → handles drag exactly as before (gap + duration clamps intact; verified live)
  • Import a short video from the gallery → no crash, friendly dialog (verified live)
  • TalkBack: adjust both trim handles on a short clip → no crash (not driven on-device; the a11y call sites use the same TrimHandleMath covered by the JVM sweep)

What could not be verified: a sub-400 ms capture with valid data can't be produced on the emulator (CameraX finalizes ERROR_NO_VALID_DATA under ~700 ms there); that path is covered by the JVM finalize test with durationOf = 335 ms. TalkBack readout of the new copy not exercised.

Checklist

  • 🧪 My changes have been verified locally and work as expected.
  • 🔍 I have performed a self-review of my own code.
  • ✍️ I have commented my code, particularly in hard-to-understand areas.
  • 📖 My changes generate no new compile warnings or errors.
  • 🧹 The git branch is clean and references to obsolete branches have been pruned.

🤖 Generated with Claude Code

https://claude.ai/code/session_0131ua1xqjNHsg8Cs2sn7dkj
https://claude.ai/code/session_01MmDD5TDJxEyqhfa9f7jn8S

…n MIN_TRIM_DURATION

A sub-400ms capture (e.g. a 335ms tap-and-release) reaches the Trim screen
with trimEndMs = durationMs and no minimum-duration floor. The first handle
drag or TalkBack setProgress then called coerceIn with runtime-derived
bounds — coerceIn(startMs + 400, 335) — which Kotlin rejects with
IllegalArgumentException on an inverted range.

Crashlytics 7169b499fee6554684dba69b1ae1a8f0, first seen v1.0.27.
Fixes #95.

- Extract the handle clamp math into pure, import-free TrimHandleMath.kt
  (same JVM-testable split as media/BoomerangSequence.kt) and use it at
  all four call sites (drag + accessibility, per handle). Each derived
  bound is clamped so the range is valid by construction; on a too-short
  clip both handles pin (start=0, end=duration), matching the already
  disabled NEXT button (trimValid) on the Trim screen.
- Add TrimHandleMathTest: the exact crash values (335 vs 400), normal-clip
  behavior preservation, boundary durations, and a duration/target sweep
  asserting the clamp is total. Verified the suite fails with the exact
  production exception against the unguarded logic (5 failures) and
  passes 12/12 with the guard.
- Add lesson 025 + index entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131ua1xqjNHsg8Cs2sn7dkj
claude and others added 2 commits July 2, 2026 02:52
Play rollout for the issue #95 trim-handle crash fix (Crashlytics 7169b499).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131ua1xqjNHsg8Cs2sn7dkj
… a silent dead-end

Issue #95 follow-up. The clamp fix (Lesson 025) stopped the crash, but a
sub-MIN_TRIM_DURATION clip still landed on the Trim screen with pinned
handles and a no-op SAVE. Now both entry points gate at 400 ms and say so:

- Import (onVideoPicked): reject before any copy (re-checked post-copy)
  with a "That clip's a bit short" dialog — the sibling of the existing
  "a bit long" dialog, same recreation-safe StateFlow pattern.
- Capture (Finalize): a finalize under 400 ms, or ERROR_NO_VALID_DATA
  (tap-and-release, zero encoded frames), discards the scratch and shows
  a "hold the record button a little longer" snackbar on the viewfinder.
- Clamp math + trimValid gate stay as the backstop.

5 new JVM tests (pre/post-copy reject, exact-400ms accepted, short
finalize, ERROR_NO_VALID_DATA). Verified live on the Pixel 8 AVD — proof
screenshots + report in docs/e2e/2026-07-06_111500-too-short-ux.md, and
the PR #97 pre-merge E2E gate report in docs/e2e/2026-07-06_104410.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmDD5TDJxEyqhfa9f7jn8S
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@stozo04 stozo04 changed the title fix(trim): never invert the coerceIn range when a clip is shorter than MIN_TRIM_DURATION fix(trim): short-clip coerceIn crash + friendly 'clip too short' messaging Jul 6, 2026
…e too-short snackbar

"Hold the record button a little longer" implied press-and-hold recording,
but the shutter toggles on tap (CameraScreen tap -> startBurstCapture,
tap -> stopBurstCapture). Now: "That was quick! Record a little longer to
make a loop." Re-verified live on the Pixel 8 AVD; proof screenshot in
docs/e2e refreshed to show the corrected copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmDD5TDJxEyqhfa9f7jn8S
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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.

[Crashlytics] io.github.stozo04.openloop.ui.components.TrimFilmstripControlsKt$FilmstripTrimSelector$1$14$1.invoke$lambda$3

2 participants