Skip to content

fix(widget): recover from launcher bitmap-budget rejection instead of freezing#38

Merged
timbortnik merged 2 commits into
mainfrom
fix/widget-budget-rejection-retry
Jun 29, 2026
Merged

fix(widget): recover from launcher bitmap-budget rejection instead of freezing#38
timbortnik merged 2 commits into
mainfrom
fix/widget-budget-rejection-retry

Conversation

@timbortnik

@timbortnik timbortnik commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Problem

A field report on v1.2.3: the widget stops updating permanently in some conditions, fixed
only by re-creating it; a newly created widget works; and tapping the stuck widget opens the app
but it fails to update.

Root cause: the widget sets two chart bitmaps per update (widget_chart_light +
widget_chart_dark). When their combined size exceeds the launcher's per-widget RemoteViews
budget (~1.5 × screen × 4 bytes), updateAppWidget() throws IllegalArgumentException. Because
the rejection depends on the widget's size (per-id state), one widget can be permanently stuck
while a fresh, smaller one renders fine.

This was largely addressed in #35 (v1.2.6) with WidgetUtils.clampChartDimensions() + a
try/catch around updateAppWidget(). But the try/catch only swallows the rejection — if the
clamp ever under-protects (split-screen / multi-window, or a launcher whose budget basis differs
from our displayMetrics), the widget freezes permanently and silently: every later update
recomputes the same over-budget size → rejected again → fixable only by re-creating the widget.
That residual silent-freeze is exactly the reported symptom, minus the crash #35 removed.

Fix

Add updateWidgetWithRetry() around both updateAppWidget() call sites. On a rejection it
halves both bitmap dimensions (quartering memory) and re-renders, up to 4 attempts down to a
64px floor, so the update eventually fits instead of freezing. The per-widget RemoteViews assembly
is extracted into a pure buildWidgetViews(w, h) so it can be rebuilt at a smaller size.

The chart is vector (SVG stretched FIT_XY), so a lower-res raster on retry is visually identical
— far better than a frozen widget. The happy path is unchanged (one render, one update); only
a rejection triggers a retry.

Verification

  • ./gradlew :app:compileDebugKotlin
  • ./gradlew :app:test (Kotlin unit tests) ✅
  • ./gradlew :app:lintDebug — no NewApi errors; the only Error is a pre-existing
    MissingTranslation (app_name) unrelated to this change.

Notes

🤖 Generated with [Claude Code]

Summary by CodeRabbit

  • Bug Fixes
    • Improved widget reliability so updates are less likely to fail or freeze when the launcher rejects large image updates.
    • Added a retry flow that automatically reduces widget image size before giving up.
    • Widget updates now fall back more gracefully when chart rendering isn’t available, helping keep the widget responsive.
    • Resized widget states now refresh more safely, with fewer update crashes and better handling of missing or unavailable chart data.

… freezing

When updateAppWidget() is rejected for exceeding the launcher's per-widget
bitmap-memory budget, the #35 try/catch only swallowed the throw, leaving the
widget permanently and silently frozen: every later update recomputed the same
over-budget size and was rejected again, fixable only by re-creating the widget
(the "widget stops updating, new widget works, app fails to update on tap"
report).

Add updateWidgetWithRetry(): on rejection, halve both bitmap dimensions
(quartering memory) and re-render via the new pure buildWidgetViews(w, h),
up to 4 attempts down to a 64px floor. The chart is vector (SVG stretched
FIT_XY), so a lower-res raster is visually identical — far better than a freeze.
The happy path is unchanged (one render, one update).

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

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@timbortnik, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b05980fc-a892-4fd3-afdd-406dd728a557

📥 Commits

Reviewing files that changed from the base of the PR and between 3de00a1 and 964fec4.

📒 Files selected for processing (1)
  • android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt
📝 Walkthrough

Walkthrough

MeteogramWidgetProvider gains a retry-and-shrink strategy for widget bitmap updates. A new updateWidgetWithRetry helper progressively halves bitmap dimensions up to MAX_UPDATE_ATTEMPTS times, stopping at MIN_CHART_DIMENSION_PX. A new buildWidgetViews centralizes RemoteViews construction. Both onUpdate and onAppWidgetOptionsChanged are updated to use these helpers.

