public final class WearableConnection

  1. Object
  2. 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.DroppedDeliveryHandlerHow a port recovers a parked delivery the cap discarded.

Methods

public static boolean isSupported()Returns true when this PLATFORM provides a wearable link, not when a counterpart exists.
public static boolean isPaired()Returns true when a counterpart device is paired, whether or not it is switched on or in range.
public static boolean isReachable()Returns true when the peer app can receive a live message right now.
public static boolean isCompanionAppInstalled()Returns true when the counterpart app is installed on the paired device.
public static List<WearableNode> getConnectedNodes()Returns the counterpart devices currently connected.
public static void sendMessage(WearableMessage message)Sends a live message to the peer app, with no reply expected.
public static void sendMessage(WearableMessage message, WearableReplyHandler reply)Sends a live message to the peer app and waits for its answer.
public static void putData(WearableMessage data)Publishes the current value at a path, replacing whatever was there.
public static WearableMessage getData(String path)Reads the replicated value at a path, as published by either side.
public static void removeData(String path)Removes the replicated value at a path.
public static List<String> getDataPaths()Returns every path that currently holds a replicated value.
public static void transferFile(String path, String name, byte[] contents)Sends a file to the peer in the background.
public static void addMessageListener(WearableMessageListener l)Registers a listener for live messages from the peer.
public static void removeMessageListener(WearableMessageListener l)Removes a previously registered message listener.
public static void addDataListener(WearableDataListener l)Registers a listener for replicated data changes.
public static void removeDataListener(WearableDataListener l)Removes a previously registered data listener.
public static void addStateListener(WearableStateListener l)Registers a listener for changes to the link itself – reachability, pairing, whether the peer app is installed.
public static void removeStateListener(WearableStateListener l)Removes a previously registered state listener.
public static void deliverMessage(String path, byte[] payload, int replyToken)Framework/port entry point: hands a message received from the peer to the app.
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.
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.
public static boolean hasMessageListener()Framework/port entry point: hands the peer’s answer to the waiting reply handler.
public static boolean hasDataListener()The same question for replicated-data listeners, which receive removals.
public static boolean hasPendingReply(int replyToken)
public static void deliverReply(int replyToken, byte[] payload, String error)
public static void deliverDataChanged(String path, byte[] payload)Framework/port entry point: reports that the peer published or updated a replicated value.
public static void deliverDataRemoved(String path)Framework/port entry point: reports that the peer removed a replicated value.
public static void deliverDataRemoved(String path, Runnable delivered)The same, with a callback for a port holding the removal in a durable spool.
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.
public static void notifyStateChanged()Framework/port entry point: reports that reachability, pairing or peer-app installation changed.
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.
public static boolean deliverDataChangedTracked(String path, byte[] payload, Runnable onDelivered)As above, invoking onDelivered once application listeners have actually RUN.
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.
public static void resetForReload()Framework entry point: forgets every listener and everything parked for them.
public static void setDroppedDeliveryHandler(WearableConnection.DroppedDeliveryHandler handler)Framework/port entry point: registers what to do about a discarded delivery.
public static void runWhenListenerRegisters(String key, Runnable action)Framework/port entry point: runs action the next time a listener of any kind registers.
public static void requestReplayAfterDrain(String key, Runnable replay)Framework/port entry point: asks for replay to run once a listener exists.

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

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 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

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 sendMessage never arrives.

Returns

true if the peer app is installed

getConnectedNodes

public static List<WearableNode> 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

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

message WearableMessage
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

message WearableMessage
the payload to send
reply WearableReplyHandler
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

data WearableMessage
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

path String
the path to read

Returns

the value, or null when nothing is published at that path

removeData

public static void removeData(String path)
Removes the replicated value at a path. The peer is notified through WearableDataListener.dataRemoved(String).

Parameters

path String
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

the published paths, never null

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

path String
the path the peer matches on
name String
the file name to present to the peer
contents byte[]
the file bytes

addMessageListener

public static void addMessageListener(WearableMessageListener l)
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 WearableMessageListener
the listener to add

removeMessageListener

public static void removeMessageListener(WearableMessageListener l)
Removes a previously registered message listener.

Parameters

l WearableMessageListener
the listener to remove

addDataListener

public static void addDataListener(WearableDataListener l)
Registers a listener for replicated data changes. Register from your app’s init() for the same reason as addMessageListener(WearableMessageListener).

Parameters

l WearableDataListener
the listener to add

removeDataListener

public static void removeDataListener(WearableDataListener l)
Removes a previously registered data listener.

Parameters

l WearableDataListener
the listener to remove

addStateListener

public static void addStateListener(WearableStateListener l)
Registers a listener for changes to the link itself – reachability, pairing, whether the peer app is installed.

Parameters

l WearableStateListener
the listener to add

removeStateListener

public static void removeStateListener(WearableStateListener l)
Removes a previously registered state listener.

Parameters

l WearableStateListener
the listener to remove

deliverMessage

public static void deliverMessage(String path, byte[] payload, int replyToken)
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 String
the path the message arrived on
payload byte[]
the encoded payload
replyToken int
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

path String
the path the message arrived on
payload byte[]
the encoded payload
replyToken int
a positive token when the peer is waiting for an answer, otherwise 0
delivered Runnable
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

path String
the path the message arrived on
payload byte[]
the encoded payload
replyToken int
a positive token when the peer is waiting for an answer, otherwise 0
delivered Runnable
run after the listeners have seen it, or null
dropped Runnable
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.

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()
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

public static void deliverReply(int replyToken, byte[] payload, String error)

deliverDataChanged

public static void deliverDataChanged(String path, byte[] payload)
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 String
the path whose value changed
payload byte[]
the encoded new value

deliverDataRemoved

public static void deliverDataRemoved(String path)
Framework/port entry point: reports that the peer removed a replicated value. Called by the platform port.

Parameters

path String
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

path String
the path whose value was removed
delivered Runnable
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

path String
the path whose value was removed
delivered Runnable
run after the listeners have seen it, or null
dropped Runnable
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

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

path String
the path whose value changed
payload byte[]
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

path String
the path whose value changed
payload byte[]
the encoded new value
onDelivered Runnable
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

path String
the path whose value changed
payload byte[]
the encoded new value
onDelivered Runnable
run on the EDT after the listeners, or null
onRelinquished Runnable
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)
Framework/port entry point: registers what to do about a discarded delivery.

Parameters

handler WearableConnection.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

key String
identifies the operation; a later request with the same key supersedes this one
action Runnable
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

key String
identifies the operation; a later request with the same key supersedes this one
replay Runnable
the port’s re-offer action