Repository navigation
Send to watch on Android: set up, send and remove books on a Wear OS watch - #284
Merged
Merged
Conversation
modules/audiosilo-watch gains android/: the same JS surface as the iOS WatchConnectivity link, over the Wear Data Layer, so the Android phone can set up a Wear OS watch and send it books. WearLink.kt is the phone's only Data Layer user: pair, book and remove go out as DataItems of UTF-8 JSON under /audiosilo/v1/to/<watchnode>/, the watch's watch-state and taken receipts come in through WearLinkService (Play services wakes the app) or the running app's listener. The phone deletes its own item only when a receipt's path and sent_at match the send it holds, since a cross-device delete loses to the put when the phone's clock runs ahead. Expired pairs are pruned at every snapshot, send and start. One watch at a time: the node with the app whose watch-state this phone saw last, else one nearby. openInstallOnWatch opens the Play listing on the watch; isSupported is false without Play services. The rules are pure in WearRules.kt with JVM tests (paths, book keys with wear-core's vector, the snapshot, the target, receipt matching, pair expiry, JSON). The phone advertises audiosilo_phone_app for the watch's Setup. Part of Track W4 (W4b).
src/watch/link.ios.ts becomes link.native.ts, so the Android phone reaches its Wear OS watch through the same WatchLink as the iPhone; link.ts stays the web no-op. Every pair, book and remove now carries sent_at (RFC 3339): on Wear OS it makes each send a new Data Layer item, orders a remove against a later download and is echoed in the watch's receipt; the Apple Watch ignores the field. A watchKind (apple or wear) picks the wording: on Android the card is "Wear OS watch", Set up says "Set up your watch", Downloads says "On your watch", and nothing asks for the watch app to be opened, since its listener service takes what is sent with the app closed. With no app on the watch the card offers Install on watch, which opens Play there. AppleWatchCard is now WatchCard. The Downloads section keeps the W3c rules; on Wear OS nothing is ever failed, so "delivered" means the watch app took it. New keys in all six locales; the Apple Watch texts are unchanged. Part of Track W4 (W4b).
The phone-side watch entry named link.ios.ts and said Android had no link. It now names link.native.ts and gains the Android bullet: WearLink as the one Data Layer user, the item paths and book key, sent_at, the receipt the phone acts on, pair pruning, the one-watch target, the phone's capability and the Wear OS wording. CLAUDE.md's index and layout lines follow. Part of Track W4 (W4b).
…event A watch back in reach puts a receipt per item, and both the listener service and the running app's listener see each one. Every receipt took the lock, read its item and deleted it alone, and every change sent JS a full snapshot (five Data Layer round trips), so a send queued behind the backlog and could answer "no watch" on the 5 s limit. Receipts from one callback are now settled under one lock against one read of the phone's items, the snapshot's four reads run together, and the watchState event is conflated: one snapshot at a time and one more for whatever changed meanwhile. A snapshot in flight is cancelled with the observer instead of reading the React context after the module is destroyed. A message's place under the target watch (pair, or book/<key>) is named once before the lock instead of through a dummy node, the book identity is read in one place, and the async functions use Expo's coroutine form with coded exceptions. Part of the Wear OS phone side (W4b).
NO_WATCH_LINK says kind 'apple', and an Android phone without Play services (or a binary built without the module) fell back to it, so any wording read off the link would have named an Apple Watch there. The native link now keeps the platform's kind when there is no way to a watch. Also one RFC 3339 helper for a message's times, and the conventions name the real symbol (watchLink.kind). Part of the Wear OS phone side (W4b).
The phone advertises audiosilo_phone_app from a string array that only Play services reads, by name. The tools:keep meant to protect it sat on the root of the module's values file, and that attribute is dropped when a library's values are merged, so the release shrinker reported the array as not reachable and removed it. A release phone app would then never advertise the capability, and the Wear app would offer "Get AudioSilo on your phone" with the app installed. Debug builds do not shrink, so nothing showed it. The keep is now a res/raw file of its own, which the shrinker does read from a library (resources.txt: reachable from keep xml file). Part of the Wear OS phone side (W4b).
The encoder compared against literal U+2028 and U+2029 characters, which read as two spaces in the source: a formatter or editor that normalises whitespace would turn the rule into a space check with no visible diff. Part of the Wear OS phone side (W4b).
Install on watch shared the card's error with Set up and only ever set a flag, so after one success and one failure the card said both that Google Play had opened on the watch and that it could not be opened, a failure stayed under the card once the app was installed, and a second tap while the watch had yet to answer asked it again. The ask is now idle, opening, opened or failed: the button shows it is waiting and ignores taps meanwhile, and each status shows only its own failure. Part of the Wear OS phone side (W4b).
The phone now stamps pair, book and remove with sent_at on every platform (Wear OS needs it). The Apple Watch does not read it; this pins that a stamped message decodes to the same message as one without, so the contract's change has a test on both sides. Part of the Wear OS phone side (W4b).
A Play services call that failed or took over 5 s (the wait for the link's lock included) made the Android snapshot answer paired: false with no watch id. JS takes that as the watch's real status: during a send the book's row was restored away, a second pairing token was minted for a watch already signed in, and outside a send the card, the Downloads section and Send to watch vanished until the next event. getSnapshot now rejects (ERR_NO_ANSWER, or the error) unless the Wearable API is known to be absent, and the watchState event is skipped, so refreshWatch keeps its last answer. The sweep at start and the receipt settle may hold the lock for 30 s at most, so a call that never answers cannot keep every send waiting, and the sweep asks whether the Wearable API is there first (no warning at every launch on a phone with no Wear OS app). A watchState event JS cannot take is logged instead of ending the process from an unhandled coroutine failure. The exceptions spell out their codes: R8 renames the classes, so a code inferred from the name came out as ERR_L90. Part of the Wear OS phone side (W4b).
This was referenced Nov 12, 2026
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.
The Android phone's half of Send to watch, for the Wear OS app from #282 (Track W4b, v2.7.0). With this, an Android phone sets up a Wear OS watch with no typing, sends and removes books, and shows the watch in Downloads, as the iPhone does for the Apple Watch.
What is in it
modules/audiosilo-watch/android/:WearLink.ktis the phone's only Data Layer user. It sendspair,bookandremoveas urgent DataItems under/audiosilo/v1/to/<node>/, settles the watch'stakenreceipts (it deletes its own item only on an exactsent_atmatch; the watch never deletes the phone's items), prunes expired pairs, readswatch-state, and opens Play on the watch (Install on watch).WearLinkServicewakes the app for receipts and state. The phone advertisesaudiosilo_phone_app, kept through release resource shrinking by ares/rawkeep file. The pure rules (paths, book key, target watch, snapshot, receipt matching, JSON) are inWearRules.ktwith JVM tests.isSupportedis false without Play services. When the Data Layer gives no answer,getSnapshotrejects instead of reporting "no watch", so the card and the ledger keep their last state.src/watch/link.ios.tsbecomeslink.native.ts(same surface on both platforms).watch-model.tsputssent_atonpair,bookandremove(the Apple Watch ignores it; a Swift test proves the decode is unchanged).AppleWatchCardbecomesWatchCard; on Android it says "your watch" and offers Install on watch with its own state. Downloads > On your watch on Android follows the W3c rules minusfailed: Waiting on your watch, Preparing, Downloading, On your watch. Hidden for kids, API keys and demo, as before. New keys in all six locales; the Apple keys are untouched..github/workflows/wear.ymlalso runs:audiosilo-watch:testDebugUnitTest.Tests and review
:audiosilo-watch, jest forsent_at, the wording choice, the Wear card and Downloads section and the ledger,npm run test:watch(245 Swift Core tests), the JS gate, prebuild then:wear:assembleDebug :wear:testDebugUnitTest :wear:assembleRelease :audiosilo-watch:testDebugUnitTest :app:assembleDebug. One jest suite (src/downloads/offline-companion.test.tsx, untouched here) failed once under full-suite load and passes alone.Verified live (Pixel 6a phone + Wear OS 5.1 emulator; no real Wear OS watch)
On the final heads of this branch and #282: Set up with no typing; Send to watch with the watch app open and swiped away, the row going Waiting on your watch, Preparing, Downloading, On your watch; Remove (playback stops, place kept); Send again; the other-account case on the card, in Downloads and on the watch; Install on watch opening Play on the watch; a 2.5.1 server hides Send to watch. An R8 release build keeps the phone's capability. The Apple Watch target still builds (
xcodebuild -scheme AudioSiloWatch); the phone's iOS app target was not built.Pairs with #282 (merged), KodeStar/audiosilo-docs#294, KodeStar/audiosilo-site#65, KodeStar/audiosilo#179.