Skip to content

Add an Android next-alarm home screen widget in a new home-widgets plugin - #307

Open
ScottMorris wants to merge 7 commits into
mainfrom
feat/156-next-alarm-widget
Open

Add an Android next-alarm home screen widget in a new home-widgets plugin#307
ScottMorris wants to merge 7 commits into
mainfrom
feat/156-next-alarm-widget

Conversation

@ScottMorris

@ScottMorris ScottMorris commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Threshold's first home screen widget: a glanceable Android card showing the next alarm's resolved time and label, following the mockup and build sheet in docs/ui/widgets/widget-next-alarm-mockup.svg and the design decided in #156's research comments. For a Random Window alarm this shows the actual sampled minute (the scheduler resolves it at schedule time), so there's always one precise time — never a range.

Per discussion, the widget lives in its own native plugin (plugins/home-widgets, Kotlin package ca.liminalhq.threshold.homewidgets) so #279's history widget can join it later. The core emits a new alarm:next-changed event (only when the app-wide next alarm actually changes); the plugin's Rust side forwards a flat snapshot to Kotlin via run_mobile_plugin; Kotlin persists it to plugin-owned ThresholdWidget SharedPreferences and forces a redraw. The provider renders purely from that snapshot, so the widget works with the app process dead and survives reboots. Taps deep-link via the existing threshold:// scheme (edit/<id>, empty state → home), cold-starting the full stack — no toggle/dismiss on the widget itself, keeping "Rust is the single writer" intact.

The widget follows the app's chosen theme (all six static themes and Material You). TypeScript owns theme resolution, so on every theme application the frontend maps the active theme's light and dark variants onto eight widget colour roles, WCAG-corrects them with the shared ensureContrastAA utility from #291, and pushes the pair via a new set_widget_theme app command — the established Rust-copy-of-a-TS-setting pattern. The palette rides inside alarm:next-changed; Kotlin persists it separately (theme: null means "not pushed yet", never "clear") and picks light/dark by the launcher's night mode at render time, so an OS theme flip while the app is dead still renders the right variant. The static resource palette remains the fallback before the first push.

Accessibility: text sizes are sp throughout; layout-bucket thresholds scale with the system fontScale so large-font settings get the narrow layout before the hero would clip; TalkBack announces a single sentence ("Next alarm 7:14 AM, Weekday Alarm. Opens Threshold.") via a root content description with child views excluded; the static light eyebrow was contrast-fixed from #e2703d (3.17:1 on white, under AA) to #b5582f (4.77:1); pushed theme colours are AA-corrected before they reach the widget.

Deliberately absent: webview commands on the plugin (COMMANDS is empty per docs/plugins/command-conventions.md — the theme push is an app-level command, not a plugin command), Android permissions (no build.rs injection), and desktop support (parked per the feasibility research). Known limitation: the snapshot only refreshes when Rust runs, so a dismiss from the heads-up notification while the app is fully cold leaves the widget showing the fired alarm until Rust next initializes — registered on #255 as a future native-bus consumer (invalidation signals only).

Closes #156

Changes

  • apps/threshold/src-tauri/src/alarm/events.rs / mod.rs: NextAlarm/AlarmNextChanged/WidgetThemePalettes payloads; compute_next_alarm (earliest next_trigger among enabled alarms, id tie-break) and emit_next_changed_if_needed with change-only emission under a single lock, called from emit_batch_update (before the alarms:batch:updated seal) and seeded at heal_on_launch; the 12/24-hour settings listener re-emits on a format toggle.
  • apps/threshold/src-tauri/src/commands.rs / lib.rs: new set_widget_theme command storing WidgetThemeState and re-emitting; plugin registration.
  • apps/threshold/src/theme/widgetTheme.ts + contexts/ThemeContext.tsx: pure theme→widget-palette mapping with AA correction, invoked on every theme application including Material You.
  • plugins/home-widgets/ (new crate tauri-plugin-home-widgets): Rust listener on alarm:next-changedrun_mobile_plugin("updateWidgetSnapshot", …), theme forwarded opaquely as a JSON string; empty COMMANDS; desktop no-op.
  • plugins/home-widgets/android/: HomeWidgetsPlugin.kt, NextAlarmWidget.kt (SharedPreferences persistence, RemoteViews rendering with unconditional palette application, pure formatWidgetTime/selectWidgetLayoutBucket helpers), WidgetTheme.kt (pure JVM theme/hex parsing), NextAlarmWidgetProvider.kt, FrameLayout-rooted hero + narrow layouts with a tintable background layer, light/dark resources, widget metadata, manifest receiver.
  • Cargo.toml (workspace members), apps/threshold/src-tauri/Cargo.toml + lib.rs (plugin registration), .github/workflows/test.yml (home-widgets added to test-kotlin-plugins).
  • docs/architecture/event-architecture.md: alarm:next-changed added to the taxonomy, event definitions (incl. theme semantics), and create-alarm flow ordering.

