Fix Up Next scroll-to-top crash on long queues#5327
Conversation
The smooth scroller in quickScrollToTop computed pixelsInRange as scrollRange * scrollOffset / scrollRange.toFloat(). The leading Int * Int multiplication can overflow on long lists, producing a negative value that propagates into LinearSmoothScroller as a negative duration and crashes with IllegalStateException: "If you provide an interpolator, you must set a positive duration". The formula simplifies to scrollOffset; use Float math directly and floor to 1 to guarantee a positive speed.
Extract the speed-per-pixel math out of the anonymous LinearSmoothScroller so it can be unit-tested without Android dependencies, and cover the Int-overflow case that triggered the original crash.
Generated by 🚫 Danger |
There was a problem hiding this comment.
Pull request overview
Fixes a crash in the “Up Next” screen’s scroll-to-top behavior on long queues by preventing integer overflow from producing a negative smooth-scroller duration.
Changes:
- Refactors
RecyclerView.quickScrollToTop()speed calculation into aninternalhelper (quickScrollSpeedPerPixel) and removes the overflow-prone intermediate multiplication. - Ensures speed-per-pixel stays positive via float math (and a
>= 1ffloor), preventingLinearSmoothScrollerfrom receiving a non-positive duration. - Adds unit tests covering edge cases (non-positive inputs, overflow regime) and updates the changelog.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| modules/services/views/src/main/java/au/com/shiftyjelly/pocketcasts/views/extensions/RecyclerView.kt | Replaces overflow-prone speed math with a safe helper and fallbacks to super when inputs are invalid. |
| modules/services/views/src/test/java/au/com/shiftyjelly/pocketcasts/views/extensions/RecyclerViewTest.kt | Adds unit tests validating positive speed output and guarding edge/overflow cases. |
| CHANGELOG.md | Documents the crash fix under 8.13 Bug Fixes. |
|
@copilot resolve the merge conflicts in this pull request |
Co-authored-by: joashrajin <16253818+joashrajin@users.noreply.github.com>
Resolved — I merged Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot resolve the merge conflicts in this pull request |
|
@geekygecko / @sztomek I can confirm the crash no longer appears on debug. I'll get a recording done tomorrow |
Co-authored-by: joashrajin <16253818+joashrajin@users.noreply.github.com>
Resolved — I merged the latest Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Screen.Capture.on.2026-05-26.at.17-49-06.mp48.11 vs debug build ^ |
Description
The following crash is something I could replicate:
Screen_Recording_20260521_131139_One.UI.Home.mp4
The lower "Up Next" button calls
RecyclerView.quickScrollToTop(), which installs aLinearSmoothScrollerwhosecalculateSpeedPerPixelcomputed:The leading
scrollRange * scrollOffsetisInt * Int, evaluated before the divide-by-float. On long Up Next queues both values can be tens of thousands and their product overflowsInt.MAX_VALUE, producing a negativepixelsInRange. That cascades into a negative speed-per-pixel, a negative duration handed toAction.update(…, duration, interpolator), and a fatal:The reporter's debug log confirms this path — the crash trace ends in a
Choreographerframe callback dispatching aLinearSmoothScroller$Action.validate(), and the user had 332 episodes in Up Next at the time.This PR:
internaltop-level function so it can be unit-tested.scrollRange * scrollOffset / scrollRange.toFloat()(mathematically equivalent toscrollOffsetignoring overflow) withscrollOffset.toFloat().coerceAtLeast(1f). The floor at1fguarantees a positive speed (and therefore a positive duration), even if some future input falls outside the early-return guards.scrollOffset×scrollRangecombination that would have overflowedInt.MAX_VALUEunder the old implementation.Fixes PCDROID-417 (https://linear.app/a8c/issue/PCDROID-417/android-up-next-lower-button-causes-fatal-crash)
Testing Instructions
scrollOffsetaccumulates.The new
RecyclerViewTestalso exercises the math directly:Screenshots or Screencast
N/A — bug fix, no UI changes.
Checklist
./gradlew spotlessApplyto automatically apply formatting/linting)modules/services/localization/src/main/res/values/strings.xml(n/a)