Syncing Data Between Wearable and Handheld Devices Using Xamarin in Android

Introduction

My previous article explained how to set up your development environment to get started with Android development using Xamarin and Visual Studio and also provided a brief introduction about wearables. This shows how to sync data in your Android application.

Creating the Wear App Project

To get started, let's fire up Visual Studio 2013 and select "File" -> "New" -> "Project...". Under Templates select C# > Android, select Wear App (Android) Project. You should be able to see the following:



Name your app to whatever you like and then click OK to let Visual Studio generate the necessary files for you. In this example I named it "WearDemo". The image below shows the generated files with default sample codes to help you get started on building wear apps.



Before we start modifying the default code I'd like to point out that there are two ways to communicate between wearable and handheld devices and these are the DataApi and the MessageApi. The following are the short descriptions of each API.

DataApi exposes an API for components to read or write data items and assets. A DataItem provides data storage with automatic syncing between the handheld and wearable. An asset is used for sending BLOBs of data such as images. You attach assets to DataItems and the system automatically takes care of the transfer for you.

MessageApi exposes an API for components to send messages to other nodes. Messages should generally contain small payloads. You should use Assests with DataApi to store larger data.

In this particular demo, I'm going to use the DataApi to send/sync data between devices. Since DataApi is part of Google Play Services, then the first thing we need here is to add the following namespaces below:

  1. using Android.Gms.Common.Apis;  
  2. using Android.Gms.Wearable;  
Android.Gms.Common.Apis allows us to use GoogleApiClient that is the main entry point for Google Play Services integration. The Android.Gms.Wearable enables us to use the WearableClass. Next is to extend our MainActivity class to inherit the following interfaces:
  • IDataApiDataListener.
  • IGoogleApiClientConnectionCallbacks IGoogleApiClientOnConnectionFailedListener.
IDataApiDataListener is used to receive data events. The IGoogleApiClientConnectionCallbacks provides callbacks that are called when the client is connected or disconnected from the service. IGoogleApiClientOnConnectionFailedListener Provide callbacks for scenarios that result in a failed attempt to connect the client to the service.

Wrapping everything up, here's the sample code for sending data to the handheld device:
  1. using System;  
  2. using Android.Runtime;  
  3. using Android.Widget;  
  4. using Android.OS;  
  5. using Android.Support.Wearable.Views;  
  6. using Java.Interop;  
  7. using Android.Gms.Common.Apis;  
  8. using Android.Gms.Wearable;  
  9. using System.Linq;  
  10.   
  11. namespace WearDemo  
  12. {  
  13.     [Activity(Label = "WearDemo", MainLauncher = true, Icon = "@drawable/icon")]  
  14.     public class MainActivity : Activity,IDataApiDataListener, IGoogleApiClientConnectionCallbacks, IGoogleApiClientOnConnectionFailedListener  
  15.     {  
  16.   
  17.         private IGoogleApiClient _client;  
  18.         const string _syncPath = "/WearDemo/Data";  
  19.         protected override void OnCreate(Bundle bundle) {  
  20.             base.OnCreate(bundle);  
  21.   
  22.             _client = new GoogleApiClientBuilder(thisthisthis)  
  23.                              .AddApi(WearableClass.Api)  
  24.                              .Build();  
  25.   
  26.             // Set our view from the "main" layout resource  
  27.             SetContentView(Resource.Layout.Main);  
  28.             var v = FindViewById<WatchViewStub>(Resource.Id.watch_view_stub);  
  29.             v.LayoutInflated += delegate {  
  30.   
  31.                 // Get our button from the layout resource,  
  32.                 // and attach an event to it  
  33.                 Button button = FindViewById<Button>(Resource.Id.myButton);  
  34.   
  35.                 button.Click += delegate {  
  36.                     SendData();  
  37.                 };  
  38.             };  
  39.         }  
  40.   
  41.         public void SendData() {  
  42.             try {  
  43.                 var request = PutDataMapRequest.Create(_syncPath);  
  44.                 var map = request.DataMap;  
  45.                 map.PutString("Message""Vinz says Hello from Wearable!");  
  46.                 map.PutLong("UpdatedAt", DateTime.UtcNow.Ticks);  
  47.                 WearableClass.DataApi.PutDataItem(_client, request.AsPutDataRequest());  
  48.             }  
  49.             finally {  
  50.                 _client.Disconnect();  
  51.             }  
  52.   
  53.         }  
  54.         protected override void OnStart() {  
  55.             base.OnStart();  
  56.             _client.Connect();  
  57.         }  
  58.         public void OnConnected(Bundle p0) {  
  59.             WearableClass.DataApi.AddListener(_client, this);  
  60.         }  
  61.   
  62.         public void OnConnectionSuspended(int reason) {  
  63.             Android.Util.Log.Error("GMSonnection suspended " + reason);  
  64.             WearableClass.DataApi.RemoveListener(_client, this);  
  65.         }  
  66.   
  67.         public void OnConnectionFailed(Android.Gms.Common.ConnectionResult result) {  
  68.             Android.Util.Log.Error("GMSonnection failed " + result.ErrorCode);  
  69.         }  
  70.   
  71.         protected override void OnStop() {  
  72.             base.OnStop();  
  73.             _client.Disconnect();  
  74.         }  
  75.   
  76.         public void OnDataChanged(DataEventBuffer dataEvents) {  
  77.             var dataEvent = Enumerable.Range(0, dataEvents.Count)  
  78.                                       .Select(i => dataEvents.Get(i).JavaCast<IDataEvent)  
  79.                                       .FirstOrDefault(x => x.Type == DataEvent.TypeChanged && x.DataItem.Uri.Path.Equals(_syncPath));  
  80.             if (dataEvent == null)  
  81.                 return;  
  82.   
  83.             //do stuffs here  
  84.         }  
  85.     }  
  86. }  
