Skip to content

fix: widget loading skeleton + first-add bitmap-memory crash, and edit-frequency carousel desync - #180

Merged
thomasluizon merged 1 commit into
mainfrom
fix/widget-render-and-edit-frequency
Jun 16, 2026
Merged

fix: widget loading skeleton + first-add bitmap-memory crash, and edit-frequency carousel desync#180
thomasluizon merged 1 commit into
mainfrom
fix/widget-render-and-edit-frequency

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Fixes two pre-existing bugs reported on-device (Android).

1. Android widget

Blank cold state. Before the first successful sync there is no cache and the binder-thread fetch is unreliable on ColorOS, so the card rendered blank. Added a loading skeleton (3 placeholder bars) shown while signed-in but never-synced (habits_updated_at == 0), so a freshly added widget never paints blank. Distinct from the existing "All clear!" empty state and the signed-out state.

"Problema ao carregar o widget" on first add. Confirmed via logcat:

java.lang.IllegalArgumentException: RemoteViews for widget update exceeds
maximum bitmap memory usage (used: 32713056, max: 21155904)
  ... AppWidgetManager.tryAdapterConversion ... notifyCollectionWidgetChange

The card background and every habit-row background were rasterized at full device resolution. Android inlines the collection items into one update and caps total bitmap memory at ~6× screen px; with ~30 rows (~26 MB) plus the first-add card bg sized to half-screen (AppWidgetOptions isn't populated on the first onUpdate, ~7 MB) it hit ~31 MB → the host rejected the update and showed the launcher's white error card. Re-add "worked" only because the bg was then smaller.

Fix: cap rasterized bitmaps to 512 px in createRoundedBitmap (radius/stroke scaled to match). All consumer ImageViews are scaleType="fitXY", so the capped bitmap upscales to fill — identical look, but widget bitmap memory drops from ~31 MB to ~5 MB (headroom for 100+ rows).

2. Edit habit frequency carousel (web + mobile)

Editing a Recurrent habit showed "Tarefa única" on the card even though the dots/detail fields were correctly on Recurrent — a scroll-position-vs-state desync: the carousel scrolled to the active card before its slides had laid out at full width, so scrollTo clamped back to index 0.

  • Mobile: re-position on onContentSizeChange (fires after slides reach full width); card tap now selects the tapped card instead of re-applying the active index (was frequencyHandlers[activeIndex]); per-card accessibility state corrected. Regression test added.
  • Web: scroll effect retries via requestAnimationFrame until the track width is measured (parity hardening).

Verification

  • Mobile: type-check, lint, 578 unit tests pass (incl. new card-tap test).
  • Web: type-check, lint, 1589 unit tests pass.
  • Release APK builds; on-device + logcat confirmed: widget renders on first add with 0 bitmap-memory errors, and the Recurrent habit opens on the correct frequency.

Notes

Both bugs predate this branch — the frequency carousel was reworked in #175/#177, and the full-resolution widget bitmaps are long-standing. The widget is Android-only (no web/iOS counterpart).

🤖 Generated with Claude Code

…it-frequency carousel desync

Widget (Android):
- Add a loading skeleton for the cold/never-synced window (gated on
  habits_updated_at) so a freshly added widget never renders as a blank card.
- Cap rasterized RemoteViews bitmaps at 512px in createRoundedBitmap (consumer
  ImageViews are fitXY, so they upscale). The card + every habit-row background
  were rendered at full device resolution; Android inlines the collection on
  update and rejects views whose summed bitmap memory exceeds ~6x screen
  ("RemoteViews for widget update exceeds maximum bitmap memory usage"), which
  surfaced as the launcher's white "Problem loading widget" on first add
  (~31MB used vs ~20MB cap). Now ~5MB.

Edit habit frequency (web + mobile):
- The frequency carousel scrolled to the active card before its slides laid out
  at full width, so it stayed on "one-time" while the form state (dots + detail
  fields) was correctly Recurrent. Mobile re-positions on onContentSizeChange;
  web retries via requestAnimationFrame until the track width is measured.
- Mobile card tap now selects the tapped card instead of re-applying the active
  index (+ regression test); per-card accessibility state corrected.

Both bugs were pre-existing (carousel rework in #175/#177; full-resolution
widget bitmaps predate this branch).

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

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jun 16, 2026 5:51pm

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 6aaa9c9 into main Jun 16, 2026
8 checks passed
@thomasluizon
thomasluizon deleted the fix/widget-render-and-edit-frequency branch June 16, 2026 17:59
thomasluizon added a commit that referenced this pull request Jun 19, 2026
…ne (#224) (#234)

The widget refresh spinner could spin forever because widget_refresh_loading
was only ever hidden inside OrbitWidgetFactory.onDataSetChanged, the
ColorOS-unreliable RemoteViews binder callback. When that callback is dropped
(or the synchronous binder-thread fetch stalls), no clear-site ran.

Decouple the spinner-clear from the binder callback: when onReceive raises the
spinner it now enqueues a one-time OrbitWidgetRefreshTimeoutWorker (12s, unique,
REPLACE) that force-restores the refresh button even if onDataSetChanged never
fires. The deadline (12s) sits above the native fetch budget (5s connect + 5s
read) so a slow-but-succeeding fetch still wins the race; a normal resolve
clears the spinner first and the worker becomes a harmless no-op.

The worker recomputes the #180 cold-state skeleton gate from prefs
(!isSignedOut && habits_updated_at <= 0) instead of blind-hiding it, and
touches only setViewVisibility on the two spinner flags + the skeleton flag, so
it neither re-rasterizes bitmaps nor stomps list content. The app-fed
syncWidgetData feed is untouched.

The pure skeleton predicate and the timeout constant are mirrored in a small TS
module (lib/widget-refresh-timeout.ts) with Vitest coverage, since Kotlin has no
unit runner in this repo.

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