Changes

Widget bitmap retry-and-shrink strategy

Layer / File(s) Summary
Retry tuning constants
android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt
Adds MAX_UPDATE_ATTEMPTS and MIN_CHART_DIMENSION_PX companion-object constants to bound the shrink/retry loop.
updateWidgetWithRetry and buildWidgetViews helpers
android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt
Implements updateWidgetWithRetry (catches updateAppWidget failures, halves dimensions, retries) and buildWidgetViews (centralized RemoteViews creation with native/fallback bitmap rendering, placeholder/refresh visibility, and theme override).
onAppWidgetOptionsChanged integration
android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt
Replaces single-pass native bitmap generation with updateWidgetWithRetry; adds early-exit guard after the retry block to skip the weather-fetch fallback when rendering succeeded.
onUpdate integration
android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt
Removes per-widget manual updateAppWidget/try-catch; delegates each widget update to updateWidgetWithRetry with buildWidgetViews for the current retry dimensions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • timbortnik/widget#35: Directly related — also modifies MeteogramWidgetProvider to prevent widget crashes from over-budget bitmap updateAppWidget() calls by shrinking/retrying chart bitmap dimensions.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main fix: handling launcher bitmap-budget rejections to prevent widget freezing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/widget-budget-rejection-retry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt (4)

420-457: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resize path re-implements buildWidgetViews instead of reusing it.

This callback duplicates the click-intent wiring, native light/dark chart generation, and applyThemeOverride that already live in buildWidgetViews. The only intended difference is returning null when no native bitmap was produced (so the caller can fall back to a weather fetch) versus showing the placeholder. Keeping two near-identical RemoteViews builders invites drift (e.g., placeholder/refresh-indicator/theme handling diverging between resize and update). Consider parameterizing buildWidgetViews (e.g., a nativeOnly/returnNullIfNoChart flag) so both paths share one builder.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt`
around lines 420 - 457, The resize/update callback in MeteogramWidgetProvider is
duplicating the RemoteViews construction logic from buildWidgetViews, which
risks the two paths drifting apart. Refactor the shared widget rendering into
buildWidgetViews (or a small helper it calls) and add a parameter such as
nativeOnly or returnNullIfNoChart so the resize path can return null when both
chart bitmaps are missing while still reusing the same light/dark chart
generation, applyThemeOverride, and click PendingIntent wiring.

285-303: 🚀 Performance & Scalability | 🔵 Trivial

Retry mechanism is sound, but it can amplify synchronous bitmap work on the main thread.

onUpdate (a BroadcastReceiver callback) and onAppWidgetOptionsChanged run on the app's main thread. On the rejection path, each retry rebuilds the RemoteViews, which regenerates two SVG charts (light + dark) via generateChartBitmap. Worst case is roughly MAX_UPDATE_ATTEMPTS × 2 synchronous SVG renders per widget, all on the main thread, which can risk an ANR when several widgets are rejected at once. The happy path is unaffected; consider whether the rejection path should be offloaded (e.g., via goAsync()/a background dispatcher) if field telemetry shows rejections clustering.

As per path instructions: "Watch for RemoteViews restrictions, null safety, and main-thread work."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt`
around lines 285 - 303, The retry loop in MeteogramWidgetProvider’s widget
update path is rebuilding RemoteViews and generating SVG bitmaps synchronously
on the main thread for every failed attempt, which can multiply expensive work
during rejection retries. Move the rejection-path update work in
onUpdate/onAppWidgetOptionsChanged off the BroadcastReceiver main thread, for
example by using goAsync() with a background dispatcher or equivalent, and keep
buildViews/updateAppWidget retries from blocking the UI thread while preserving
the existing retry logic and logs.

Source: Path instructions


305-311: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: the “pure function” doc comment overstates the contract.

