Ring the watch natively before Rust boots, plus a dev fan-out toggle - #299
Open
ScottMorris wants to merge 2 commits into
Open
Ring the watch natively before Rust boots, plus a dev fan-out toggle#299ScottMorris wants to merge 2 commits into
ScottMorris wants to merge 2 commits into
Conversation
3 tasks
This was referenced Aug 13, 2026
ScottMorris
marked this pull request as ready for review
August 14, 2026 04:44
Contributor
Author
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a7d9ac9231
ℹ️ 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".
ScottMorris
force-pushed
the
feat/255-p3b-wear-sync-listener
branch
from
August 18, 2026 16:56
a7d9ac9 to
7ff293e
Compare
…v fan-out toggle **Native fan-out (issue #254 fix):** a `ContentProvider` (`WearRingInitProvider`) registers `NativeFiredListener` with `NativeEventBus` in `onCreate()`, which is guaranteed to run before `AlarmReceiver.onReceive()` even on a cold multi-plugin process start. The listener builds the ring payload entirely from `WearSyncCache` (no Rust involved), skips stale events (>90s per the frozen Phase 3 payload contract) and skips when the new developer toggle disables it, then sends the ring via Play Services and returns the `"watch-ring"` tag *after initiating* the send (at-least-once semantics). `WearSyncPlugin.sendAlarmRing`'s JSON-construction logic is factored into a shared `buildAlarmRingPayload` helper so both the Rust-invoked and native paths build an identical wire format. **Rust-side gate:** wear-sync's `alarm:fired` listener now skips `send_alarm_ring` when the event's `handled_natively` already contains `"watch-ring"`, or when the event is stale by the same 90s window -- the staleness half alone retroactively fixes the shipped "2:30 AM ghost ring" bug for events queued before this ships. `handled_natively: Vec<String>` (`#[serde(default)]`) is added to wear-sync's own local `AlarmFired` copy in `models.rs` (a separate type from the app crate's `events.rs`, which Phase 3C owns). **Developer toggle:** `getNativeFanOutEnabled`/`setNativeFanOutEnabled` Tauri commands (SharedPreferences-backed on the Kotlin side via `NativeFanOutPrefs`), wired through `mobile.rs`/`desktop.rs` (desktop no-ops, always reports enabled) and a new `tauri-plugin-wear-sync-api` guest-js package, with a "Disable native watch fan-out" switch in Settings' Developer > Testing section (Android-only). `wear-sync:default` added to the app's default capability so the two new commands aren't silently ACL-denied from the webview. **Watch-originated dedup groundwork:** `WatchDismissAlarm`/`WatchSnoozeAlarm` (and the `WatchMessage` envelope they're threaded from) gain an `event_id`/`eventId` field, sourced from `WearSyncEventQueue`'s envelope id, so a same-process dedup pass keyed on event id (issue #255 Phase 3C) has something to key watch-originated dismiss/snooze on too -- not just the fired path, which Phase 3A already wires an id into. Kotlin JUnit (`NativeFiredListenerTest`, 17 tests) and Rust unit tests (`should_skip_native_watch_ring`/`is_stale` in `lib.rs`, `event_id` serde round-trips in `models.rs`) cover the new pure logic. Full `cargo test --workspace`, `cargo clippy -p tauri-plugin-wear-sync -- -D warnings`, `pnpm -r run typecheck`, `pnpm --filter threshold test`, and wear-sync's Kotlin `testDebugUnitTest`/`assembleDebug`/`assembleRelease` (via the CI mobile image) all pass -- no cross-branch dependency actually blocked this worktree's build, since wear-sync's `AlarmFired`/`WatchMessage` are its own local types, not shared with the app crate or alarm-manager. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013WVm6JTTpiSY8KSKJ3p5Qq
…ut listener Five fixes from review, all confirmed with a full re-run of cargo test/clippy/fmt, the Kotlin JUnit suite (via the CI mobile Docker image), and the frontend typecheck/test suite: - `NativeFiredListener.handle()` was reading `NativeFanOutPrefs` (a `SharedPreferences` boolean) synchronously inline, violating `NativeEventBus`'s own documented threading contract (cheap/non-blocking work only). The toggle's value has to be known synchronously though -- deferring it would mean returning the `"watch-ring"` tag optimistically, which would make the Rust-side gate skip its own ring too and drop the ring entirely whenever the toggle is off. Resolved by caching the toggle in an in-memory `@Volatile` field, warmed once by `WearRingInitProvider.onCreate()` (before the listener is even registered) rather than literally moving the read into the async coroutine, which would have reintroduced that correctness gap. - Factored the duplicated "iterate connected nodes, send message" loop out of `WearSyncPlugin.sendAlarmRing` and `NativeFiredListener.ringWatch` into a shared `sendAlarmRingToConnectedNodes`, alongside the already-shared `buildAlarmRingPayload` -- the two copies had already drifted (only one logged to `NativeEventLog`). - Cross-referenced the independently-defined 90s staleness window between Kotlin's `NativeFiredListener.STALENESS_WINDOW_MS` and Rust's `lib.rs::STALENESS_WINDOW_MS` with comments pointing at each other, so a future tuning of one doesn't silently leave the other out of sync. - Renamed `Settings.tsx`'s toggle state from a negated `nativeFanOutEnabled` (bound via `!nativeFanOutEnabled`/`!e.target.checked`) to `disableNativeFanOut`, bound straight through to the switch with no inversion at the render site -- the negation now lives only at the two points that cross the `enabled`-phrased API boundary. - Added an `acl_tests` module to wear-sync's `lib.rs`, mirroring alarm-manager's own (added after issue #195): asserts every `generate_handler!`-registered command has a matching `allow-*` permission in `permissions/default.toml`, and that the plugin's Rust-internal (`run_mobile_plugin`-only) commands stay out of that list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013WVm6JTTpiSY8KSKJ3p5Qq
ScottMorris
force-pushed
the
feat/255-p3b-wear-sync-listener
branch
from
August 18, 2026 17:08
7ff293e to
bacb63c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Phase 3B of #255's implementation plan — wear-sync's listener side of the fired→watch-ring fan-out. This is the piece that actually closes #254: a production
ContentProvider(WearRingInitProvider, always-on, not debug-gated — distinct from the earlier throwaway spike in #293) registers a native listener before any Activity/Service/BroadcastReceiver callback can run, even on a cold multi-plugin process start. When alarm-manager (#298) publishes a fired event, this listener rings the watch directly via Play Services, entirely natively, with no dependency on Rust/WebView having booted.Built against the same frozen payload contract as #298 and #300.
What's here
NativeFiredListener— skips (no tag) if the event is stale (>90s) or the dev toggle disables fan-out; otherwise builds the ring payload fromWearSyncCacheand sends it, returning"watch-ring"after initiating (not confirming) the send, per Design: shared native event-queue layer for plugin↔Rust handoff, plus cross-plugin native fan-out #255's at-least-once design decision.WearSyncPlugin.sendAlarmRing's payload construction and connected-nodes send loop are now shared helpers used by both the Rust-invoked path and the new native listener.alarm:firedlistener — the staleness half alone retroactively fixes the shipped "2:30 AM ghost ring" bug for events queued before this ships.event_idthreaded through watch-originated dismiss/snooze (WatchDismissAlarm/WatchSnoozeAlarm) so those topics can also participate in the dedup mechanism Carry the watch-ring tag through AlarmFired and dedup redelivered native events #300 builds — found and fixed independently while implementing this (mirrors a bug flagged in alarm-manager's own queue dispatch).Testing checklist (automated, no device needed)
cargo clippy -D warningsandcargo fmt --checkcleanpnpm -r run typecheck, 83 frontend teststestDebugUnitTest/assembleDebug/assembleRelease) — 30 tests pass; confirmed the provider is present in both debug and release merged manifests (genuinely always-on)Review status
Reviewed via
/code-review high. The dominant first-pass finding ("this feature is unreachable, nothing publishes to the bus") was an expected cross-branch artifact of reviewing this worktree in isolation — resolved once combined with #298. Real fixes applied: a synchronous SharedPreferences read on the listener's hot path (now a pre-warmed in-memory cache, with the reasoning for not simply deferring it documented — deferring would have broken the toggle's synchronous gating of the returned tag); a duplicated Play Services send loop consolidated into the shared helper; a double-negative UI toggle simplified; and an ACL parity test added, mirroring alarm-manager's existing pattern from a prior silent-ACL-denial incident (#195).Stacked on #298. Part of #255.