Class 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 Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interfaceHow a port recovers a parked delivery the cap discarded. -
Method Summary
Modifier and TypeMethodDescriptionstatic voidRegisters a listener for replicated data changes.static voidRegisters a listener for live messages from the peer.static voidRegisters a listener for changes to the link itself -- reachability, pairing, whether the peer app is installed.static voiddeliverDataChanged(String path, byte[] payload) Framework/port entry point: reports that the peer published or updated a replicated value.static booleandeliverDataChangedTracked(String path, byte[] payload) Framework/port entry point: asdeliverDataChanged(String, byte[]), reporting whether the delivery reached a registered listener rather than being parked for a cold start.static booleandeliverDataChangedTracked(String path, byte[] payload, Runnable onDelivered) As above, invokingonDeliveredonce application listeners have actually RUN.static booleandeliverDataChangedTracked(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.static voiddeliverDataRemoved(String path) Framework/port entry point: reports that the peer removed a replicated value.static voiddeliverDataRemoved(String path, Runnable delivered) The same, with a callback for a port holding the removal in a durable spool.static voiddeliverDataRemoved(String path, Runnable delivered, Runnable dropped) The same, telling the port when the cap discarded the removal instead of running it.static voiddeliverMessage(String path, byte[] payload, int replyToken) Framework/port entry point: hands a message received from the peer to the app.static voiddeliverMessage(String path, byte[] payload, int replyToken, Runnable delivered) The same, with a callback for a port that has the message written down somewhere durable.static voiddeliverMessage(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.static voiddeliverReply(int replyToken, byte[] payload, String error) static List<WearableNode> Returns the counterpart devices currently connected.static WearableMessageReads the replicated value at a path, as published by either side.Returns every path that currently holds a replicated value.static booleanThe same question for replicated-data listeners, which receive removals.static booleanFramework/port entry point: hands the peer's answer to the waiting reply handler.static booleanhasPendingReply(int replyToken) static booleanReturns true when the counterpart app is installed on the paired device.static booleanisPaired()Returns true when a counterpart device is paired, whether or not it is switched on or in range.static booleanReturns true when the peer app can receive a live message right now.static booleanReturns true when this PLATFORM provides a wearable link, not when a counterpart exists.static voidFramework/port entry point: reports that reachability, pairing or peer-app installation changed.static voidputData(WearableMessage data) Publishes the current value at a path, replacing whatever was there.static voidremoveData(String path) Removes the replicated value at a path.static voidRemoves a previously registered data listener.static voidRemoves a previously registered message listener.static voidRemoves a previously registered state listener.static voidrequestReplayAfterDrain(String key, Runnable replay) Framework/port entry point: asks forreplayto run once a listener exists.static voidFramework entry point: forgets every listener and everything parked for them.static voidrunWhenListenerRegisters(String key, Runnable action) Framework/port entry point: runsactionthe next time a listener of any kind registers.static voidsendMessage(WearableMessage message) Sends a live message to the peer app, with no reply expected.static voidsendMessage(WearableMessage message, WearableReplyHandler reply) Sends a live message to the peer app and waits for its answer.static voidFramework/port entry point: registers what to do about a discarded delivery.static voidtransferFile(String path, String name, byte[] contents) Sends a file to the peer in the background.
-
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, andisReachable()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
true if the platform provides the wearable link
-
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
WearableStateListenerand 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
true if a counterpart device is known to be paired
-
isReachable
public static boolean isReachable()Returns true when the peer app can receive a live message right now. This is the condition
sendMessage(WearableMessage)needs;putData(WearableMessage)does not.Returns
true if the peer app is reachable
-
isCompanionAppInstalled
public static boolean isCompanionAppInstalled()Returns true when the counterpart app is installed on the paired device. A watch that is paired but has no watch app installed is worth prompting the user about, and is the usual reason a correct-looking
sendMessagenever arrives.Returns
true if the peer app is installed
-
getConnectedNodes
Returns the counterpart devices currently connected. Apple pairs one watch at a time, so expect at most one; Wear OS allows several.
Returns
the connected nodes, never null
-
sendMessage
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
message: the payload to send
-
sendMessage
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
message: the payload to sendreply: notified with the answer, or null when no answer is wanted
-
putData
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
data: the payload to publish, addressed to the path to publish under
-
getData
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 anyinvokeAndBlock, do not call this frompaint().Parameters
path: the path to read
Returns
the value, or null when nothing is published at that path
-
removeData
Removes the replicated value at a path. The peer is notified through
WearableDataListener.dataRemoved(String).Parameters
path: the path to clear
-
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
the published paths, never null
-
transferFile
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
path: the path the peer matches onname: the file name to present to the peercontents: the file bytes
-
addMessageListener
Registers a listener for live messages from the peer. Register from your app's
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
l: the listener to add
-
removeMessageListener
Removes a previously registered message listener.
Parameters
l: the listener to remove
-
addDataListener
Registers a listener for replicated data changes. Register from your app's
init()for the same reason asaddMessageListener(WearableMessageListener).Parameters
l: the listener to add
-
removeDataListener
Removes a previously registered data listener.
Parameters
l: the listener to remove
-
addStateListener
Registers a listener for changes to the link itself -- reachability, pairing, whether the peer app is installed.
Parameters
l: the listener to add
-
removeStateListener
Removes a previously registered state listener.
Parameters
l: the listener to remove
-
deliverMessage
Framework/port entry point: hands a message received from the peer to the app. Called by the platform port on whatever thread the native transport uses; delivery is marshalled to the EDT, and queued if no listener has been registered yet.
Parameters
path: the path the message arrived onpayload: the encoded payloadreplyToken: a positive token when the peer is waiting for an answer, otherwise 0
-
deliverMessage
The same, with a callback for a port that has the message written down somewhere durable.
deliveredruns 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
path: the path the message arrived onpayload: the encoded payloadreplyToken: a positive token when the peer is waiting for an answer, otherwise 0delivered: 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.
deliveredsays the listeners saw it, so the record can go;droppedsays 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
droppedthe message was parked as an ordinary runnable, which is whatevictOne(List, String)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
path: the path the message arrived onpayload: the encoded payloadreplyToken: a positive token when the peer is waiting for an answer, otherwise 0delivered: run after the listeners have seen it, or nulldropped: run when the cap evicted this delivery undelivered, or null
-
hasMessageListener
public static boolean hasMessageListener()Framework/port entry point: hands the peer's answer to the waiting reply handler. Called by the platform port; a token with no waiting handler is ignored.
Parameters
replyToken: the token returned with the original requestpayload: the encoded reply payload, or null when the request failederror: a description of the failure, or null on success Framework/port entry point: whether a request is still waiting on this token.
A port uses this to decide whether an inbound reply is worth waking the application for. A reply whose requester is gone -- the process was killed while the peer was answering -- has nowhere to be delivered, and on Android starting the app for it brings the UI forward only for
deliverReply(int, byte[], String)to drop the payload on the next line.Parameters
replyToken: the token the reply arrived under
Returns
truewhen 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.
Displaybeing initialized is not the same question: between initialization and the app'saddMessageListenercall 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.Returns
true when at least one message listener is registered
-
hasDataListener
public static boolean hasDataListener()The same question for replicated-data listeners, which receive removals.
Returns
true when at least one data listener is registered
-
hasPendingReply
public static boolean hasPendingReply(int replyToken) -
deliverReply
-
deliverDataChanged
Framework/port entry point: reports that the peer published or updated a replicated value. Called by the platform port; queued across a cold start like a message.
Parameters
path: the path whose value changedpayload: the encoded new value
-
deliverDataRemoved
Framework/port entry point: reports that the peer removed a replicated value. Called by the platform port.
Parameters
path: the path whose value is gone
-
deliverDataRemoved
The same, with a callback for a port holding the removal in a durable spool.
See [#deliverMessage(String,byte[],int,Runnable)]:
deliveredruns once the listeners have been offered the removal, and never while it is parked for want of one.Parameters
path: the path whose value was removeddelivered: run after the listeners have seen it, or null
-
deliverDataRemoved
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
path: the path whose value was removeddelivered: run after the listeners have seen it, or nulldropped: run when the cap evicted this delivery undelivered, or null
-
notifyStateChanged
public static void notifyStateChanged()Framework/port entry point: reports that reachability, pairing or peer-app installation changed. Called by the platform port. Unlike payload delivery this is not queued -- state is re-queried by the listener, so a stale notification is worthless. -
deliverDataChangedTracked
Framework/port entry point: as
deliverDataChanged(String, byte[]), 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
path: the path whose value changedpayload: the encoded new value
Returns
truewhen a listener was registered and the delivery was dispatched;falsewhen it was queued for a listener that does not exist yet. -
deliverDataChangedTracked
As above, invoking
onDeliveredonce application listeners have actually RUN.The distinction matters for anything that records a delivery durably.
deliverreturning 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
path: the path whose value changedpayload: the encoded new valueonDelivered: run on the EDT after the listeners, or null
Returns
truewhen 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.
onRelinquishedundoes exactly that bookkeeping, so the payload is offered again on the next scan instead of waiting for a restart.Parameters
path: the path whose value changedpayload: the encoded new valueonDelivered: run on the EDT after the listeners, or nullonRelinquished: run when the parked delivery is evicted undelivered, or null
Returns
truewhen 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
Framework/port entry point: registers what to do about a discarded delivery.
Parameters
handler: the port's recovery action, or null to remove it
-
runWhenListenerRegisters
Framework/port entry point: runs
actionthe 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
key: identifies the operation; a later request with the same key supersedes this oneaction: what to run once someone is listening
-
requestReplayAfterDrain
Framework/port entry point: asks for
replayto 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
key: identifies the operation; a later request with the same key supersedes this onereplay: the port's re-offer action
-