Add an Android next-alarm home screen widget in a new home-widgets plugin - #307
Add an Android next-alarm home screen widget in a new home-widgets plugin#307ScottMorris wants to merge 7 commits into
Conversation
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
|
@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
There was a problem hiding this comment.
💡 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".
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
|
All four Codex findings confirmed and fixed in 84adc5e (plus b0cd866 earlier for the CI failure —
Generated by Claude Code |
…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
|
@codex review |
There was a problem hiding this comment.
💡 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".
… 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
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.svgand 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 packageca.liminalhq.threshold.homewidgets) so #279's history widget can join it later. The core emits a newalarm:next-changedevent (only when the app-wide next alarm actually changes); the plugin's Rust side forwards a flat snapshot to Kotlin viarun_mobile_plugin; Kotlin persists it to plugin-ownedThresholdWidgetSharedPreferences 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 existingthreshold://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
ensureContrastAAutility from #291, and pushes the pair via a newset_widget_themeapp command — the established Rust-copy-of-a-TS-setting pattern. The palette rides insidealarm:next-changed; Kotlin persists it separately (theme: nullmeans "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
spthroughout; layout-bucket thresholds scale with the systemfontScaleso 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 (
COMMANDSis empty perdocs/plugins/command-conventions.md— the theme push is an app-level command, not a plugin command), Android permissions (nobuild.rsinjection), 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/WidgetThemePalettespayloads;compute_next_alarm(earliestnext_triggeramong enabled alarms, id tie-break) andemit_next_changed_if_neededwith change-only emission under a single lock, called fromemit_batch_update(before thealarms:batch:updatedseal) and seeded atheal_on_launch; the 12/24-hour settings listener re-emits on a format toggle.apps/threshold/src-tauri/src/commands.rs/lib.rs: newset_widget_themecommand storingWidgetThemeStateand 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 cratetauri-plugin-home-widgets): Rust listener onalarm:next-changed→run_mobile_plugin("updateWidgetSnapshot", …), theme forwarded opaquely as a JSON string; emptyCOMMANDS; desktop no-op.plugins/home-widgets/android/:HomeWidgetsPlugin.kt,NextAlarmWidget.kt(SharedPreferences persistence, RemoteViews rendering with unconditional palette application, pureformatWidgetTime/selectWidgetLayoutBuckethelpers),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 totest-kotlin-plugins).docs/architecture/event-architecture.md:alarm:next-changedadded to the taxonomy, event definitions (incl. theme semantics), and create-alarm flow ordering.Test plan
cargo test --workspace— 98 passing, includingcompute_next_alarm, wire-contract JSON tests for snapshot + theme payloads, and the serde round-trip forWidgetThemePalettescargo fmt --all -- --checkandcargo clippy --all-features -- -D warningscleanscripts/check-headers.shclean (licence headers on all new.rs/.kt/.ts)widgetTheme.test.ts+themes.test.tspass (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-notificationresolution errors, confirmed identical on the unmodified branchtest-kotlin-pluginsjob🤖 Generated with Claude Code
https://claude.ai/code/session_01D9ka1GwQKWbcuVixhS4hyn