Okay, I'll try to explain what happened in the code. At the OnCreate event we build a Google Play Services client that includes the Wearable API. We then called the SendData() method at the Button's click handler to send the data. The SendData() method contains the actual logic for sending the data. There we created a DataMapRequest by passing the path of the data object that is "/WearDemo/Data". The actual data is the DataMap that cotains a Message and UpdatedAt values. The receiving side can use the path to identify the origin of the data that I'll talk about later in this article.

The Events

  • OnStart connects to the data layer when the activity starts. 
  • OnConnected  triggers when the data layer connection is successful. 
  • OnStop  disconnects from the data layer when the activity stops OnConnectionSuspended and OnConnectionFailed is where you do stuff for the required connection callbacks (for example in this demo we log errors and detach the service). 
  • OnDataChanged  triggers when the data changes.
Things to Keep in Mind
  • The path should always start with a forward-slash (/). 
  • Timestamps is a must when sending data because the OnDataChanged() event is only called when the data really changes. Adding the Timestamp to the data will ensure that the method is called.
Add this MetaData in the AndroidManifest.xml under <application> element:
  1. <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />  
Creating the Main App Project

To test the syncing and sending of the data we need to create the main Android app that will receive the data object coming from the wearable. The main app will be installed in the handheld device (for example mobile or tablet).

Now right-click on the solution project and select ADD > NEW PROJECT. In the Add New Project window select Visual C# > Android > Blank App (Android). You should be able to see like this:



I named the project "MainAppDemo" for simplicity. Just click OK to generate the necessary files for you. You should have something like this in your solution now.



Before we start adding the logic to the main app, I'd like to highlight the following.

The Namespace of your Wear app and Main app should be the same. In this example the Wear app uses the namespace "WearDemo". So be sure to rename the namespace of your main app to "WearDemo" to match up. To change the default namespace you can follow these steps.
  • Go to PROJECT > PROPERTIES > DEFAULT NAMESPACE.
     
  • To change the rest you can use CTRL + H and replace the default namespace to "WearDemo".
     
  • You can also use the refactor code to change the namespace. To do this just simply right-click on the namespace and select REFACTOR > RENAME.
The package name of your Wear and Main app should also be the same. You can find the package name by right-clicking on the project and select PROPERTIES > ANDROID MANIFEST as shown in the image below:



If you are following this example then make sure that both package names are set to "WearDemo.WearDemo". Be sure to build both projects to see if it builds successfully. Once you've done that then let's go ahead and start modifying the project. First change the value of "Compile using Android version" to "API Level 21 (Xamarin.Android v5.0 Support). See the image below:



Under references check if you have Xamarin.Android.Support.V4. If you don't have that then just right-click on the References and select MANAGE NUGET PACKAGES. Under Online > Nuget.Org search "Xamarin.Android.Support.V4". You should be able to see something like this:



Just click install and wait until it is done. Now do the same procedure and install "Xamarin.Android.Wear -Version 1.0.0".

Adding the WearableListenerService

Extending the WearableListenerService lets you listen for any updates in the data layer. The system manages the lifecycle of the service, binding to the service when it needs to send data items or messages and unbinding the service when no work is needed.

