When it comes to performing quick, simple tasks, wearable apps have the advantage, as a smartwatch that’s right there on your wrist is always going to be more accessible than a smartphone or tablet that’s floating around somewhere in your bag.
But there’s no such thing as the perfect gadget, and no-one’s raving about their smartwatch’s battery life or claiming that it’s every bit as quick and powerful as their smartphone or tablet.
To deliver the best possible user experience, you need to play to a device’s strengths. If you’re developing for Wear OS (the operating-system-formerly-known-as-Android-Wear), then you’re in a unique position to cherry-pick the best features from two very different devices.
Basically, you can have the best of both worlds!
In this article, I’ll show you how to make the most out of everything Android OS and Wear OS have to offer, by opening a channel of communication between the two. Once your handheld app and its wearable counterpart are chatting, you can delegate tasks based on the device it’s best suited for—whether that’s offloading battery-intensive tasks to the handheld, or making sure your app’s most important information is always easily accessible by displaying it on the user’s wrist.
By the end of this article, you’ll have created a handheld and a wearable application that can exchange information via the Wearable Data Layer and the MessageClient
API.
What Is the Wearable Data Layer?
The Wearable Data Layer provides access to various client classes that you can use to store and retrieve data, without having to get your hands dirty with technical details such as data serialization. Once this information is on the Data Layer, it’s accessible to both the handheld and the wearable device.
In this article, we’ll be focusing on the MessageClient
API, which is a one-way communication mechanism that you can use to send information to the Wearable Data Layer. This API is particularly handy for executing remote procedure calls (RPC), such as launching an Activity
remotely on the paired handheld or wearable device.
Let’s look at an example: imagine you’ve created a navigation app. This app needs to a) retrieve location updates, and b) give the user directions.
Monitoring the device’s location is an intensive task that can quickly drain the limited battery available to your typical wearable. Using the MessageClient
API, your wearable app can instruct its handheld counterpart to perform this work instead. Once the handheld has performed this heavy lifting, it can send the resulting information back to the wearable via the Data Layer, so your app gets the information it needs without taking a chunk out of the wearable’s remaining battery.
As a general rule, if your wearable app needs to perform a task that requires significant battery or processing power, or complex user interactions, then you should consider offloading this work to the corresponding handheld app. By contrast, if your app deals with particularly time-sensitive information, or content that the user is likely to access on the go, then you should display this information on the wearable app.
In our navigation app example, pushing each set of directions from the handheld to the wearable makes this information more easily accessible, especially for someone who’s out and about, and hopelessly lost!
Out of the box, the MessageClient
API is a one-way communication mechanism, but you can implement bidirectional messaging by creating a sender and a receiver in both your project’s handheld and wearable module—which is exactly what we’re going to do.
Creating a Wearable and a Handheld Module
In this article, we’re going to create a wearable app that’ll recognise when its handheld counterpart sends a new message to the Data Layer. This wearable app will then respond by retrieving this message and displaying it as part of its UI.
Then, we’ll rinse and repeat, creating a handheld app that monitors the Data Layer for messages sent from its wearable counterpart.
Information sent via the MessageClient
API is only accessible to the application that created it. If the system is going to identify your wearable and handheld as belonging to the same application, then they’ll need to have the same package name, version code, and signing certificate. The easiest way to tick all these boxes is to create a project that consists of both a wearable and a handheld module:
- Create a new project called DataLayer.
- On the Target Android Device screen, select Phone and Tablet and Wear. Click Next.
- For your phone and tablet module, select the Empty Activity template, and then click Next.
- For your wearable module, select the Blank Wear Activity template, and then click Next, followed by Finish.
Creating Your Handheld App
Since we’re implementing bidirectional communication, both our handheld and our mobile modules need their own listener and sender. Let’s start by implementing this functionality in our handheld application.
I’m going to keep things simple and create a UI consisting of a TextView
that’ll display the various messages retrieved from the Data Layer and a button that, when tapped, will send its own message to the Data Layer.
Open your mobile module’s activity_main.xml file, and add the following:
Since we referenced a few dimens.xml values, we need to provide definitions for these values:
- Control-click the mobile module’s res/values directory.
- Select New > Values resource file.
- Name this file dimens.xml and then click OK.
- Add the following:
20dp 20dp
This gives us the following user interface:
Add Your Dependencies
Open the mobile module’s build.gradle file and add the following dependencies:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) wearApp project(':wear') implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.google.android.gms:play-services-wearable:+' }
Displaying and Sending Messages in MainActivity
In MainActivity
, we need to perform the following:
-
Keep the user in the loop!
When the user taps the Talk to the Wearable button, two things need to happen:
- The handheld sends a message to the wearable. I’m going to use “I received a message from the handheld.”
- The handheld provides visual confirmation that the message has been sent successfully. I’m going to use “I sent a message to the wearable.”
When the user taps the handheld’s Talk to the Wearable button, the handheld will attempt to send a message to the Data Layer. The system only considers this message successfully sent once it’s queued for delivery to a specific device, which means at least one paired device needs to be available.
In the best case scenario, the user taps Talk to the Wearable, the message gets queued for delivery, and our handheld triumphantly declares that: “I just sent a message to the wearable.”
However, if no wearable devices are available then the message isn’t queued, and by default the user gets no confirmation that our app has even tried to send a message. This could lead the user to wonder whether the app is broken, so I’m also going to display a Sending message…. notification, regardless of whether the message is successfully queued or not.
When testing this app, you may also want to trigger multiple messages in quick succession. To make it clear when each message has been queued for delivery, I’m adding a counter to each message, so our handheld will display I just sent a message to the wearable 2, I just sent a message to the wearable 3, and so on. On the other side of the connection, our wearable will display I just received a message from the handheld 2, I just received a message from the handheld 3, and so on.
2. Display received messages
In the next section, we’ll be creating a MessageService
that monitors the Data Layer and retrieves messages. Since our service will be performing its work on a different thread, it’ll broadcast this information to our MainActivity
, which will then be responsible for updating the UI.
3. Define the path
Every message you transmit via the MessageClient
API must contain a path, which is a string that uniquely identifies the message and allows your app to access it from the other side of the connection.
This path always starts with a forward slash (I’m using /my_path) and can also contain an optional payload, in the form of a byte array.
4. Check your nodes!
In Google Play services 7.3.0 and higher, you can connect multiple wearables to a single handheld device—for example, a user might splash out on multiple wearables that they switch between or use simultaneously. A Wear OS device may also be connected to multiple handheld devices during its lifetime, for example if the user owns an Android smartphone and a tablet, or they replace their old smartphone with a shiny new one. Note that any device that’s capable of connecting to the Data Layer is referred to as a node in the application code.
In this article, I’m going to assume there will only ever be a single available wearable. Alternatively, you can get selective about which devices you send messages to, using GetConnectedNodes or getLocalNode.
Let’s implement all this in our MainActivity
:
import android.support.v7.app.AppCompatActivity; import android.content.BroadcastReceiver; import android.widget.Button; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.support.v4.content.LocalBroadcastManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.View; import android.widget.TextView; import com.google.android.gms.wearable.Node; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.android.gms.wearable.Wearable; import java.util.List; import java.util.concurrent.ExecutionException; public class MainActivity extends AppCompatActivity { Button talkbutton; TextView textview; protected Handler myHandler; int receivedMessageNumber = 1; int sentMessageNumber = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); talkbutton = findViewById(R.id.talkButton); textview = findViewById(R.id.textView); //Create a message handler// myHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { Bundle stuff = msg.getData(); messageText(stuff.getString("messageText")); return true; } }); //Register to receive local broadcasts, which we'll be creating in the next step// IntentFilter messageFilter = new IntentFilter(Intent.ACTION_SEND); Receiver messageReceiver = new Receiver(); LocalBroadcastManager.getInstance(this).registerReceiver(messageReceiver, messageFilter); } public void messageText(String newinfo) { if (newinfo.compareTo("") != 0) { textview.append("n" + newinfo); } } //Define a nested class that extends BroadcastReceiver// public class Receiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { //Upon receiving each message from the wearable, display the following text// String message = "I just received a message from the wearable " + receivedMessageNumber++;; textview.setText(message); } } public void talkClick(View v) { String message = "Sending message.... "; textview.setText(message); //Sending a message can block the main UI thread, so use a new thread// new NewThread("/my_path", message).start(); } //Use a Bundle to encapsulate our message// public void sendmessage(String messageText) { Bundle bundle = new Bundle(); bundle.putString("messageText", messageText); Message msg = myHandler.obtainMessage(); msg.setData(bundle); myHandler.sendMessage(msg); } class NewThread extends Thread { String path; String message; //Constructor for sending information to the Data Layer// NewThread(String p, String m) { path = p; message = m; } public void run() { //Retrieve the connected devices, known as nodes// Task> wearableList = Wearable.getNodeClient(getApplicationContext()).getConnectedNodes(); try { List
nodes = Tasks.await(wearableList); for (Node node : nodes) { Task sendMessageTask = //Send the message// Wearable.getMessageClient(MainActivity.this).sendMessage(node.getId(), path, message.getBytes()); try { //Block on a task and get the result synchronously// Integer result = Tasks.await(sendMessageTask); sendmessage("I just sent the wearable a message " + sentMessageNumber++); //if the Task fails, then…..// } catch (ExecutionException exception) { //TO DO: Handle the exception// } catch (InterruptedException exception) { //TO DO: Handle the exception// } } } catch (ExecutionException exception) { //TO DO: Handle the exception// } catch (InterruptedException exception) { //TO DO: Handle the exception// } } } }
Create a Listening Service
At this point, our handheld is capable of pushing messages to the Data Layer, but since we want to implement bidirectional communication, it also needs to listen for messages arriving on the Data Layer.
In this section, we’re going to create a service that performs the following:
-
Monitor the Data Layer for events
You can monitor the Data Layer either by implementing the DataClient.OnDataChangedListener
interface or by extending WearableListenerService
. I’m opting for the latter, as there are a few benefits to extending WearableListenerService
. Firstly, WearableListenerService
does its work on a background thread, so you don’t have to worry about blocking the main UI thread. Secondly, the system manages the WearableListenerService
lifecycle to ensure it doesn’t consume unnecessary resources, binding and unbinding the service as required.
The drawback is that WearableListenerService
will listen for events even when your application isn’t running, and it will launch your app if it detects a relevant event. If your app only needs to respond to events when it’s already running, then WearableListenerService
can drain the device’s battery unnecessarily.
2. Override the relevant data callbacks
WearableListenerService
can listen for a range of Data Layer events, so you’ll need to override the data event callback methods for the events you’re interested in handling. In our service, I’m implementing onMessageReceived
, which will be triggered when a message is sent from the remote node.
3. Check the path
Every time a message is sent to the Data Layer, our app needs to check whether it has the correct my_path
identifier.
4. Broadcast messages to MainActivity
Since WearableListenerService
runs on a different thread, it can’t update the UI directly. To display a message in our application, we need to forward it to MainActivity
, using a LocalBroadcastManager
.
To create the service:
- Make sure you have the mobile module selected.
- Select New > Service from the Android Studio toolbar.
- Name this service
MessageService
. - Add the following:
import android.content.Intent; import android.support.v4.content.LocalBroadcastManager; import com.google.android.gms.wearable.MessageEvent; import com.google.android.gms.wearable.WearableListenerService; //Extend WearableListenerService// public class MessageService extends WearableListenerService { @Override public void onMessageReceived(MessageEvent messageEvent) { //If the message’s path equals "/my_path"...// if (messageEvent.getPath().equals("/my_path")) { //...retrieve the message// final String message = new String(messageEvent.getData()); Intent messageIntent = new Intent(); messageIntent.setAction(Intent.ACTION_SEND); messageIntent.putExtra("message", message); //Broadcast the received Data Layer messages locally// LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); } else { super.onMessageReceived(messageEvent); } } }
Finally, open the Manifest
and add some information to the MessageService
entry:
//Add metadata for Google Play Services// //Add the gms.wearable.MESSAGE_RECEIVED intent filter// //Specify your path, and a host for the filter. Here, I’m using a wildcard host//
As already mentioned, the system only considers a message successfully sent once it’s queued for delivery, which can only occur if one or more wearable devices are available.
You can see this in action by installing the mobile module on a compatible smartphone or tablet, or an Android Virtual Device (AVD). Click the Talk to the Wearable button and the app will display the Sending message… text only. The I just sent the wearable… text won’t make an appearance.
If our message is ever going to be queued for delivery, then we need to implement another set of sender and receiver components in our project’s wearable module.
Creating Your Wearable App
Our wearable app is going to have similar functionality as its handheld counterpart, so I’ll be skipping over all the code that we’ve already covered.
Once again, let’s start by creating the app’s user interface. Open the wear module’s activity_main.xml file and add the following:
//Wrap the layout in BoxInsetLayout so it displays correctly on both square and round watches//
At this point, your user interface should look something like this:
Open your build.gradle and add the following dependencies:
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) provided 'com.google.android.wearable:wearable:2.2.0' implementation 'com.google.android.support:wearable:2.2.0' implementation 'com.google.android.gms:play-services-wearable:12.0.1' implementation 'com.android.support:wear:26.1.0' }
Now, we need to send our message to the Data Layer:
import android.content.BroadcastReceiver; import android.os.Bundle; import android.widget.Button; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.support.v4.content.LocalBroadcastManager; import android.support.wearable.activity.WearableActivity; import android.widget.TextView; import android.view.View; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.android.gms.wearable.Wearable; import com.google.android.gms.wearable.Node; import java.util.List; import java.util.concurrent.ExecutionException; public class MainActivity extends WearableActivity { private TextView textView; Button talkButton; int receivedMessageNumber = 1; int sentMessageNumber = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.text); talkButton = findViewById(R.id.talkClick); //Create an OnClickListener// talkButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String onClickMessage = "I just sent the handheld a message " + sentMessageNumber++; textView.setText(onClickMessage); //Use the same path// String datapath = "/my_path"; new SendMessage(datapath, onClickMessage).start(); } }); //Register to receive local broadcasts, which we'll be creating in the next step// IntentFilter newFilter = new IntentFilter(Intent.ACTION_SEND); Receiver messageReceiver = new Receiver(); LocalBroadcastManager.getInstance(this).registerReceiver(messageReceiver, newFilter); } public class Receiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { //Display the following when a new message is received// String onMessageReceived = "I just received a message from the handheld " + receivedMessageNumber++; textView.setText(onMessageReceived); } } class SendMessage extends Thread { String path; String message; //Constructor for sending information to the Data Layer// SendMessage(String p, String m) { path = p; message = m; } public void run() { //Retrieve the connected devices// Task> nodeListTask = Wearable.getNodeClient(getApplicationContext()).getConnectedNodes(); try { //Block on a task and get the result synchronously// List
nodes = Tasks.await(nodeListTask); for (Node node : nodes) { //Send the message/// Task sendMessageTask = Wearable.getMessageClient(MainActivity.this).sendMessage(node.getId(), path, message.getBytes()); try { Integer result = Tasks.await(sendMessageTask); //Handle the errors// } catch (ExecutionException exception) { //TO DO// } catch (InterruptedException exception) { //TO DO// } } } catch (ExecutionException exception) { //TO DO// } catch (InterruptedException exception) { //TO DO// } } } }
Next, we need to create a listener that’ll monitor the Data Layer for incoming messages and notify MainActivity whenever a new message is received:
- Make sure the wear module is selected.
- Choose New > Service from the Android Studio toolbar.
- Name this service MessageService and then add the following:
import android.content.Intent; import com.google.android.gms.wearable.MessageEvent; import android.support.v4.content.LocalBroadcastManager; import com.google.android.gms.wearable.WearableListenerService; public class MessageService extends WearableListenerService { @Override public void onMessageReceived(MessageEvent messageEvent) { //If the message’s path equals "/my_path"...// if (messageEvent.getPath().equals("/my_path")) { //...retrieve the message// final String message = new String(messageEvent.getData()); Intent messageIntent = new Intent(); messageIntent.setAction(Intent.ACTION_SEND); messageIntent.putExtra("message", message); //Broadcast the received Data Layer messages locally// LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); } else { super.onMessageReceived(messageEvent); } } }
Open the module’s Manifest
, and create an intent filter for the WearableListenerService
:
//Add the gms.wearable.MESSAGE_RECEIVED intent filter// //Specify your path, and a host for the filter. Again, I’m using a wildcard//
You can download the complete project from GitHub.
Testing Your App
At this point you have two apps that can exchange messages over the Data Layer, but if you’re going to put these communication skills to the test, you’ll need to install your project on a handheld and a wearable device.
If you’re an Android developer, then chances are you have at least one Android smartphone or tablet laying around, but wearables still feel like a relatively new and niche product, so you might not have invested in a smartwatch just yet.
If you do decide to pursue Wear OS development, then you should take the plunge and purchase a smartwatch at some point, as there’s no substitute for testing your app on a real Android device. However, if you’re just experimenting with Wear OS, then you can create an AVD that emulates a wearable, in exactly the same way you create an AVD that emulates a smartphone or tablet. You can then get your AVD and your physical Android device talking, using port forwarding.
The first step is to create a wearable AVD and install your wear module on this emulated device:
- Select Tools > Android > AVD Manager from the Android Studio toolbar.
- Click Create Virtual Device…
- Select Wear from the left-hand menu.
- Choose the wearable that you want to emulate, and then click Next.
- Select your system image, and then click Next.
- Give your AVD a name, and then click Finish.
- Select Run > Run… from the Android Studio toolbar.
- In the little popup that appears, select Wear…
- Select the wearable AVD that you just created. After a few moments, the AVD will launch with your wearable component already installed.
Next, install the handheld module on your smartphone or tablet:
- Connect your physical Android device to your development machine.
- Select Run > Run… from the Android Studio toolbar.
- Choose mobile when prompted.
Finally, we need to get our physical Android device and our AVD talking:
- Make sure Bluetooth is enabled on your handheld (Settings > Bluetooth) and that it’s connected to your development machine via USB cable.
- On your handheld device, open the Play Store and download the Wear OS by Google app (formerly Android Wear).
- Launch the Wear OS application.
- On your emulated wearable, click the Home button in the accompanying strip of buttons (where the cursor is positioned in the following screenshot) and then open the Settings app.
- Select System > About and click the Build number repeatedly, until you see a You are now a developer message.
- Return to the main Settings menu by clicking the Back button twice. You should notice a new Developer Options item; give it a click.
- Select ADB Debugging.
- On your development machine, open a new Command Prompt (Windows) or Terminal (Mac) and then change directory (
cd
) so it’s pointing at the Android SDK’s platform-tools folder. For example, my command looks like this:
cd /Users/jessicathornsby/Library/Android/sdk/platform-tools
- Make sure ADB (Android Debug Bridge) is recognizing both the emulator and your attached smartphone or tablet, by running the
/.adb devices
command. It should return the codes for two separate devices. - Forward your AVD’s communication port to the attached smartphone or tablet, by running the following command in the Terminal/Command Prompt window:
./adb -d forward tcp:5601 tcp:5601
- On your handheld, launch the Wear OS app. Navigate through any introductory dialogues, until you reach the main Wear OS screen.
- Open the dropdown in the upper-left corner and select Add a new watch.
- Tap the dotted icon in the upper-right corner, and select Pair with emulator. After a few moments, the handheld should connect to your emulator.
You’re now ready to test your app! Launch the Wear component on your emulator and the mobile component on your handheld, and experiment by tapping the different Talk… buttons.
When you tap Talk to the wearable on the handheld, the following messages should appear:
- Handheld: “I just sent the handheld a message.”
- Wearable: “I just received a message from the handheld.”
When you tap Talk to the handheld on the wearable, the following messages should appear:
- Wearable: “I just sent the handheld a message.”
- Handheld: ‘I just received a message from the wearable.”
Conclusion
In this article, we looked at how to exchange messages between your handheld and your wearable app, over the Wearable Data Layer.
In production, you’d probably use this technique to do something more interesting than simply exchanging the same few lines of text! For example, if you developed an app that plays music on the user’s smartphone, you could give them the ability to play, pause, and skip songs directly from their wearable, by sending these instructions from the wearable to the handheld, over the Data Layer.
You can learn more about the Wearable Data Layer, including how to sync more complex data, over at the official Android docs.
Powered by WPeMatico