cranpose_services/wearable.rs
1//! Messages and byte streams between an application and the same application
2//! on the paired phone or watch: a watch app streams its microphone to the
3//! phone, and the phone sends its results back.
4//!
5//! On Android the link is the Wear OS Data Layer. An application turns it on
6//! with `cranpose { services.add("wearable") }`, which brings the framework's
7//! Java and Google Play services' wearable library into its build. Between
8//! an iPhone app and its Apple Watch app the link is WatchConnectivity, which
9//! the `wearable` feature of the `cranpose` crate turns on; the session has one
10//! peer, the paired device, and a peer cannot open links on the other. On other
11//! platforms, and in a build without the service, every call returns
12//! [`WearableError::Unavailable`].
13//!
14//! **Sending.** [`wearable_peers`] lists the connected devices and whether
15//! the application is installed on each, [`send_wearable_message`] delivers
16//! a small message, and [`open_wearable_stream`] opens a byte stream for
17//! continuous data such as audio. [`open_on_wearable_peer`] opens a link on a
18//! device's screen, such as its store page for this application, so a person
19//! can install the companion from the device they hold. Each call blocks
20//! until the platform answers, so call them off the UI thread.
21//!
22//! **The companion.** On Android the phone and watch applications must share
23//! one application id. On Apple devices the iPhone app carries the watch app,
24//! whose bundle id starts with the iPhone app's. The build advertises the application on the device it
25//! is installed on, so the other device sees it as soon as it is installed,
26//! also before it first runs.
27//!
28//! **Receiving.** The application installs one [`WearableReceiver`] with
29//! [`set_wearable_receiver`]. Events arrive on a platform thread. A
30//! [`WearableStream`] is read until it ends, on whatever thread the receiver
31//! hands it to. An event that arrives while no receiver is installed is
32//! dropped. The system starts the application without a window for what the
33//! paired device sends, so the receiver is installed in the application's
34//! process start (`AppLauncher::with_process_start` in `cranpose`), not in
35//! composable content.
36
37use std::{
38 io::{Read, Write},
39 sync::Arc,
40};
41
42use crate::registry::ServiceRegistry;
43
44/// A connected phone or watch.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct WearablePeer {
47 /// The platform's identifier of the device, the `peer` the other calls take.
48 pub id: String,
49 /// The name the person gave the device.
50 pub name: String,
51 /// Whether this application is installed on the device.
52 pub has_app: bool,
53}
54
55/// Why a call to the paired device did not happen.
56#[derive(thiserror::Error, Debug)]
57pub enum WearableError {
58 /// This platform or this build has no link to a paired device.
59 #[error("there is no link to a paired device in this build")]
60 Unavailable,
61 /// The platform reported a failure, for example a device out of range.
62 #[error("the paired device link failed: {0}")]
63 Failed(String),
64}
65
66/// A byte stream the paired device opened to this application.
67pub struct WearableStream {
68 /// The device that opened it.
69 pub peer: String,
70 /// The path it was opened under.
71 pub path: String,
72 reader: Box<dyn Read + Send>,
73}
74
75impl WearableStream {
76 /// A stream from `peer` under `path`, read through `reader`. Platform
77 /// backends create it.
78 pub fn new(peer: String, path: String, reader: Box<dyn Read + Send>) -> Self {
79 Self { peer, path, reader }
80 }
81}
82
83impl Read for WearableStream {
84 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
85 self.reader.read(buffer)
86 }
87}
88
89/// Something the paired device sent.
90pub enum WearableEvent {
91 /// A message sent with [`send_wearable_message`] on the other device.
92 Message {
93 /// The device that sent it.
94 peer: String,
95 /// The path it was sent under.
96 path: String,
97 /// Its bytes.
98 data: Vec<u8>,
99 },
100 /// A stream opened with [`open_wearable_stream`] on the other device.
101 Stream(WearableStream),
102}
103
104/// Receives what the paired device sends.
105pub trait WearableReceiver: Send + Sync {
106 /// Handles one event, on a platform thread.
107 fn receive(&self, event: WearableEvent);
108}
109
110impl<F: Fn(WearableEvent) + Send + Sync> WearableReceiver for F {
111 fn receive(&self, event: WearableEvent) {
112 self(event);
113 }
114}
115
116/// The platform's link to the paired device, installed by the platform
117/// backend with [`set_platform_wearable_link`].
118pub trait WearableLink: Send + Sync {
119 /// The connected devices.
120 fn peers(&self) -> Result<Vec<WearablePeer>, WearableError>;
121 /// Delivers `data` to `peer` under `path`.
122 fn send_message(&self, peer: &str, path: &str, data: &[u8]) -> Result<(), WearableError>;
123 /// Opens a byte stream to `peer` under `path`.
124 fn open_stream(&self, peer: &str, path: &str) -> Result<Box<dyn Write + Send>, WearableError>;
125 /// Opens `url` on `peer`'s screen.
126 fn open_url(&self, peer: &str, url: &str) -> Result<(), WearableError>;
127}
128
129static PLATFORM: ServiceRegistry<dyn WearableLink> = ServiceRegistry::new();
130static RECEIVER: ServiceRegistry<dyn WearableReceiver> = ServiceRegistry::new();
131
132/// Installs the platform's link, or removes it with `None`; without one,
133/// every call returns [`WearableError::Unavailable`]. Platform backends call it.
134pub fn set_platform_wearable_link(link: Option<Arc<dyn WearableLink>>) {
135 match link {
136 Some(link) => PLATFORM.set(link),
137 None => PLATFORM.clear(),
138 }
139}
140
141fn link() -> Result<Arc<dyn WearableLink>, WearableError> {
142 PLATFORM.get().ok_or(WearableError::Unavailable)
143}
144
145/// The connected phones or watches.
146pub fn wearable_peers() -> Result<Vec<WearablePeer>, WearableError> {
147 link()?.peers()
148}
149
150/// Delivers `data` to the application on `peer` under `path`.
151pub fn send_wearable_message(peer: &str, path: &str, data: &[u8]) -> Result<(), WearableError> {
152 link()?.send_message(peer, path, data)
153}
154
155/// Opens a byte stream to the application on `peer` under `path`; the other
156/// device receives it as [`WearableEvent::Stream`]. Dropping the writer
157/// closes the stream.
158pub fn open_wearable_stream(
159 peer: &str,
160 path: &str,
161) -> Result<Box<dyn Write + Send>, WearableError> {
162 link()?.open_stream(peer, path)
163}
164
165/// Opens `url` on `peer`'s screen, for example the store page of this
166/// application, so the person can install the companion there. The person
167/// sees it on that device; nothing happens here.
168pub fn open_on_wearable_peer(peer: &str, url: &str) -> Result<(), WearableError> {
169 link()?.open_url(peer, url)
170}
171
172/// Installs the application's receiver of what the paired device sends,
173/// replacing any earlier one, or removes it with `None`; later events are then
174/// dropped.
175pub fn set_wearable_receiver(receiver: Option<Arc<dyn WearableReceiver>>) {
176 match receiver {
177 Some(receiver) => RECEIVER.set(receiver),
178 None => RECEIVER.clear(),
179 }
180}
181
182/// Hands an event from the platform to the application's receiver, or drops
183/// it when there is none. Platform backends call it.
184pub fn publish_wearable_event(event: WearableEvent) {
185 match RECEIVER.get() {
186 Some(receiver) => receiver.receive(event),
187 None => log::warn!(
188 "a paired device sent something, and the application has no wearable receiver"
189 ),
190 }
191}
192
193#[cfg(test)]
194#[path = "tests/wearable_tests.rs"]
195mod tests;