Wearable apps provide a great way to extend handheld devices; however, sometimes, a wearable is not powerful enough to perform a task on its own, or it needs information from the handheld.
As we are going to be communicating between the devices using Google Play services, we need to install the Xamarin.GooglePlayServices.Wearable NuGet before we start.
Apps running on wearables can communicate with apps running on the handheld devices using the Google Play services. To implement data communication between the handheld and the wearable, we can actually use the same code:
IDataApiDataListener
interface in our activity:public class MainActivity : Activity, IDataApiDataListener { public void OnDataChanged(DataEventBuffer dataEvents) { } }
OnCreate()
method of the activity. We use various callbacks to add and remove the Data API listener:apiClient = new GoogleApiClientBuilder(this) .AddConnectionCallbacks(bundle => { // attach the data listener WearableClass.DataApi.AddListener(apiClient, this); }, cause => { // detach the listener WearableClass.DataApi.RemoveListener(apiClient, this); }) .AddOnConnectionFailedListener(result => { // handle errors }) .AddApi(WearableClass.API) .Build();
Connect()
method of the client. This is typically done in the OnResume()
method of the activity when we are in the foreground:protected override void OnResume() { base.OnResume(); apiClient.Connect(); }
OnPause()
method:protected override void OnPause(){
if (apiClient != null &&apiClient.IsConnected) {
WearableClass.DataApi.RemoveListener(apiClient, this);
apiClient.Disconnect();
}
base.OnPause();
}
Once we are connected, we can start updating our app's data using the Data API. These updates are then synchronized with the handheld and wearable:
private const string CountKey ="keys.count"; private const string CounterPath = "/games/counter";
DataMap
and PutDataMapRequest
types:var mapRequest = PutDataMapRequest.Create(CounterPath); mapRequest.DataMap.PutInt(CountKey, count);
DataMap
instance over the Data API using the PutDataItemAsync()
method:var dataRequest = mapRequest.AsPutDataRequest(); var dataApi = WearableClass.DataApi; await dataApi.PutDataItemAsync(apiClient, dataRequest);
OnDataChanged()
method of the IDataApiDataListener
interface:public void OnDataChanged(DataEventBuffer dataEvents) { if (dataEvents.Status.IsSuccess) { // get the value from the event foreach (var dataEvent in dataEvents) { if (dataEvent.Type == DataEvent.TypeChanged) { var item = dataEvent.DataItem; if (item.Uri.Path == CounterPath) { var map = DataMapItem.FromDataItem(item).DataMap; count = map.GetInt(CountKey); break; } } } // make sure we update on the UI thread RunOnUiThread(() => { // update the UI }); } else { // handle errors } }
var dataApi = WearableClass.DataApi; var dataItems = await dataApi.GetDataItemsAsync(apiClient); foreach (var item in dataItems) { if (item.Uri.Path == CounterPath) { var dataMap = DataMapItem.FromDataItem(item).DataMap; count = dataMap.GetInt(CountKey); break; } }
Creating a wearable app is a great way to extend the handheld and provide an additional user experience. However, the device is often much more limited in hardware, most certainly so with regards to the screen size.
One of the great ways we can extend the handheld experience, but without losing on the power of the handheld, is by establishing communication between the devices. By communicating with a handheld, the wearable is able to offload any intensive or complex tasks to a more capable device.
The devices can communicate using several methods, and one of the easiest and most common methods is to synchronize the data, such as app settings or user preferences. This type of communication or synchronization uses the Google Play services' Data API.
The Data API not only synchronizes data between the wearable and handheld, but also with the cloud and any other devices connected to the cloud. To use the Data API in an Android wearable app, we must first install the Xamarin.GooglePlayServices.Wearable NuGet, which will install the prerequisites.
By storing the data on the cloud, our app can preserve its data when the user gets new devices, as well as provide a persistent storage option for the app data. Another benefit of cloud storage is that if the wearable loses its connectivity temporarily, the wearable can retrieve the data from the cloud when it reconnects.
To communicate between devices, we need to set up the Google API client. This client manages the connections and events, providing a simple, listener-based notification system. In addition to the Data API events, we are notified of connection events, such as when the connection is established or if it goes down temporarily.
The Data API has an event to notify listeners when new data is synchronized or changed on one of the devices. Since we want to know when the handheld or wearable changes the data, we must implement the IDataApiDataListener
interface. It is not necessary to create a new object for this, and therefore, the current activity can be used.
The IDataApiDataListener
interface has a single method—OnDataChanged()
. This method is invoked as soon as the client is informed about another device changing the data. If the wearable is connected via Bluetooth, then the data and events come through the Bluetooth stack. Other devices receive events through the cloud, possibly over Wi-Fi.
Before we can transfer data, we need to connect to the Google Play services. To create the client, we instantiate a new GoogleApiClientBuilder
instance.
Because we care about the connection status, we need to pass two delegates to the AddConnectionCallbacks()
method. The first method is invoked when the client connection succeeds. Here, we make sure to attach our IDataApiDataListener
interface instance to the Data API. The second delegate is invoked when the connection is suspended. In this delegate, we make sure to detach our interface instance so that no memory is leaked.
The other connection event method is the AddOnConnectionFailedListener()
method. This method allows us to attach a delegate that will be invoked if the connection fails.
Finally, before we create the client instance, we specify that we are going to be using the wearable APIs for communication with the wearable. We do this by passing the value of the API
property of the WearableClass
to the AddApi()
method. The WearableClass
type contains various properties that are used to access various APIs used for communication with wearables. The API
property contains a token used by the client to set up the wearable APIs.
After a client has been connected, we can use the DataApi
property of the WearableClass
type to access the data APIs for wearables. The DataApi
property returns an instance of the IDataApi
interface, which we can use to attach and detach the listeners relating to the wearable Data API.
The IDataApi
interface has several methods, but we only need the AddListener()
and RemoveListener()
methods. We pass the current client and the listener
instance to each of these methods, making sure to attach the listener when the client connects, and detaching it when the connection is suspended.
We invoke the Connect()
method of the client in OnResume
, as we want to connect only when our activity is in the foreground. Conversely, we invoke the Disconnect()
method when we leave the foreground or when the OnPause()
method is invoked. We should detach the listener before we disconnect so that we do not leak any memory.
Once we are connected, we can start sending data to the service. The service will then process and synchronize the data to various devices and the cloud. The easiest way to manage the data is using the DataMap
type. A DataMap
instance is stored at a specific path and consists of key-value pairs. There are various put
and get
methods, such as PutString
and GetString
, which are used to store and retrieve data.
Even though we are using the DataMap
type to store our data, the Data API expects the DataItem
types. When storing and retrieving data, the DataMap
type is serialized and deserialized into a DataItem
type, which stores the DataMap
type as a byte array.
When we want to store a new data map, we create an instance of PutDataMapRequest
using the static Create()
method. The PutDataMapRequest
instance is a container type for a DataMap
instance and the path to the data map. To access or update the data map that will be stored, we use the DataMap
property.
The Create()
method requires that we specify the path where we are going to store the data map. The path is similar to a Unix-based, file-system path. It uses forward slashes and must begin with a forward slash. Although similar to a file system, it is not actually stored at this location on the local file system.
Before we can synchronize our data map, we must obtain the data item from our PutDataMapRequest
using the AsPutDataRequest()
method. This method returns another container object, a PutDataRequest
instance, which now contains the serialized byte array that was once a DataMap
type.
We can now pass the new PutDataRequest
instance to the PutDataItemAsync()
method of the Data API, along with an open API client. We can verify that the operation was successful using the result of this method, which returns an IDataApiDataItemResult
instance. This type contains both the data item that was saved and the status of the operation. The data item is obtained through the DataItem
property and the status through the Status
property.
After the data is passed to the Data API, it will be passed on to any attached wearables or other devices. Because we attached an instance of IDataApiDataListener
to the Data API, the OnDataChanged()
method of this listener will be invoked. The data has been received on another thread, so we have to make sure that when we update the UI, we do so on the UI thread. We can easily do this by using the RunOnUiThread()
method.
The OnDataChanged()
method has one argument—a DataEventBuffer
instance. This type inherits from IEnumerable<IDataEvent>
with an additional Status
property. We can iterate through the IDataEvent
items and process them accordingly by taking advantage of LINQ.
Each IDataEvent
item has a Type
property, which is either DataEvent.TypeChanged
or DataEvent.TypeDeleted
. Since we only process data that has not been deleted, we will first check the Type
property. Another property that is used to identify the data is the Uri
property, which contains the path that was specified when the data was created. If there are multiple sources of data, which might be the case in some apps, we must first verify that it is the data we are expecting.
Once we have determined that we are going to process the item, we can read the associated data using the DataItem
property. This property has a type of IDataItem
, which contains the serialized DataMap
instance.
As a result of using a DataMap
type to synchronize data, we have to convert the IDataItem
type back into DataMap
. This is done using the DataMapItem
type. This type has a static FromDataItem()
method, which accepts an IDataItem
instance and returns a DataMapItem
instance. This type also has a Uri
property that we could read, but, as we have already read the value from the IDataItem
type, we can move on to the DataMap
property.
The DataMap
property returns the deserialized DataMap
instance that was originally passed to the Data API from another device. We can now read the data map using the various get
methods, such as GetString()
or GetBoolean()
. Each get
method has two overloads: one that returns a system default, if no value is found for the specified key; and another that returns a provided default.
When our app starts, we want to be able to read any previously synchronized data. Instead of waiting for the data to be sent over the Data API, we can request the current data that uses the GetDataItemsAsync()
method of the wearable Data API. The return type of this method is DataItemBuffer
, which inherits from IEnumerable<IDataItem>
with an additional Status
property. We can use the Status
property to determine if the request was successful, and then iterate through the result. Similar to items obtained from a DataEventBuffer
instance, we read the Uri
property and convert each item into a DataMap
property. Then, we process the data map accordingly.
In any case where we don't want to synchronize the data, but rather send it directly to another device, we can use the Message API instead of the Data API.
IMessageApiMessageListener
interface on the activity:public class MainActivity : Activity, IMessageApiMessageListener { public void OnMessageReceived(IMessageEvent messageEvent) { } }
listener
instance to the Message API:WearableClass.MessageApi.AddListenerAsync(client, this);
GetConnectedNodesAsync()
method to fetch all the connected nodes, making sure that the node is available for communication using the IsNearby
property:await WearableClass.NodeApi.GetConnectedNodesAsync(client); var node = result.Nodes.FirstOrDefault(n => n.IsNearby);
SendMessageAsync()
method:var bytes = BitConverter.GetBytes(count); await WearableClass.MessageApi.SendMessageAsync( client, node.Id, CounterPath, bytes);
OnMessageReceived()
method will be invoked, and we can read the data using the GetData()
method:public void OnMessageReceived(IMessageEvent messageEvent) { var bytes = messageEvent.GetData(); if (bytes != null && bytes.Length> 0) { count = BitConverter.ToInt32(bytes, 0); } }