Test plan

  • cargo test --workspace — 98 passing, including compute_next_alarm, wire-contract JSON tests for snapshot + theme payloads, and the serde round-trip for WidgetThemePalettes
  • cargo fmt --all -- --check and cargo clippy --all-features -- -D warnings clean
  • scripts/check-headers.sh clean (licence headers on all new .rs/.kt/.ts)
  • TS: widgetTheme.test.ts + themes.test.ts pass (role mapping, AA-correction of a zero-contrast input); full vitest/typecheck otherwise unchanged — the only failures are the pre-existing vendored @tauri-apps/plugin-notification resolution errors, confirmed identical on the unmodified branch
  • Kotlin JUnit tests (time formatting, font-scale-aware layout buckets, theme/hex parsing) wired into CI's test-kotlin-plugins job
  • WCAG ratios measured: light eyebrow 3.17:1 → 4.77:1; all other static pairs 5.4–17.4:1
  • Codex review findings (race, height-blind bucket, persistent rail tint, comment wrapping) fixed in 84adc5e
  • On-device pass: add the widget, verify next-alarm display and live update on edit/toggle, theme change follows the app (incl. Material You), empty state, resize between hero/narrow, large-font rendering, TalkBack announcement, reboot survival, tap deep links, light/dark

🤖 Generated with Claude Code

https://claude.ai/code/session_01D9ka1GwQKWbcuVixhS4hyn

claude added 2 commits August 17, 2026 17:05
Recompute the earliest next_trigger among enabled alarms after every mutation (and once at heal-on-launch to seed startup) and emit `alarm:next-changed` only when the answer differs from the last emission, just before the `alarms:batch:updated` seal. The payload carries the alarm's id, label, and resolved trigger plus the 12/24-hour preference so native consumers can format times without a webview round trip. This feeds the upcoming home-widgets plugin (issue #156).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9ka1GwQKWbcuVixhS4hyn
…dget