buildWidgetViews reads SharedPreferences, emits Log calls, and rasterizes SVGs, so it isn't pure/referentially transparent. The relevant property for updateWidgetWithRetry is that it can be safely re-invoked at a smaller size; consider rewording to "deterministic for given inputs / safe to re-invoke at a smaller size" to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt`
around lines 305 - 311, Update the KDoc for buildWidgetViews in
MeteogramWidgetProvider to remove the claim that it is a “pure function,” since
it also reads SharedPreferences, logs, and rasterizes SVGs. Reword the contract
to say it is deterministic for the same inputs and safe to re-invoke at a
smaller size, which is the behavior relied on by updateWidgetWithRetry.

334-350: 🚀 Performance & Scalability | 🔵 Trivial

Note: both light and dark bitmaps count toward the same budget.

estimateMemoryUsage sums every bitmap in a single RemoteViews, so setting both widget_chart_light and widget_chart_dark at full size submits ~2× the per-chart memory in one update — which makes the over-budget rejection this PR guards against more likely on smaller screens. The shrink-retry correctly halves both, so this is informational, but if rejections are common you may want to render only the bitmap matching the active/forced theme rather than both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt`
around lines 334 - 350, The widget update path in MeteogramWidgetProvider
currently generates and submits both widget_chart_light and widget_chart_dark
bitmaps in the same RemoteViews, which counts twice against the shared memory
budget. If this becomes a problem, adjust the chart rendering logic in the
widget update flow to generate only the bitmap that matches the active or forced
theme instead of always creating both, while keeping the shrink-retry behavior
intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt`:
- Around line 420-457: The resize/update callback in MeteogramWidgetProvider is
duplicating the RemoteViews construction logic from buildWidgetViews, which
risks the two paths drifting apart. Refactor the shared widget rendering into
buildWidgetViews (or a small helper it calls) and add a parameter such as
nativeOnly or returnNullIfNoChart so the resize path can return null when both
chart bitmaps are missing while still reusing the same light/dark chart
generation, applyThemeOverride, and click PendingIntent wiring.
- Around line 285-303: The retry loop in MeteogramWidgetProvider’s widget update
path is rebuilding RemoteViews and generating SVG bitmaps synchronously on the
main thread for every failed attempt, which can multiply expensive work during
rejection retries. Move the rejection-path update work in
onUpdate/onAppWidgetOptionsChanged off the BroadcastReceiver main thread, for
example by using goAsync() with a background dispatcher or equivalent, and keep
buildViews/updateAppWidget retries from blocking the UI thread while preserving
the existing retry logic and logs.
- Around line 305-311: Update the KDoc for buildWidgetViews in
MeteogramWidgetProvider to remove the claim that it is a “pure function,” since
it also reads SharedPreferences, logs, and rasterizes SVGs. Reword the contract
to say it is deterministic for the same inputs and safe to re-invoke at a
smaller size, which is the behavior relied on by updateWidgetWithRetry.
- Around line 334-350: The widget update path in MeteogramWidgetProvider
currently generates and submits both widget_chart_light and widget_chart_dark
bitmaps in the same RemoteViews, which counts twice against the shared memory
budget. If this becomes a problem, adjust the chart rendering logic in the
widget update flow to generate only the bitmap that matches the active or forced
theme instead of always creating both, while keeping the shrink-retry behavior
intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d77c8ee8-adc4-40a0-b9c0-3cc2fdc6f50a

📥 Commits

Reviewing files that changed from the base of the PR and between ce4dfaa and 3de00a1.

📒 Files selected for processing (1)
  • android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt

Address PR review:
- Resize callback no longer re-implements the click intent + native chart
  generation + theme override; it now calls buildWidgetViews(..., nativeOnly =
  true). The nativeOnly flag skips the saved-SVG-file fallback and placeholder
  and returns null when no fresh chart is produced, preserving the resize path's
  "render now or fetch" behaviour while removing the duplicate builder (no more
  drift risk between the resize and update paths).
- Reword buildWidgetViews doc: it reads prefs, logs, and rasterizes, so it isn't
  "pure" — the relevant contract is deterministic-for-inputs / safe to re-invoke
  at a smaller size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@timbortnik timbortnik merged commit 63544d6 into main Jun 29, 2026
6 checks passed
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.

1 participant