Class WearableConnection

java.lang.Object
com.codename1.wearable.WearableConnection

public final class WearableConnection extends Object

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.

  • 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: 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: the payload to send
      • reply: 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: 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: 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: 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: the path the peer matches on
      • name: the file name to present to the peer
      • contents: 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: the listener to add
    • removeMessageListener

      public static void removeMessageListener(WearableMessageListener l)

      Removes a previously registered message listener.

      Parameters
      • l: 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: the listener to add
    • removeDataListener

      public static void removeDataListener(WearableDataListener l)

      Removes a previously registered data listener.

      Parameters
      • l: 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: the listener to add
    • removeStateListener

      public static void removeStateListener(WearableStateListener l)

      Removes a previously registered state listener.

      Parameters
      • l: 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: the path the message arrived on
      • payload: the encoded payload
      • replyToken: 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: the path the message arrived on
      • payload: the encoded payload
      • replyToken: a positive token when the peer is waiting for an answer, otherwise 0
      • delivered: 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(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 on
      • payload: the encoded payload
      • replyToken: a positive token when the peer is waiting for an answer, otherwise 0
      • delivered: run after the listeners have seen it, or null
      • dropped: 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 request
      • payload: the encoded reply payload, or null when the request failed
      • error: 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

      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.

      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

      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: the path whose value changed
      • payload: 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: 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: the path whose value was removed
      • delivered: 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: the path whose value was removed
      • delivered: run after the listeners have seen it, or null
      • dropped: 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(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 changed
      • payload: 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: the path whose value changed
      • payload: the encoded new value
      • onDelivered: 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: the path whose value changed
      • payload: the encoded new value
      • onDelivered: run on the EDT after the listeners, or null
      • onRelinquished: 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: 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: identifies the operation; a later request with the same key supersedes this one
      • action: 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: identifies the operation; a later request with the same key supersedes this one
      • replay: the port's re-offer action