New tauri-plugin-home-widgets crate (issue #156): plugin Rust listens for the core `alarm:next-changed` event and forwards a flattened snapshot to Kotlin via `run_mobile_plugin("updateWidgetSnapshot", ...)`. Kotlin persists it to the plugin-owned `ThresholdWidget` SharedPreferences and forces a redraw, so `NextAlarmWidgetProvider` renders purely from the snapshot -- the widget works with the app process dead, and taps deep-link through the existing `threshold://edit/<id>` / `threshold://home` routes, cold-starting the full stack.

**Deliberately absent:** no webview commands (`COMMANDS` is empty -- the plugin is driven entirely from Rust, per `docs/plugins/command-conventions.md`), no Android permissions (so no `build.rs` manifest injection), no TypeScript involvement, and no desktop implementation (parked per the issue's feasibility research).

Layouts follow the build sheet in `docs/ui/widgets/widget-next-alarm-mockup.svg`: a 4x2 hero and a 2x1 narrow variant chosen by host-reported width, light/dark palettes matching the default deep-night theme, and the alarm-card accent rail as the widget family's signature. Time formatting honours the app's 12/24-hour preference, falling back to the OS-wide setting when unknown.

Registered in the Cargo workspace and app builder; CI's `test-kotlin-plugins` job now also runs this plugin's JUnit tests.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9ka1GwQKWbcuVixhS4hyn
@ScottMorris ScottMorris added android Android toolchain and mobile CI concerns plugin Plugin work rust Rust tooling and build and test work labels Aug 18, 2026 — with Claude
@ScottMorris

Copy link
Copy Markdown
Contributor Author

@codex review

XML forbids `--` inside comments; AAPT2's resource parse failed on both widget layouts in CI's test-kotlin-plugins job (the local Rust test suite never exercises the Android resource compile, which is why this only surfaced in CI).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9ka1GwQKWbcuVixhS4hyn

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ffe3b1a2c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/threshold/src-tauri/src/alarm/mod.rs Outdated
Comment thread plugins/home-widgets/src/models.rs Outdated
Four findings from the automated review, all confirmed and fixed:

- **Race in `emit_next_changed_if_needed`:** the mutex now covers the database read and recompute, not just compare-emit-store, so overlapping mutations can no longer emit a stale snapshot out of order.
- **Height-blind layout selection:** `selectWidgetLayoutBucket` now also considers `OPTION_APPWIDGET_MIN_HEIGHT` (narrow below 100dp), so a wide one-row resize no longer clips the hero layout. Tests extended.
- **Muted rail tint persisting into the scheduled state:** the empty state now swaps to a dedicated `widget_rail_shape_muted` drawable (and the scheduled state sets the normal one explicitly) instead of a runtime colour filter, which RemoteViews reapplication could leave behind -- and which resolved light/dark in the app process instead of the launcher's.
- **Hard-wrapped comment prose:** newly added Rustdoc/Kotlin comment paragraphs consolidated onto single lines per AGENTS.md's Markdown formatting rule; the two multi-line licence-header summaries reduced to one sentence.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9ka1GwQKWbcuVixhS4hyn

Copy link
Copy Markdown
Contributor Author

All four Codex findings confirmed and fixed in 84adc5e (plus b0cd866 earlier for the CI failure — -- inside XML comments broke AAPT2's resource parse):

  • Race: the last_next_alarm mutex now covers the DB read and recompute, not just compare-emit-store, so overlapping mutations can't emit a stale snapshot out of order.
  • Height-blind bucket: selectWidgetLayoutBucket now also takes OPTION_APPWIDGET_MIN_HEIGHT (narrow below 100dp); a wide one-row resize renders the narrow layout instead of clipping the hero. Tests extended.
  • Persistent muted tint: the empty state swaps to a dedicated widget_rail_shape_muted drawable and the scheduled state sets the normal one explicitly — no runtime colour filter to survive a RemoteViews reapply, and the colour now resolves in the launcher's own light/dark configuration.
  • Wrapped comment prose: newly added Rustdoc/Kotlin comment paragraphs consolidated to single lines per AGENTS.md.

Generated by Claude Code

claude added 2 commits August 18, 2026 01:06
…lity

The widget now renders in the app's chosen theme instead of a fixed deep-night palette. TypeScript owns theme resolution (including Material You), so on every theme application the frontend maps the active theme's light and dark variants onto eight widget colour roles, runs each through the shared WCAG `ensureContrastAA` utility, and pushes the pair via a new `set_widget_theme` app command -- the established Rust-copy-of-a-TS-setting pattern (`TimeFormatState`, `SnoozeLengthState`). Rust stores it in `WidgetThemeState`, includes it in `alarm:next-changed` (change-only emission dedups as before), and the plugin forwards it to Kotlin as a JSON string. Kotlin persists the theme under its own prefs key -- `theme: null` on the wire means "not pushed yet" and never clears it -- and picks the light or dark palette by the current UI night mode at render time, so an OS theme flip while the app is dead still renders the right variant. Every render now applies one resolved palette to the background, rail, and all text unconditionally, which also removes the muted-rail drawable special case. The static resource palette remains as the fallback before the first push.

Accessibility fixes, all measured or testable:

- **Contrast:** the static light eyebrow `#e2703d` failed WCAG AA on white at 3.17:1; now `#b5582f` at 4.77:1. Theme-pushed colours are AA-corrected on the TS side before they ever reach the widget.
- **Font scale:** layout-bucket thresholds now scale with the system `fontScale`, so a large-font setting drops to the narrow layout before the hero's stacked rows would clip.
- **TalkBack:** the widget announces one sentence ("Next alarm 7:14 AM, Weekday Alarm. Opens Threshold.") via a root content description, with child views marked not important for accessibility.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9ka1GwQKWbcuVixhS4hyn
CI's `pnpm format:check` gates markdown too; the theme example JSON block now matches prettier's expansion.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9ka1GwQKWbcuVixhS4hyn
@ScottMorris

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cea58734a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/home-widgets/android/src/main/res/layout/widget_next_alarm_narrow.xml Outdated
… in one row

Two review findings: the single background colour filter flattened the card's 1dp stroke into the fill, so the background is now two stacked layers (stroke under a 1dp-inset fill) tinted independently; and the narrow layout's 28sp time plus 12dp padding could not fit the declared 40dp one-row minimum resize height, so it drops to 22sp with 4dp vertical padding. The now-dead gradient background drawables and their night-only colour pair are removed -- every render tints the background layers from the resolved palette.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9ka1GwQKWbcuVixhS4hyn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android Android toolchain and mobile CI concerns plugin Plugin work rust Rust tooling and build and test work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create an alarm home screen widget

2 participants