Skip to content

fix(widget): clamp chart bitmaps to launcher's memory budget on large resize#35

Merged
timbortnik merged 2 commits into
mainfrom
fix/widget-resize-bitmap-budget-crash
Jun 28, 2026
Merged

fix(widget): clamp chart bitmaps to launcher's memory budget on large resize#35
timbortnik merged 2 commits into
mainfrom
fix/widget-resize-bitmap-budget-crash

Conversation

@timbortnik

@timbortnik timbortnik commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Resizing the widget to cover more than ~75% of the screen area crashed both the widget and the app. The widget sets two bitmaps per update (light + dark charts, toggled by night-mode qualifiers), and the launcher rejects a RemoteViews update whose total bitmap memory exceeds 1.5x the screen area, throwing IllegalArgumentException from updateAppWidget():

RemoteViews for widget update exceeds maximum bitmap memory usage
(used: 200000000, max: 26956800)
at MeteogramWidgetProvider.onAppWidgetOptionsChanged(...)

The receiver runs in the app process, so the uncaught throw took the app and widget down together. Stock launchers cap widget size below the threshold, but third-party launchers (e.g. Smart Launcher) allow resizing past it.

Fix:

  • WidgetUtils.clampChartDimensions() scales requested bitmap dimensions down (preserving aspect ratio) so two bitmaps fit the budget, targeting 0.6x screen area each for margin. Applied on resize and on update.
  • Wrap both updateAppWidget() calls in try/catch as a safety net so an over-budget update can never crash the in-process receiver.

Covers MeteogramWidgetProvider and the inheriting weekly provider. Adds unit tests for the clamp math (pass-through, full-screen scaling, very tall/narrow, non-positive inputs).

Summary by CodeRabbit

  • Bug Fixes

    • Improved widget resizing so chart images stay within safe size limits on both resize and refresh.
    • Prevented widget updates from crashing when a launcher rejects an oversized image; failures are now handled gracefully.
  • Tests

    • Added coverage for widget size clamping, including normal, oversized, extreme aspect-ratio, and non-positive dimensions.

… resize

Resizing the widget to cover more than ~75% of the screen area crashed
both the widget and the app. The widget sets two bitmaps per update
(light + dark charts, toggled by night-mode qualifiers), and the launcher
rejects a RemoteViews update whose total bitmap memory exceeds 1.5x the
screen area, throwing IllegalArgumentException from updateAppWidget():

  RemoteViews for widget update exceeds maximum bitmap memory usage
  (used: 200000000, max: 26956800)
    at MeteogramWidgetProvider.onAppWidgetOptionsChanged(...)

The receiver runs in the app process, so the uncaught throw took the app
and widget down together. Stock launchers cap widget size below the
threshold, but third-party launchers (e.g. Smart Launcher) allow resizing
past it.

Fix:
- WidgetUtils.clampChartDimensions() scales requested bitmap dimensions
  down (preserving aspect ratio) so two bitmaps fit the budget, targeting
  0.6x screen area each for margin. Applied on resize and on update.
- Wrap both updateAppWidget() calls in try/catch as a safety net so an
  over-budget update can never crash the in-process receiver.

Covers MeteogramWidgetProvider and the inheriting weekly provider. Adds
unit tests for the clamp math (pass-through, full-screen scaling, very
tall/narrow, non-positive inputs).

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

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 48 minutes and 21 seconds. Learn how PR review limits work.

To continue reviewing without waiting, enable usage-based billing in the billing tab.

⌛ How to resolve this issue?

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 credits.

🚦 How do rate 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 see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ca0ea75c-6ab7-487b-a67f-639c406ef5b4

📥 Commits

Reviewing files that changed from the base of the PR and between d425f2d and aa1a843.

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

Walkthrough

Adds WidgetUtils.clampChartDimensions which scales requested bitmap dimensions to stay within 60% of screen area. Both onAppWidgetOptionsChanged and onUpdate in MeteogramWidgetProvider now use this helper instead of raw dp-to-px conversion, and both updateAppWidget calls are wrapped in try/catch. Four Robolectric tests cover the new helper.

