Skip to content

Fix Sentry JETPACK-ANDROID-1G8W: guard ReaderPostPagerActivity adapter restore - #22856

Merged
nbradbury merged 10 commits into
trunkfrom
fix/sentry-1g8w-readerpostpager-fragment-restore
May 13, 2026
Merged

Fix Sentry JETPACK-ANDROID-1G8W: guard ReaderPostPagerActivity adapter restore#22856
nbradbury merged 10 commits into
trunkfrom
fix/sentry-1g8w-readerpostpager-fragment-restore

Conversation

@nbradbury

@nbradbury nbradbury commented May 12, 2026

Copy link
Copy Markdown
Contributor

Fixes JETPACK-ANDROID-1G8W.

Summary

Two issues, both surfaced when ReaderPostPagerActivity is recreated after process death or with "Don't keep activities" enabled.

1. Fragment no longer exists for key f#… crash (the Sentry issue)

ViewPager2's adapter iterates the saved f# (fragment-reference) keys and unconditionally calls FragmentManager.getFragment(bundle, key) — it does not consult containsItem() for those keys. After process recreation those who-UUIDs no longer resolve in the rebuilt FragmentManager, so the call throws:

java.lang.IllegalStateException: Fragment no longer exists for key f#0:
  unique id fb41c68e-6cb8-410e-b91a-f539fce46654
  at ReaderPostPagerActivity$loadPosts$1.run$lambda$0

This is the top Jetpack-Android crash by user count after the recent merges (Sentry: 92 distinct users in the last 30 days, Play Console: 36 distinct users for the same stack).

2. Pre-existing UX bug: launcher icon wipes the restored stack

Surfaced while testing the fix for #1 with "Don't keep activities" enabled: tapping the launcher icon after the activity stack was destroyed always returned the user to the home tab. Cause: WPLaunchActivity is the LAUNCHER target, and on every cold launch it restarts WPMainActivity with FLAG_ACTIVITY_CLEAR_TASK, wiping any restored stack underneath it.

Fixes

  • Drop pending FSA state on restore. Override onRestoreInstanceState and assign a non-StatefulAdapter placeholder to ViewPager2. That makes restorePendingState() discard mPendingAdapterState without invoking the real adapter's restoreState(), so the stale f# references never get resolved.
  • Tighten hasPagerAdapter() to test viewPager.adapter is PostPagerAdapter, so the placeholder doesn't masquerade as a loaded adapter and cause onResume() to skip loadPosts().
  • Add isDestroyed alongside the existing isFinishing early-out so we also drop the UI work if the activity was torn down between the worker thread and the UI hop.
  • Keep a narrow try/catch (IllegalStateException) around viewPager.adapter = adapter as defense in depth: on failure, log via AppLog.e and finish() so the user returns to the previous screen instead of being stranded on a blank pager.

Test plan

  • Enable "Don't keep activities"
  • Open a post in the Reader
  • Press Home, then restore the app
  • Verify the post is restored and there's no crash

…r restore

`ReaderPostPagerActivity.loadPosts` runs a background thread and hops to
the main thread to assign `viewPager.adapter = PostPagerAdapter(...)`.
After a process-death restore, `FragmentStateAdapter` tries to rehydrate
saved-state fragment keys and throws `IllegalStateException("Fragment no
longer exists for key f#0: unique id …")` when the IDs no longer match
the freshly-built adapter, crashing the activity.

Wrap the adapter assignment and follow-on `setCurrentItem` /
`trackPostAtPositionIfNeeded` calls in a try/catch for
`IllegalStateException`, log the failure, and leave the user on the
previous screen instead of bringing the activity down. Also add an
`isDestroyed` check alongside `isFinishing` so we drop the work if the
activity has already been torn down between the worker thread and the
UI hop.

Sentry: JETPACK-ANDROID-1G8W

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dangermattic

dangermattic commented May 12, 2026

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@wpmobilebot

wpmobilebot commented May 12, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr22856-9bdf799
Build Number1488
Application IDorg.wordpress.android.prealpha
Commit9bdf799
Installation URL6n8qgntebk45g
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented May 12, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr22856-9bdf799
Build Number1488
Application IDcom.jetpack.android.prealpha
Commit9bdf799
Installation URL6jru4422jr73g
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