Having that statement we will use the WearableListenerService to listen for an update from the data layer and handle the data. So the next step is to add a class that extends WearableListenerService. To do this right-click on the project root and select ADD > Class and name it "WearService". Here's the entire logic of the class.
  1. using System.Linq;  
  2. using Android.App;  
  3. using Android.Content;  
  4. using Android.Runtime;  
  5. using Android.Gms.Wearable;  
  6. using Android.Gms.Common.Apis;  
  7. using Android.Support.V4.Content;  
  8.   
  9. namespace WearDemo  
  10. {  
  11.     [Service]  
  12.     [IntentFilter(new[] { "com.google.android.gms.wearable.BIND_LISTENER" })]  
  13.     public class WearService : WearableListenerService  
  14.     {  
  15.         const string _syncPath = "/WearDemo/Data";  
  16.         IGoogleApiClient _client;  
  17.   
  18.         public override void OnCreate() {  
  19.             base.OnCreate();  
  20.             _client = new GoogleApiClientBuilder(this.ApplicationContext)  
  21.                     .AddApi(WearableClass.Api)  
  22.                     .Build();  
  23.   
  24.             _client.Connect();  
  25.   
  26.             Android.Util.Log.Info("WearIntegrationreated");  
  27.         }  
  28.   
  29.         public override void OnDataChanged(DataEventBuffer dataEvents) {  
  30.             var dataEvent = Enumerable.Range(0, dataEvents.Count)  
  31.                                       .Select(i => dataEvents.Get(i).JavaCast<IDataEvent)  
  32.                                       .FirstOrDefault(x => x.Type == DataEvent.TypeChanged && x.DataItem.Uri.Path.Equals(_syncPath));  
  33.             if (dataEvent == null)  
  34.                 return;  
  35.   
  36.             //get data from wearable  
  37.             var dataMapItem = DataMapItem.FromDataItem(dataEvent.DataItem);  
  38.             var map = dataMapItem.DataMap;  
  39.             string message = dataMapItem.DataMap.GetString("Message");  
  40.   
  41.             Intent intent = new Intent();  
  42.             intent.SetAction(Intent.ActionSend);  
  43.             intent.PutExtra("WearMessage", message);  
  44.             LocalBroadcastManager.GetInstance(this).SendBroadcast(intent);  
  45.         }  
  46.     }  
  47. }  
The code above implements OnDataChanged event that filters incoming data events for those of "TypeChanged", checks for the data object path of "/WearDemo/Data", then broadcasts the data locally.

The Main Activity

Here's the code block for our main activity class.
  1. using Android.App;  
  2. using Android.Content;  
  3. using Android.Widget;  
  4. using Android.OS;  
  5. using Android.Support.V4.Content;  
  6.   
  7. namespace WearDemo  
  8. {  
  9.     [Activity(Label = "MainAppDemo", MainLauncher = true, Icon = "@drawable/icon")]  
  10.     public class MainActivity : Activity  
  11.     {  
  12.         TextView _txtMsg;  
  13.   
  14.         protected override void OnCreate(Bundle bundle) {  
  15.             base.OnCreate(bundle);  
  16.   
  17.             // Set our view from the "main" layout resource  
  18.             SetContentView(Resource.Layout.Main);  
  19.   
  20.             // Get our TextBox from the layout resource,  
  21.             _txtMsg = FindViewById<TextView>(Resource.Id.txtMessage);  
  22.   
  23.   
  24.             IntentFilter filter = new IntentFilter(Intent.ActionSend);  
  25.             MessageReciever receiver = new MessageReciever(this);  
  26.             LocalBroadcastManager.GetInstance(this).RegisterReceiver(receiver, filter);  
  27.         }  
  28.   
  29.         public void ProcessMessage(Intent intent) {  
  30.             _txtMsg.Text = intent.GetStringExtra("WearMessage");  
  31.         }  
  32.   
  33.         internal class MessageReciever : BroadcastReceiver  
  34.         {  
  35.             MainActivity _main;  
  36.             public MessageReciever(MainActivity owner) { this._main = owner; }  
  37.             public override void OnReceive(Context context, Intent intent) {  
  38.                 _main.ProcessMessage(intent);  
  39.             }  
  40.         }  
  41.     }  
  42. }  
In the code above, we register to receive broadcasts from the ListenerService in the OnCreate() event and then define a nested class that extends the BroadcastReceiver, implements the OnReceive() method and extract the data. The Process() method handles the displaying of the data to the UI.

The Main layout

Change your Main.xaml to this:
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:orientation="vertical"  
  4.     android:layout_width="fill_parent"  
  5.     android:layout_height="fill_parent">  
  6.     <TextView  
  7.         android:id="@+id/txtMessage"  
  8.         android:layout_width="fill_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:layout_marginTop="50dp"  
  11.         android:gravity="center"  
  12.         android:textColor="@android:color/white"  
  13.         android:textSize="80sp" />  
  14. </LinearLayout>  
Adding the MetaData for Google Play Services

And finally, add the Meta data in the AndroidManifest.xml under the <application> element:
  1. <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />  
That's it. In my next article I'm going to show how to deploy each application in various devices and test the result. I hope someone finds this article useful.