Skip to content

Wire up the phone/watch sync bridge (issue #276) - #281

Merged
mapitman merged 1 commit into
mainfrom
issue-276-wear-sync-bridge
Sep 2, 2026
Merged

mapitman merged 1 commit into
mainfrom
issue-276-wear-sync-bridge

Conversation

@mapitman

@mapitman mapitman commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Step 4 of #276 (see #278-280 for steps 1-3). The phone now pushes its Next Up queue and playback position to a paired watch, and applies position reports back from it, both directions durable to either device being briefly unreachable.

  • QueueSyncPublisher (:app) debounces QueueRepository.observeQueue() and pushes snapshots via WearSyncClient; started from MainActivity.onCreate (not Application.onCreate, which also runs under every Robolectric-hosted unit test — same reasoning as the existing feedRefreshScheduler wiring already there).
  • PlaybackService's existing 5s position-save loop now also pushes to WearSyncClient alongside its local FeedRepository write.
  • PositionSyncApplier (:core) applies an incoming position update last-write-wins by timestamp; shared by both apps' manifest-registered WearableListenerService (WearSyncListenerService on :wear, PhoneSyncListenerService on :app), which wake the process even when it isn't already running.
  • WearQueueSyncApplier (:wear) applies an incoming queue snapshot into the watch's own local Room database (upserting the referenced Feed rows first, for the feed_items FK).
  • QueueRepository's download trigger is a no-op on the watch in phase 1 (NoOpQueueDownloadTrigger) — streaming only, downloads are phase 2 (Send downloaded episodes to Wear OS for offline playback (phase 2) #277).

Moved PlayServicesWearSyncClient (added in #280 under :wear) into :core along with its Hilt bindings, since :app now needs the same DataClient-backed WearSyncClient — Hilt's per-app module discovery means defining it once in :core wires it into both apps' components for free, avoiding a duplicate copy in each.

Hit and fixed a real bug along the way: an early version started the queue publisher from Application.onCreate(), which caused every Robolectric-hosted :app unit test to spin up a background coroutine hitting a real database file and real Play Services. Moved the start call to MainActivity.onCreate() instead, matching the codebase's existing pattern for exactly this kind of startup work.

Test plan

  • ./gradlew assembleDebug testDebugUnitTest lintDebug passes across :app/:core/:wear
  • New unit tests: QueueSyncPublisherTest (debounce/coalescing, failure resilience — against a virtual-time-controlled Flow rather than Room's real-dispatcher-backed one, which proved unreliable to assert exact timing against), PositionSyncApplierTest (last-write-wins), WearQueueSyncApplierTest (Room upsert/replace behavior)
  • Installed and launched both debug builds (phone emulator + mycasts_wear_test Wear OS emulator) — confirmed no crash on either
  • Not verified: a live phone↔watch sync round trip. That needs the two emulators (or a real watch) actually paired, which is separate setup from anything this PR touches — noted as a manual follow-up.

Step 4 of Wear OS phase 1: the phone now pushes its Next Up queue
and playback position to a paired watch, and applies position
reports back from it, both directions durable to either device
being briefly unreachable.

- QueueSyncPublisher (:app) debounces QueueRepository.observeQueue()
  and pushes snapshots via WearSyncClient; started from
  MainActivity.onCreate (not Application.onCreate, which also runs
  under every Robolectric-hosted unit test -- same reasoning as the
  existing feedRefreshScheduler wiring there).
- PlaybackService's existing 5s position-save loop now also pushes
  to WearSyncClient alongside its local FeedRepository write.
- PositionSyncApplier (:core) applies an incoming position update
  last-write-wins by timestamp; shared by both apps' manifest-
  registered WearableListenerService (WearSyncListenerService on
  :wear, PhoneSyncListenerService on :app), which wake the process
  even when it isn't already running.
- WearQueueSyncApplier (:wear) applies an incoming queue snapshot
  into the watch's own local Room database (upserting the
  referenced Feed rows first, for the feed_items FK).
- QueueRepository's download trigger is a no-op on the watch in
  phase 1 (NoOpQueueDownloadTrigger) -- streaming only, downloads
  are phase 2 (issue #277).

Moved PlayServicesWearSyncClient (added in #280 under :wear) into
:core along with its Hilt bindings, since :app now needs the same
DataClient-backed WearSyncClient -- Hilt's per-app module discovery
means defining it once in :core wires it into both apps' components
for free, avoiding a duplicate copy in each.

Verified: full assembleDebug/testDebugUnitTest/lintDebug across all
three modules, installed and launched both debug builds with no
crash. Did not verify a live phone<->watch sync round trip -- that
needs the two emulators (or a real watch) actually paired, which is
a separate setup from anything this PR touches.
@mapitman
mapitman merged commit d35a788 into main Sep 2, 2026
1 check passed
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