nbradbury and others added 8 commits May 12, 2026 14:31
Address the FragmentStateAdapter rehydration crash at the root: bumping
pagerGeneration after a saved-state restore makes containsItem() reject
the stale FSA fragment keys, so FSA never tries to resurrect fragments
that no longer exist. The remaining try/catch is narrowed to the actual
adapter-assignment line and now finish()es the activity rather than
leaving the user on a blank pager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the Android framework's vocabulary (onRestoreInstanceState,
FragmentStateAdapter.restoreState).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
androidx.viewpager2:viewpager2:1.1.0's FragmentStateAdapter.restoreState
iterates over saved f# keys and unconditionally calls
FragmentManager.getFragment(bundle, key) without consulting
containsItem(). After process recreation those who-UUIDs no longer
resolve in the rebuilt FragmentManager, so the call throws
IllegalStateException — which we caught and finish()ed, silently kicking
the user back to the post list whenever the activity was recreated.

Install a non-StatefulAdapter placeholder in onPostCreate so
ViewPager2.restorePendingState() drops its mPendingAdapterState before
loadPosts() attaches the real PostPagerAdapter. blogId / postId restore
in onSaveInstanceState still puts the user back on the same post.

Also tighten hasPagerAdapter() to test for PostPagerAdapter so the
placeholder doesn't masquerade as a loaded adapter, and revert the
pagerGeneration+1 bump (it only guarded s# keys, which never get a
chance to fire now that FSA never sees the saved bundle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
onPostCreate's relative ordering vs. view-hierarchy state restoration
isn't reliable across Android versions, so the placeholder adapter we
installed there sometimes ran while mPendingAdapterState was still null
— leaving the real ISE still in play on restore. Move the assignment
directly into onRestoreInstanceState, after super has populated
mPendingAdapterState. Also add a debug log so the path is visible in
logcat for verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Logcat from a "Don't keep activities" restore shows the placeholder
adapter installs correctly (no ISE) but the activity pauses 6ms after
onResume — something else is tearing it down. Add diagnostics on
onPause / onStop / onDestroy / finish() / onNewIntent (with a stack
trace on finish) so we can pinpoint the actual finish source. To be
removed once the cause is identified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnostics on onCreate (with intent action/data), onStart, onResume
(with hasPagerAdapter/backFromLogin), and the deep-link branch entry,
so we can correlate against onPause/onStop/onDestroy and tell whether
the system is destroying the activity through a code path or simply
mid-lifecycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restore ReaderPostPagerActivity.kt to its post-fix state from
673135c now that the cause of the post-restore "returns to post
list" symptom has been identified (WPLaunchActivity's CLEAR_TASK
launch path, fixed in the next commit). Keeps the FSA fix and the
"dropping pending FSA state" log; drops the rest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user taps the launcher icon after their task was destroyed
(process death or "Don't keep activities"), Android brings the existing
task forward and starts WPLaunchActivity on top of the restored stack —
e.g. a ReaderPostPagerActivity the user had open. WPLaunchActivity then
launches WPMainActivity with FLAG_ACTIVITY_CLEAR_TASK, wiping the
restored screen and leaving the user back on the home tab.

Detect this case via isTaskRoot — if we're not the root, the task
already has activities to restore, so we just finish() and let Android
bring the existing stack forward unchanged. The CLEAR_TASK launch path
still runs as before for genuinely cold starts (where WPLaunchActivity
is the task root).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nbradbury
nbradbury marked this pull request as ready for review May 12, 2026 21:08
@nbradbury
nbradbury requested a review from adalpari May 12, 2026 21:08

@adalpari adalpari left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@nbradbury
nbradbury enabled auto-merge (squash) May 13, 2026 11:02
@nbradbury
nbradbury merged commit 58080b6 into trunk May 13, 2026
21 of 22 checks passed
@nbradbury
nbradbury deleted the fix/sentry-1g8w-readerpostpager-fragment-restore branch May 13, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants