Skip to content

Fix Up Next scroll-to-top crash on long queues#5327

Open
joashrajin wants to merge 5 commits into
mainfrom
pcdroid-417-fix-up-next-scroll-crash
Open

Fix Up Next scroll-to-top crash on long queues#5327
joashrajin wants to merge 5 commits into
mainfrom
pcdroid-417-fix-up-next-scroll-crash

Conversation

@joashrajin
Copy link
Copy Markdown
Contributor

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 a LinearSmoothScroller whose calculateSpeedPerPixel computed:

val pixelsInRange = scrollRange * scrollOffset / scrollRange.toFloat()

The leading scrollRange * scrollOffset is Int * Int, evaluated before the divide-by-float. On long Up Next queues both values can be tens of thousands and their product overflows Int.MAX_VALUE, producing a negative pixelsInRange. That cascades into a negative speed-per-pixel, a negative duration handed to Action.update(…, duration, interpolator), and a fatal:

IllegalStateException: If you provide an interpolator, you must set a positive duration

The reporter's debug log confirms this path — the crash trace ends in a Choreographer frame callback dispatching a LinearSmoothScroller$Action.validate(), and the user had 332 episodes in Up Next at the time.

This PR:

  • Extracts the speed-per-pixel math into an internal top-level function so it can be unit-tested.
  • Replaces scrollRange * scrollOffset / scrollRange.toFloat() (mathematically equivalent to scrollOffset ignoring overflow) with scrollOffset.toFloat().coerceAtLeast(1f). The floor at 1f guarantees a positive speed (and therefore a positive duration), even if some future input falls outside the early-return guards.
  • Adds a unit test that exercises a scrollOffset × scrollRange combination that would have overflowed Int.MAX_VALUE under the old implementation.

Fixes PCDROID-417 (https://linear.app/a8c/issue/PCDROID-417/android-up-next-lower-button-causes-fatal-crash)

Testing Instructions

  1. Build and install the app.
  2. Add ~20+ episodes to the Up Next queue (enough that the queue scrolls past a screenful).
  3. Open the player, then open Up Next via the lower "Up Next" button (the one with the text label, not the badge button).
  4. Scroll down through the queue for 5+ seconds so a large scrollOffset accumulates.
  5. Tap the lower "Up Next" button again.
    • Verify the queue smoothly scrolls back to the top with no crash.
    • Repeat a few times to confirm the crash never reappears.
    • Tap the lower "Up Next" button while already at the top — verify nothing breaks (regression check).

The new RecyclerViewTest also exercises the math directly:

./gradlew :modules:services:views:testDebugUnitTest --tests "au.com.shiftyjelly.pocketcasts.views.extensions.RecyclerViewTest"

Screenshots or Screencast

N/A — bug fix, no UI changes.

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml (n/a)
  • Any jetpack compose components I added or changed are covered by compose previews (n/a)
  • I have updated (or requested that someone edit) the spreadsheet to reflect any new or changed analytics. (n/a)

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.
@joashrajin joashrajin requested a review from a team as a code owner May 22, 2026 05:25
@joashrajin joashrajin requested review from Copilot and sztomek and removed request for a team May 22, 2026 05:25
@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented May 22, 2026

1 Warning
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 an internal helper (quickScrollSpeedPerPixel) and removes the overflow-prone intermediate multiplication.
  • Ensures speed-per-pixel stays positive via float math (and a >= 1f floor), preventing LinearSmoothScroller from 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.

@joashrajin joashrajin added [Type] Bug Not functioning as intended. [Area] Up Next labels May 22, 2026
@joashrajin joashrajin self-assigned this May 25, 2026
@joashrajin
Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: joashrajin <16253818+joashrajin@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 25, 2026

@copilot resolve the merge conflicts in this pull request

Resolved — I merged origin/main into this branch and fixed the conflict in CHANGELOG.md in commit e3baea45.

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:

  • dl.google.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED -Xmx8048M -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant -cp /home/REDACTED/.gradle/wrapper/dists/gradle-9.5.1-bin/iq79hdu3mqx29lgffhp8bfmx/gradle-9.5.1/lib/gradle-daemon-main-9.5.1.jar (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@joashrajin
Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

@joashrajin
Copy link
Copy Markdown
Contributor Author

@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>
Copilot AI review requested due to automatic review settings May 25, 2026 19:10
@joashrajin joashrajin review requested due to automatic review settings May 25, 2026 19:10
Copy link
Copy Markdown
Contributor

Copilot AI commented May 25, 2026

@copilot resolve the merge conflicts in this pull request

Resolved — I merged the latest origin/main and fixed the CHANGELOG.md conflict in commit d344acd.

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:

  • dl.google.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED -Xmx8048M -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant -cp /home/REDACTED/.gradle/wrapper/dists/gradle-9.5.1-bin/iq79hdu3mqx29lgffhp8bfmx/gradle-9.5.1/lib/gradle-daemon-main-9.5.1.jar (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@joashrajin
Copy link
Copy Markdown
Contributor Author

Screen.Capture.on.2026-05-26.at.17-49-06.mp4

8.11 vs debug build ^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Area] Up Next [Type] Bug Not functioning as intended.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants