This content originally appeared on Level Up Coding – Medium and was authored by James Cullimore

I recently worked on extending an existing Android app to include a Wear OS counterpart. The goal was to build a “handover” feature, basically a way to block app usage on either the phone or the watch when the other one takes control.
If the user starts a task on the phone, the watch should stop responding. If the watch initiates something, the phone should lock itself out. Sounds simple, but it needs solid communication between the two devices, and that’s where the fun begins.
This post breaks down how I handled communication between a Wear OS watch and its paired phone using WearableListenerService, DataClient, and MessageClient. I’ll walk through each part of the implementation and highlight things to watch out for, like message paths, service lifecycle, and when you should start thinking about encryption if you’re passing anything sensitive.
Required Dependencies
To enable communication between your phone and watch apps, you’ll need the Wearable Data Layer API from Google Play Services.
Add this to your module-level build.gradle or build.gradle.kts file:
Groovy:
implementation 'com.google.android.gms:play-services-wearable:19.0.0'
Kotlin DSL (using version catalog):
implementation(libs.play.services.wearable)
And in your libs.versions.toml:
play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version = "19.0.0" }
Without this, you won’t be able to use Wearable.getDataClient(), getMessageClient(), or getNodeClient(), which means no messages, no data sync, and no communication between devices.
Listening for Data and Messages with WearDataListenerService
At the core of the communication is a custom WearableListenerService. This service runs in the background and listens for messages and data changes sent between the phone and the watch.
Here’s what it looks like:
This onCreate() block is just a sanity check to log the connected nodes. It tells us which devices are connected and ready for communication. Nothing fancy here, but useful during debugging to know the service is alive and connected to something.
This service handles two kinds of callbacks:
- onMessageReceived for lightweight messages
- onDataChangedfor larger, structured data updates
We’ll walk through both.
Handling Messages with onMessageReceived
Messages are great for quick, one-off signals, like telling the other device to take an action. That’s what onMessageReceived() handles.
Here, we’re listening for messages on a specific path /somePath. When a message comes in, we respond by calling publishData(), which we’ll break down in a second.
The message itself doesn’t need to carry any heavy data, just the fact that it arrived is enough to trigger something useful. In this case, it kicks off a data sync.
Make sure the path matches exactly on both the sender and receiver, or nothing happens. No error, no log , just silence. Easy to miss during debugging.
Sending Structured Data with publishData()
Messages are fine for triggers, but if you need to actually pass structured information, like text, timestamps, or state, you’ll want to use the DataClient.
Here’s how that looks in publishData():
We build a PutDataMapRequest and mark it as urgent so it syncs immediately. In this case, we’re sending a key-value pair ("foo": "bar") and a timestamp. Once the data is synced successfully, we locally update the device using setData("Hello").
This pattern lets both devices stay in sync without having to constantly poll each other. Data gets pushed only when something changes.
If your app is dealing with sensitive data, this is where you should pause and think about encryption, especially if you’re storing or transmitting anything private. The Wear OS data layer doesn’t encrypt payloads by default.
Receiving Data with onDataChanged()
Sync data items with the Data Layer API | Wear OS | Android Developers
When the other device pushes data, it lands in onDataChanged(). This is your entry point for reacting to updates from the data layer.
Here’s the relevant code:
Every time data is updated, this gets called. We loop through each DataEvent, check if it’s a change, and look at the path. If it matches something we care about (in this case, /data-path-1), we pass the item to setData().
Again, paths need to match perfectly between sender and receiver. I can’t stress this enough, most “it’s not working” issues in Wear data sync come down to mismatched paths.
You can also listen for deleted data if your app needs cleanup behavior, but it’s optional.
Applying Received Data with setData()
Once data comes in and matches our expected path, we need to do something with it. That’s the job of setData():
We extract the data using DataMapItem.fromDataItem(), then grab the value tied to "foo". In this example, it’s just stored in shared preferences, but you can adapt this to trigger UI changes, update state, or whatever fits your use case.
Finally, we broadcast an intent to restart the app. This is a pattern we used to reset the app when a handover happens. Either the watch or the phone gets a signal, saves the data, and restarts itself into the correct state.
This kind of pattern is especially helpful when you want the devices to act like extensions of each other, rather than two independent apps.
Restarting the App with a Broadcast Receiver
When a handover happens, we want the app to restart itself to reflect the new state. That’s triggered by this broadcast:
sendBroadcast(Intent("restartApp"))
And here’s how we catch that on the receiving side:
We register this receiver to listen for the "restartApp" action. When it fires, we unregister it (important to avoid leaks) and call triggerRebirth(), which restarts the app cleanly.
The receiver is registered like this:
ContextCompat.registerReceiver(
this,
restartAppBroadcastReceiver,
IntentFilter("restartApp"),
ContextCompat.RECEIVER_EXPORTED
)
We use ContextCompat.registerReceiver() here so it's compatible across different Android versions and works whether you're on the watch or phone.
This setup gives us a clean handoff: the device receives data, updates its local state, and restarts to reflect the new mode. No manual refreshes, no stale UI.
Declaring and Starting the Listener Service
To make everything work, we need to declare the WearDataListenerService in the manifest and ensure it’s properly wired to receive messages and data changes.
Here’s the manifest snippet:
This declares the service and the intent filters it should listen for. You need separate filters for messages and data events, each with the correct paths. These must match what you’re using in your code, if the paths differ, the system just ignores the messages.
To start the service manually (if needed), you can do:
private fun startWearableService() {
val intent = Intent(this, WearDataListenerService::class.java)
startService(intent)
}
In most cases, Wear OS will start the service when a message or data event is received. But starting it yourself can be useful during setup or debugging.
Only Start the Listener When a Watch is Connected
On the phone side, we use the same WearDataListenerService, but we don’t want it running unless there’s actually a watch connected. No point wasting resources on a background service that has nothing to talk to.
That’s where WearableManager comes in:
Before doing anything, like syncing profile data or starting the listener, we check if a wearable is connected. This avoids unnecessary service startups and keeps things efficient.
The wearableIsConnected() function checks for connected nodes. If none are found, the service isn’t started, and data isn’t pushed. Clean and simple.
Setting Up Emulators: Pairing Phone and Watch
To test your handover feature locally, you’ll want paired emulators for phone and watch. Here’s how I set it up, following Android’s official guide and emulator pairing assistant
Connect a watch to a phone | Wear OS | Android Developers
1. Create Your Emulators
- In Android Studio’s Device Manager, create two AVDs: one Wear OS device and one Phone device.
- For the watch, choose API level 28+ (Wear OS 3+). For the phone emulator, use Android 11+ so it supports the Wear companion app.
2. Launch Both Emulators
- Start both your phone and watch emulators before pairing. They need to be running for Android Studio to recognize them .
3. Enable Debugging Options
- On the watch emulator: go to Settings → System → About, tap Build number seven times to unlock Developer Options.
- In Developer Options, enable ADB debugging and Debug over Bluetooth
4. Forward ADB Port
- In a terminal, forward communication between emulators:
adb -d forward tcp:5601 tcp:5601
Adjust if prompted. This links the watch’s ADB port to the phone.
5. Use the Pairing Assistant
- In Device Manager, click the three-dot overflow menu for either device and select Pair Wearable. Choose the other emulator in the wizard.
- On the phone emulator, open the Wear OS companion app, go to the overflow menu, and select Pair with emulator.
6. Confirm Pairing
- Android Studio will indicate pairing with a small icon by each emulator. The phone’s Wear OS app will show “Connecting…” and then “Paired” .
Why This Matters
- This setup ensures your WearDataListenerService can actually detect messages and data changes.
- Without pairing, wearable.getNodeClient() won’t see any nodes, your service can’t start.
- It replicates real-world conditions, ensuring your handover logic behaves the same way it will on actual devices.
Troubleshooting
- No devices shown in pairing: Ensure both emulators are running, debugging enabled, and port forwarded.
- Pairing stuck on “Connecting…”: Re-run the ADB forward command and retry pairing within the Wear OS app.
Wrapping Up
The whole setup was designed to support a clean handover experience. When the phone takes control, the watch locks itself. When the watch takes over, the phone blocks access. The "restartApp" broadcast helps force the app into the right state, usually to show a blocking screen, based on who’s currently in charge.
Both devices run the same WearDataListenerService, but the logic ensures they only activate when needed. When the phone wants control, it sends a message to the watch. The watch then publishes the latest data back. That update is picked up by the phone, which sets its own state accordingly. It’s a deliberate round-trip to make sure both devices are always working off the same data.
One subtle but important piece is the timestamp. If you send the same data twice, Wear OS won’t trigger an update unless something actually changes. That’s great if you want to reduce noise, but if your app logic depends on reacting every time, regardless of content, you can add a timestamp to force a change event. If that’s not needed, skip the timestamp and let the system optimize for you.
This setup can also be extended to share login data between the devices. If a user logs in on the phone, you can securely pass a token or auth state to the watch, so it doesn’t have to prompt again.
Sign-in | Wear | Android Developers
Just keep in mind: for anything sensitive, add proper encryption. The data layer isn’t encrypted by default.
How I Built Seamless Watch Phone Handover in Wear OS was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding – Medium and was authored by James Cullimore