public final class WearableConnection
- Object
- WearableConnection
The link between a phone app and its watch app. The same API on both ends, and the same API on Apple Watch and Wear OS.
// On the phone: publish state the watch should show whenever it next wakes.
WearableConnection.putData(new WearableMessage("/steps").put("count", steps));
// On the watch: react to it, and ask for a fresh value on demand.
WearableConnection.addDataListener(new WearableDataListener() {
public void dataChanged(WearableMessage data) { label.setText("" + data.getInt("count", 0)); }
public void dataRemoved(String path) { label.setText("--"); }
});
Register listeners from your app’s init(). A payload that arrives before the first listener is
registered – including the one that made the platform launch your app – is queued and replayed,
but only to a listener that exists by the time the EDT gets to it.
Where the platform provides no wearable link at all, isSupported() returns false and every
call here is an inert no-op, so this API needs no platform conditionals around it. Note what
that method does NOT tell you: an iPhone with no watch paired to it still reports true, because
the question is whether the API exists. Gate wearable UI on isPaired() or isReachable().
See the package documentation for how to choose between a message, replicated data and a file
transfer.
Nested types
interface WearableConnection.DroppedDeliveryHandler | How a port recovers a parked delivery the cap discarded. |
Methods
Inherited methods
Method details
isSupported
public static boolean isSupported()Returns true when this PLATFORM provides a wearable link, not when a counterpart exists.
False on a desktop build and on any platform with no wearable API at all, and when false every other call here does nothing. But an iPhone with no watch paired to it still reports true: the question this answers is whether the API is present, and Apple’s is. The same holds on Android whenever the app was built with the wearable glue.
Ask isPaired() whether a counterpart device is actually paired, and isReachable()
whether its app can receive something right now. Treating this method as either of those
will offer wearable features on a phone that has no watch.
Returns
isPaired
public static boolean isPaired()Returns true when a counterpart device is paired, whether or not it is switched on or in
range. Distinct from isReachable(), which asks whether its app can receive something now.
Do not decide your UI from a single call at startup. Both platforms answer from state that
is queried asynchronously, so the first calls in a cold process can report false for a
device that is paired – there is nothing to report until the first query lands. Register a
WearableStateListener and react when the answer changes; that is what it is for.
On Android there is one case this cannot see at all: a paired watch that has never run your watch app. The Data Layer exposes pairing only through the nodes it knows about, and a watch that never ran the app appears in no such list – so a phone that is genuinely paired reports false until the watch app has run once. Treat false as “no counterpart known”, not as proof that none exists, and prefer showing setup guidance over hiding it. Apple’s API answers the pairing question directly and has no such gap.
Returns
isReachable
public static boolean isReachable()sendMessage(WearableMessage) needs; putData(WearableMessage) does not.Returns
isCompanionAppInstalled
public static boolean isCompanionAppInstalled()sendMessage never arrives.Returns
getConnectedNodes
public static List<WearableNode> getConnectedNodes()Returns
sendMessage
public static void sendMessage(WearableMessage message)Sends a live message to the peer app, with no reply expected.
The message is delivered only if the peer is reachable; if it is not, the message is dropped.
Use putData(WearableMessage) when the peer needs to see it eventually rather than now.
Parameters
messageWearableMessage- the payload to send
sendMessage
public static void sendMessage(WearableMessage message, WearableReplyHandler reply)Sends a live message to the peer app and waits for its answer.
Exactly one method on the handler is called, on the EDT. A reply is not guaranteed: the peer may be asleep, out of range, or running a version of your app that does not know this path.
Parameters
messageWearableMessage- the payload to send
replyWearableReplyHandler- notified with the answer, or null when no answer is wanted
putData
public static void putData(WearableMessage data)Publishes the current value at a path, replacing whatever was there.
This is the transport to reach for by default. The value survives both apps being killed and reaches the peer whenever it next runs, so the peer always converges on the latest value. Because each path holds one value, this is state replication and not a message queue – two rapid updates to the same path may be collapsed into one delivery.
Parameters
dataWearableMessage- the payload to publish, addressed to the path to publish under
getData
public static WearableMessage getData(String path)Reads the replicated value at a path, as published by either side.
Null means the path holds nothing, and it is safe to act on that: where the platform has to
ask its own replication layer – Android does – the query is authoritative rather than
answered from a cache that a cold launch leaves empty. Called on the EDT that query runs
through invokeAndBlock, so the UI keeps painting while it waits; as with any
invokeAndBlock, do not call this from paint().
Parameters
pathString- the path to read
Returns
removeData
public static void removeData(String path)WearableDataListener.dataRemoved(String).Parameters
pathString- the path to clear
getDataPaths
public static List<String> getDataPaths()Returns every path that currently holds a replicated value.
An empty list means there is nothing published, not “not enumerated yet” – see
getData(String) for how that is arranged and what it costs on the EDT.
Returns
transferFile
public static void transferFile(String path, String name, byte[] contents)Sends a file to the peer in the background.
Delivery is not immediate and may happen after this app has exited – that is the point. Use it for anything too big for a message: a captured image, a synced document, a map tile.
Parameters
pathString- the path the peer matches on
nameString- the file name to present to the peer
contentsbyte[]- the file bytes
addMessageListener
public static void addMessageListener(WearableMessageListener l)init(): a
message queued while the app was starting is replayed only to listeners that exist by the
time the EDT drains the queue.Parameters
lWearableMessageListener- the listener to add
removeMessageListener
public static void removeMessageListener(WearableMessageListener l)Parameters
lWearableMessageListener- the listener to remove
addDataListener
public static void addDataListener(WearableDataListener l)init() for the
same reason as addMessageListener(WearableMessageListener).Parameters
lWearableDataListener- the listener to add
removeDataListener
public static void removeDataListener(WearableDataListener l)Parameters
lWearableDataListener- the listener to remove
addStateListener
public static void addStateListener(WearableStateListener l)Parameters
lWearableStateListener- the listener to add
removeStateListener
public static void removeStateListener(WearableStateListener l)Parameters
lWearableStateListener- the listener to remove
deliverMessage
public static void deliverMessage(String path, byte[] payload, int replyToken)Parameters
pathString- the path the message arrived on
payloadbyte[]- the encoded payload
replyTokenint- a positive token when the peer is waiting for an answer, otherwise 0
deliverMessage
public static void deliverMessage(String path, byte[] payload, int replyToken, Runnable delivered)The same, with a callback for a port that has the message written down somewhere durable.
delivered runs on the EDT once every registered listener has been offered the message –
not when it is queued. A port whose spool survives process death needs exactly that
distinction: releasing its record when the delivery was merely queued loses the message if
the process dies before the EDT gets to it, which is the failure the spool exists for.
Not called at all while the delivery is parked for want of a listener. That is the point: the record stays durable until something actually receives it.
Parameters
pathString- the path the message arrived on
payloadbyte[]- the encoded payload
replyTokenint- a positive token when the peer is waiting for an answer, otherwise 0
deliveredRunnable- run after the listeners have seen it, or null
deliverMessage
public static void deliverMessage(String path, byte[] payload, int replyToken, Runnable delivered, Runnable dropped)The same, telling the port when the cap discarded the delivery instead of running it.
A durable message needs BOTH callbacks or neither is safe. delivered says the listeners
saw it, so the record can go; dropped says the queue cap evicted it, so the record must
stay AND the port’s in-process claim has to be released, or nothing will ever claim that
record again in this process.
Without dropped the message was parked as an ordinary runnable, which is what
evictOne discards first and silently. A spool drain that queued more than the cap while
a listener existed – and then lost that listener before the EDT ran the batch, an app
deregistering on pause – had every re-parked message reach that path: evicted with no
callback of any kind, so the record stayed on disk marked in-flight, unclaimable until the
process restarted, having burned an attempt from its budget for a delivery no application
code ever saw.
Parameters
pathString- the path the message arrived on
payloadbyte[]- the encoded payload
replyTokenint- a positive token when the peer is waiting for an answer, otherwise 0
deliveredRunnable- run after the listeners have seen it, or null
droppedRunnable- run when the cap evicted this delivery undelivered, or null
hasMessageListener
public static boolean hasMessageListener()Returns
true when a request registered under that token is still outstanding.
Framework/port entry point: whether any application code is listening for messages yet.
A port with a durable spool asks this before handing a one-shot message to the in-memory
queue instead. Display being initialized is not the same question: between initialization
and the app’s addMessageListener call the queue holds payloads nothing has received, and a
process killed in that window loses a message the Data Layer does not retain either. A port
that can write the message down should keep doing so until someone is there to take it.
hasDataListener
public static boolean hasDataListener()Returns
hasPendingReply
public static boolean hasPendingReply(int replyToken)deliverReply
public static void deliverReply(int replyToken, byte[] payload, String error)deliverDataChanged
public static void deliverDataChanged(String path, byte[] payload)Parameters
pathString- the path whose value changed
payloadbyte[]- the encoded new value
deliverDataRemoved
public static void deliverDataRemoved(String path)Parameters
pathString- the path whose value is gone
deliverDataRemoved
public static void deliverDataRemoved(String path, Runnable delivered)The same, with a callback for a port holding the removal in a durable spool.
See [#deliverMessage(String,byte[],int,Runnable)]: delivered runs once the listeners have
been offered the removal, and never while it is parked for want of one.
Parameters
pathString- the path whose value was removed
deliveredRunnable- run after the listeners have seen it, or null
deliverDataRemoved
public static void deliverDataRemoved(String path, Runnable delivered, Runnable dropped)The same, telling the port when the cap discarded the removal instead of running it.
See [#deliverMessage(String,byte[],int,Runnable,Runnable)]. The listener side of an evicted removal is already covered – the drain re-announces it by path – but a port holding the removal in a durable spool is not: that re-announcement carries no callback, so its record stays claimed and undeliverable for the life of the process.
Parameters
pathString- the path whose value was removed
deliveredRunnable- run after the listeners have seen it, or null
droppedRunnable- run when the cap evicted this delivery undelivered, or null
notifyStateChanged
public static void notifyStateChanged()deliverDataChangedTracked
public static boolean deliverDataChangedTracked(String path, byte[] payload)Framework/port entry point: as deliverDataChanged, reporting whether the delivery reached
a registered listener rather than being parked for a cold start.
Ports use this where the answer changes what they record. A file transfer is the case: its one-shot claim must not be made durable while the payload exists only in this process’s pending queue, because a process death then loses the payload AND suppresses the redelivery that would have replaced it.
Parameters
pathString- the path whose value changed
payloadbyte[]- the encoded new value
Returns
true when a listener was registered and the delivery was dispatched; false when it was
queued for a listener that does not exist yet.deliverDataChangedTracked
public static boolean deliverDataChangedTracked(String path, byte[] payload, Runnable onDelivered)As above, invoking onDelivered once application listeners have actually RUN.
The distinction matters for anything that records a delivery durably. deliver returning
true means the runnable was handed to the EDT, not that it executed – a process death in
between loses the payload while the record says it arrived. A one-shot file transfer
suppresses its own redelivery on the strength of that record, so the difference between
“dispatched” and “delivered” is the difference between a duplicate and a permanent loss.
Parameters
pathString- the path whose value changed
payloadbyte[]- the encoded new value
onDeliveredRunnable- run on the EDT after the listeners, or null
Returns
true when a listener was registered and the delivery was dispatched.deliverDataChangedTracked
public static boolean deliverDataChangedTracked(String path, byte[] payload, Runnable onDelivered, Runnable onRelinquished)As above, additionally releasing the port’s in-process claim if the parked delivery is evicted to make room under the queue cap.
Dropping the runnable alone is not enough for a one-shot transfer. The ports suppress a
second callback for a payload they have already handed over – Android holds an in-memory
transfer claim, JavaSE has recorded the file in its seen set – so nothing would redeliver
it while this process stays alive, and Android’s sender-side retention can expire in the
meantime. onRelinquished undoes exactly that bookkeeping, so the payload is offered again
on the next scan instead of waiting for a restart.
Parameters
pathString- the path whose value changed
payloadbyte[]- the encoded new value
onDeliveredRunnable- run on the EDT after the listeners, or null
onRelinquishedRunnable- run when the parked delivery is evicted undelivered, or null
Returns
true when a listener was registered and the delivery was dispatched.resetForReload
public static void resetForReload()Framework entry point: forgets every listener and everything parked for them.
The simulator’s hot reload builds a NEW app instance while this class – static, and loaded by a class loader the reload does not replace – keeps the old one’s listeners. Every later wearable event was then delivered to both instances, so side effects ran twice and callbacks reached UI objects belonging to a screen that no longer exists. The old instance also stayed strongly reachable through these lists, so it could never be collected.
The parked deliveries and recovery records go with them. They describe work owed to the listeners being dropped; handing them to the replacement would deliver, as brand new, state the previous instance had already been told about.
Only the simulator calls this. On a device the process dies instead, which is why nothing needed it before.
setDroppedDeliveryHandler
public static void setDroppedDeliveryHandler(WearableConnection.DroppedDeliveryHandler handler)Parameters
handlerWearableConnection.DroppedDeliveryHandler- the port’s recovery action, or null to remove it
runWhenListenerRegisters
public static void runWhenListenerRegisters(String key, Runnable action)Framework/port entry point: runs action the next time a listener of any kind registers.
Distinct from requestReplayAfterDrain(String,Runnable), which asks “run this as soon as
a delivery can reach a listener” and therefore runs immediately when one already can. A
port holding a DURABLE record cannot use that: the Android bridge spools a one-shot message
and a data removal to disk, and its startup drain leaves behind whatever kind of listener
is not registered yet. Asked through the other method, a spooled MESSAGE record with a data
listener already registered would run the drain immediately, find the message listener
still absent, ask again, and run again – unbounded recursion. And nothing else re-triggers
that drain: only inbound traffic does, so after a cold launch with no new traffic the
records sat on disk indefinitely.
This one never runs the action inline. It fires on the registration itself, which is the event the port is actually waiting for, and a request re-armed from inside a waiter is simply held for the next registration.
Parameters
keyString- identifies the operation; a later request with the same key supersedes this one
actionRunnable- what to run once someone is listening
requestReplayAfterDrain
public static void requestReplayAfterDrain(String key, Runnable replay)Framework/port entry point: asks for replay to run once a listener exists.
A port whose payload was evicted from the pending queue cannot simply re-offer it: nothing has changed yet, so the delivery would be parked, immediately evict another one-shot to make room, and that one would re-offer in turn. Deferring to the moment the queue drains breaks that cycle – by then a listener exists and deliveries dispatch instead of parking.
Requests are keyed, and a repeat replaces the pending one. A port whose replay re-offers EVERYTHING it is holding – as a rescan does – should pass a constant key: one such action covers any number of evictions, and queueing one per evicted payload would both grow this map past the delivery cap and rescan the whole backlog once per eviction.
Runs immediately only when a listener exists AND nothing is parked or draining.
A registered listener is not on its own enough for a delivery to reach one. While a drain is in flight everything parks – that is what keeps a re-offer behind the batch already on its way – so running the replay here put it straight back into the queue it was evicted from. With more than MAX_PENDING transfers tracked, filling the queue evicted another one-shot, which asked for a replay, which this method ran immediately because the listener was still registered: the same durable backlog rescanned and re-evicted itself one stack frame deeper each time, until the process overflowed or stalled.
So the question is not “is there a listener” but “can a delivery reach one right now”. A nonempty queue answers no for the same reason a drain does, and both are cases the keyed request already handles – the next drain pass takes it, and drainPending will not finish while one is outstanding.
Parameters
keyString- identifies the operation; a later request with the same key supersedes this one
replayRunnable- the port’s re-offer action