Changes

Widget Bitmap Clamping

Layer / File(s) Summary
clampChartDimensions helper
android/.../WidgetUtils.kt
Adds import kotlin.math.sqrt and implements clampChartDimensions(context, widthPx, heightPx): computes screen area from displayMetrics, caps bitmap area at 60% of screen area, proportionally scales dimensions via sqrt when over budget, returns input unchanged for non-positive or in-budget inputs.
Apply clamping & guard updateAppWidget
android/.../MeteogramWidgetProvider.kt
Replaces direct dp→px conversion with clampChartDimensions in both onAppWidgetOptionsChanged and onUpdate; wraps both appWidgetManager.updateAppWidget(...) calls in try/catch that logs success or error instead of propagating exceptions.
Unit tests
android/.../WidgetUtilsTest.kt
Adds setScreen, screenArea, and assertFitsBudget test helpers, plus four @Test cases: in-budget passthrough, full-screen scaling with aspect-ratio preservation, tall/narrow clamping, and non-positive dimension passthrough.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 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 summarizes the main change: clamping widget chart bitmaps to avoid launcher memory-budget crashes during large resizes.
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-resize-bitmap-budget-crash

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 (1)
android/app/src/test/kotlin/org/bortnik/meteogram/WidgetUtilsTest.kt (1)

228-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the 0.6 safety target, not just the 0.75 hard cap.

These tests only prove the result fits the launcher's theoretical limit. A regression from the intended 0.6x screenArea budget to 0.75x would still pass, even though that margin is the core safety buffer described in this PR. Add at least one assertion that locks in the 0.6 target (or the exact expected full-screen clamp result).

Proposed test tightening
-    /** Two bitmaps of [w]x[h] must fit the launcher's 1.5x-screen budget. */
+    /** clampChartDimensions targets a 0.6x-screen area budget per bitmap. */
     private fun assertFitsBudget(w: Int, h: Int) {
         val area = w.toLong() * h.toLong()
         assertTrue(
-            "bitmap area $area must be <= 0.75x screen ${screenArea()} so two fit 1.5x",
-            area <= (screenArea() * 3L) / 4L
+            "bitmap area $area must be <= 0.6x screen ${screenArea()}",
+            area <= (screenArea() * 3L) / 5L
         )
     }
🤖 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/test/kotlin/org/bortnik/meteogram/WidgetUtilsTest.kt` around
lines 228 - 266, The WidgetUtilsTest coverage currently only verifies the
relaxed 0.75 screen-area cap, so a regression to the intended 0.6 safety budget
would still pass. Tighten the assertions in the clampChartDimensions tests,
especially the helper assertFitsBudget and the full-screen case, to validate the
0.6 target directly (or assert the exact expected clamped dimensions) using
WidgetUtils.clampChartDimensions and screenArea().
🤖 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/test/kotlin/org/bortnik/meteogram/WidgetUtilsTest.kt`:
- Around line 228-266: The WidgetUtilsTest coverage currently only verifies the
relaxed 0.75 screen-area cap, so a regression to the intended 0.6 safety budget
would still pass. Tighten the assertions in the clampChartDimensions tests,
especially the helper assertFitsBudget and the full-screen case, to validate the
0.6 target directly (or assert the exact expected clamped dimensions) using
WidgetUtils.clampChartDimensions and screenArea().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85c019db-9d3c-493f-ad08-c9bcbef6188a

📥 Commits

Reviewing files that changed from the base of the PR and between 9ad6a14 and d425f2d.

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

assertFitsBudget only checked the launcher's hard 0.75x-per-bitmap limit,
so a regression loosening clampChartDimensions' intended 0.6x margin to
0.75x would still pass. Tighten the bound to 0.6x screen area to lock in
the safety margin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@timbortnik timbortnik merged commit 0a88ac1 into main Jun 28, 2026
6 checks passed
timbortnik added a commit that referenced this pull request Jun 29, 2026
… freezing (#38)

* fix(widget): recover from launcher bitmap-budget rejection instead of 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>

* refactor(widget): unify resize path through buildWidgetViews (review)

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>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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