Fix stats widget InterruptedException family (Sentry JETPACK-ANDROID-1ATH and others)#22857
Conversation
The stats `RemoteViewsService.onDataSetChanged()` path uses `runBlocking` to fetch fresh data on the widget host thread. When the host kills the service mid-fetch the coroutine is cancelled and the resulting `InterruptedException` propagates uncaught, crashing the app. Add a shared `runBlockingForWidget(block)` helper in `widget/utils/` that wraps `runBlocking` and swallows `InterruptedException` / `CancellationException` so the VM can still fall through to its cached data read. Update every widget VM to use the helper. VMs affected: - ViewsWidgetListViewModel (Sentry JETPACK-ANDROID-1ATH) - TodayWidgetBlockListViewModel (Sentry JETPACK-ANDROID-1AV2) - WeekWidgetBlockListViewModel (Sentry JETPACK-ANDROID-1AZW) - TodayWidgetListViewModel (Sentry JETPACK-ANDROID-1AZQ) - AllTimeWidgetBlockListViewModel (Sentry JETPACK-ANDROID-1AWZ) - AllTimeWidgetListViewModel (Sentry JETPACK-ANDROID-1B0V) - WeekViewsWidgetListViewModel (Sentry JETPACK-ANDROID-1C2Y) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by 🚫 Danger |
|
|
|
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## trunk #22857 +/- ##
==========================================
- Coverage 37.22% 37.22% -0.01%
==========================================
Files 2317 2318 +1
Lines 124556 124561 +5
Branches 16917 16917
==========================================
+ Hits 46370 46371 +1
- Misses 74436 74440 +4
Partials 3750 3750 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…terruptedexception
Restore the thread interrupt flag after swallowing InterruptedException in runBlockingForWidget, per JVM convention. Remove @Suppress("TooGenericExceptionCaught") — the rule doesn't fire on these specific exception types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| } catch (_: InterruptedException) { | ||
| AppLog.w(AppLog.T.STATS, "Widget data fetch interrupted") | ||
| Thread.currentThread().interrupt() | ||
| } catch (_: CancellationException) { |
There was a problem hiding this comment.
mmm I'm not sure that swallowing CancellationException is a good idea because then the cancel propagation is stopped and upper jobs keep doing their thing.
We should limit calling this function to very specific scenarios where we are sure it has no side effects. (Which I think is the case, but still I'm concerned)
There was a problem hiding this comment.
I'm not sure that swallowing
CancellationExceptionis a good idea because then the cancel propagation is stopped and upper jobs keep doing their thing.
We could get rid of this and just catch the InterruptedException, since that's the main source of the crashes. WDYT?
There was a problem hiding this comment.
I'm wondering what would happen then when throwing the thread interrupt. But I guess it could work.
There was a problem hiding this comment.
I asked for Claude's input on this discussion:
Good instinct to flag this — but I think it's safe in this specific spot, and worth me spelling out why in the KDoc so the next reader doesn't have to re-derive it.
The "swallowing CancellationException stops propagation and upper jobs keep running" failure mode applies when there's a parent Job above. Here, runBlocking creates a brand-new root coroutine on the RemoteViewsService worker thread — there's nothing upstream to notify. The CancellationException we see is what runBlocking re-throws when the worker thread is interrupted mid-fetch by the widget host; it's terminal for the local scope, not a signal we're hiding from a surviving caller.
The real risk is someone reusing this helper from inside an existing coroutine, where the concern would apply. I'll tighten the KDoc to call that out explicitly ("only safe because this is a root runBlocking — do not call from inside another coroutine").
… cancel Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…terruptedexception
adalpari
left a comment
There was a problem hiding this comment.
After some clarifications, LGTM!


Fixes the InterruptedException family across all stats widgets:
ViewsWidgetListViewModel.onDataSetChangedTodayWidgetBlockListViewModel.onDataSetChangedWeekWidgetBlockListViewModel.onDataSetChangedTodayWidgetListViewModel.onDataSetChangedAllTimeWidgetBlockListViewModel.onDataSetChangedAllTimeWidgetListViewModel.onDataSetChangedWeekViewsWidgetListViewModel.onDataSetChangedSummary
RemoteViewsService.onDataSetChanged()in each stats widget VM usesrunBlocking { … fetchXxx() }to refresh data on the widget host thread. When the host kills the service mid-fetch (the user collapses the widget, the device goes to doze, etc.) the coroutine is cancelled and the resultingInterruptedExceptionpropagates uncaught, crashing the app.Combined Sentry user count across all seven IDs above: ~157 distinct users in the last 30 days (Jetpack). Play Console reports the same shape at ~125 users. All seven VMs share the same bug class, so the fix is structurally a single change — bundling them into one PR.
Changes
widget/utils/WidgetCoroutineHelper.ktwithrunBlockingForWidget(block)— wrapsrunBlockingand swallowsInterruptedException/CancellationException, logging viaAppLog.w(T.STATS, …). The VM falls through to its cachedgetXxx(site)read, so the widget still renders whatever data is already on disk.runBlockingForWidgetin place ofrunBlockingaround the fetch call. No other behavior changes.Test plan
There's no easy way to test this so I think a simple code review and clear CI should suffice.