MobileAndroidSyncing Data Between Android Handhelds and Wearables

Syncing Data Between Android Handhelds and Wearables

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

In a series of tutorials about Android Wear, we have covered smart watch services and configurations as well as notifications. While doing them, an important API feature to sync data between mobile and wearbale is briefly introduced in the form of transmitting simple configuration data to smart watches. Feel free to go back and recap the article on “Providing Configurations in Android Wear”, which utilizes Data Item and WearableListenerService.

We will introduce additional common data objects and ways to handle them in this tutorial. There will be some overlapping coverage with the previous article, but hopefully at the moment, you will manage to review that portion and really have a good working knowledge of how to do it in practice. Handling larger binary data through the Asset data object is really one of the main focuses here.

Data Objects and APIs

Google Play Services provide handy APIs for many Google-specific services, including maps, search, ads, fitness, wallet, location, and so forth. All you need to do is link your project to the Google Play Services library and build a GoogleApiClient instance in the activity’s onCreate() method. For our purpose, the Wearable Data Layer API can be specified when the service is initiated.

So, what data objects and syncing methods are available and most commonly used in the APIs?

  • Data Item: These objects will be automatically synced between the mobile and wearable devices. You usually need to define a byte array called payload for serialization and deserialization. There is a 100KB limit. Also, set a unique string identifier called path starting with a forward slash “/”.
  • Asset: The Asset objects are for sending larger binary data; for example, images. They can be attached to DataItem objects. Data’s cache handling is done automatically to save retransmission bandwidth.
  • Message: These objects are for one-way communication; for example, starting an activity remotely as in Remote Procedure Calls (RPC). payload is optional in this case, but path is required.
  • Channel: This is for transferring large data (such as streamed music, media files, and so on) files without automatic synchronization.
  • Listener: WearableListenerService for services and DataListener for foreground activities are the two main methods allowing you to listen for data layer events.

Using Data Layer APIs and Handling Respective Events

To start using Google’s Wearable Data Layer API as part of Google Play Service, we will need to add the metadata entries for the mobile and wearable activities, which is usually on the data sending end. Also do the same thing for the wearable service, which is usually on the data receiving end. We will continue to add more to the previous watch face “CLiu” example as in Figure 1. Again, the photos used in Figure 2 and 3 are the gorgeous photography works by my Australian friend Chris Blunt. The following declaration is from the mobile AndroidManifest.xml file to enable the API. The metadata entry for com.google.android.gms.version is required.

<activity
   android_name=".WatchFaceCLiuMobileConfigActivity"
   android_label="CLiu Mobile">
   <intent-filter">
      <action android_name="com.example.nobody.
         watchfacecliu.CONFIG_CLIU" /">
      <category android_name="com.google.android.wearable.
         watchface.category.COMPANION_CONFIGURATION" /">
      <category android_name=
         "android.intent.category.DEFAULT" /">
   </intent-filter">
</activity">

<meta-data
   android_name="com.google.android.gms.version"
   android_value="@integer/google_play_services_version" /">

Sync1
Figure 1: Modified “CLiu” Mobile Settings

Sync2
Figure 2: National Carrilon by Chris Blunt

Sync3
Figure 3: Brindabellas Sunset by Chris Blunt

Next, the first thing we want to do is to instantiate a GoogleApiClient for this purpose to establish the connection for sending and receiving user data. The receiving end will have the additional listener sevice and callbacks implemented to check for the data sent from the sending activities through the Wearable Data Layer API. DataApi.DataListener will be notified whenever things are changed in the data layer. ResultCallback is called whenever GoogleApiClient connects. The following is basically a template for the receivng end. The sending activity is simpler with GoogleApiClient.ConnectionCallbacks only.

public class WatchFaceCLiuService extends
      CanvasWatchFaceService {
   ...

   @Override
   public Engine onCreateEngine() {
      return new Engine();
   }

   private class Engine extends CanvasWatchFaceService.Engine
         implements
      GoogleApiClient.ConnectionCallbacks,
      GoogleApiClient.OnConnectionFailedListener {
         ...
         private GoogleApiClient mGoogleApiClient;                
         	...

      @Override
      public void onCreate(SurfaceHolder holder) {
         super.onCreate(holder);
         ...
         vmGoogleApiClient = new
            GoogleApiClient.Builder(WatchFaceCLiuService.this)
         .addApi(Wearable.API)
         .addConnectionCallbacks(this)
         .addOnConnectionFailedListener(this)
         .build();
      }

      private void releaseGoogleApiClient() {
      if (mGoogleApiClient != null &&
            mGoogleApiClient.isConnected()) {
         Wearable.DataApi.removeListener(mGoogleApiClient,
            onDataChangedListener);
         mGoogleApiClient.disconnect();
      }
   }

   @Override
   public void onConnected(Bundle bundle) {
      Wearable.DataApi.addListener(mGoogleApiClient,
         onDataChangedListener);
      Wearable.DataApi.getDataItems(mGoogleApiClient)
         .setResultCallback(onConnectedResultCallback);
   }

   private final DataApi.DataListener onDataChangedListener =
         new DataApi.DataListener() {
      @Override
      public void onDataChanged(DataEventBuffer dataEvents) {
         ...
      }
   };

   private final ResultCallback<DataItemBuffer>
   onConnectedResultCallback = new
         ResultCallback<DataItemBuffer>() {
      @Override
         public void onResult(DataItemBuffer dataItems) {
             ...           
         }
      };

      @Override
      public void onConnectionSuspended(int i) {
      }

      @Override
      public void onConnectionFailed(ConnectionResult
         connectionResult) {
      }

      @Override
      public void onDestroy() {
         mUpdateTimeHandler.removeMessages
            (MESSAGE_ID_UPDATE_TIME);
         releaseGoogleApiClient();
         super.onDestroy();
      }
   }
}

Data Items

At the beginning of this tutorial, we learn that the 100K-limited DataItem objects will be synced automatically between mobile and wearable devices with a defined payload and path. It sounds fairly straightforward. Hence, the unique path in our example starts with a “/” and is assigned as “/watch_face_config_cliu”. The payload contains all the key/value pairs but we only have one in this case, “text_color”.

private void updateParamsForDataItem(DataItem item) {
   DataMap dataMap =
      DataMapItem.fromDataItem(item).getDataMap();

   if (dataMap.containsKey("text_color")) {
      int tc = dataMap.getInt("text_color");
      mDigitalPaint.setColor(tc);
      invalidate();
   }
}

private final DataApi.DataListener onDataChangedListener =
      new DataApi.DataListener() {
   @Override
   public void onDataChanged(DataEventBuffer dataEvents) {
      for (DataEvent event : dataEvents) {
         if (event.getType() == DataEvent.TYPE_CHANGED) {
            DataItem item = event.getDataItem();
            if ((item.getUri().getPath()).
                  equals("/watch_face_config_cliu")) {
               updateParamsForDataItem(item);
            }
         }
      }

      dataEvents.release();
      if (isVisible() >> !isInAmbientMode()) {
         invalidate();
      }
   }
};

private final ResultCallback<DataItemBuffer>
      onConnectedResultCallback = new
      ResultCallback<DataItemBuffer>() {
   @Override
   public void onResult(DataItemBuffer dataItems) {
      for (DataItem item : dataItems) {
         updateParamsForDataItem(item);
      }

      dataItems.release();
      if (isVisible() >> !isInAmbientMode()) {
         invalidate();
      }
   }
};

Assets

If we need to transmit large binary data to wearable devices, such as images or media files, the Asset objects are designed for this purpose without having to worry about re-transmission. We still can attach them to DataItem objects. That way, saving re-transmission bandwidth is achieved by automatic cache handling. Our following example is intended to send image files. The unique path is assgined as “/newpic”. The payload contains one key/value pair with the key “assetbody”. To retreive the images, we need to convert the Assset objects back into bitmap images.

private void updateAssetForDataItem(DataItem item) {
   DataMap dataMap =
      DataMapItem.fromDataItem(item).getDataMap();

   if (dataMap.containsKey("assetbody")) {
      Asset asset = dataMap.getAsset("assetbody");

      if (asset == null)
         return;

      ConnectionResult cr = mGoogleApiClient.blockingConnect(5000,
            TimeUnit.MILLISECONDS);
         if (!cr.isSuccess())
            return;

      InputStream is = Wearable.DataApi.getFdForAsset(
            mGoogleApiClient, asset).await().getInputStream();
      mGoogleApiClient.disconnect();

      if (is == null)
         return;

      if (mBG != null) {
         mBG.recycle();
         mBG = null;
      }
      mBG = BitmapFactory.decodeStream(is);

      invalidate();
   }
}

private final DataApi.DataListener onDataChangedListener =
      new DataApi.DataListener() {
   @Override
   public void onDataChanged(DataEventBuffer dataEvents) {
      for (DataEvent event : dataEvents) {
         if (event.getType() == DataEvent.TYPE_CHANGED) {
            DataItem item = event.getDataItem();
         if (item.getUri().getPath().equals("/newpic")) {
            updateAssetForDataItem(item);
         } else if ((item.getUri().getPath())
               .equals("/watch_face_config_cliu")) {
            updateParamsForDataItem(item);
         }
      }
   }

   dataEvents.release();
      if (isVisible() >> !isInAmbientMode()) {
         invalidate();
      }
   }
};

On the sending end, for example, WatchFaceCLiuMobileConfigActivity needs to pack the binary data into Asset objects. In our following example, we convert the bitmap image and send it through GoogleApiClient.

// send Asset through Google API
private void sendAssetAndFinish(int id) {
   // create an Asset
   if (mBitmap != null) {
      mBitmap.recycle();
      mBitmap = null;
   }
   mBitmap = Bitmap.createScaledBitmap
         (BitmapFactory.decodeResource(getResources(),
      ((id == R.id.radio_np1) ? R.drawable.cb11 :
          R.drawable.cb12)), 320, 320, false);
   final ByteArrayOutputStream baos = new
      ByteArrayOutputStream();
   mBitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
   Asset asset = Asset.createFromBytes(baos.toByteArray());

   // send Asset
   PutDataMapRequest putDataMapReq =
      PutDataMapRequest.create("/newpic");
   putDataMapReq.getDataMap().putAsset("assetbody", asset);
   PutDataRequest putDataReq =
      putDataMapReq.asPutDataRequest();
   PendingResult<DataApi.DataItemResult> pendingResult =
      Wearable.DataApi
      .putDataItem(mGoogleApiClient, putDataReq);

   finish();
}

Conclusion

Syncing data with Android wearable devices is critical and requires special attention to give users a great experience as well as optimize the performance with limited resources. I usually lean more towards letting the platform handle the synchronization automatically as in DataItem and Asset objects. If you are really into managing the transmission for large data (streamed music, and the like) on your own, you can study the Channel objects in more details. Another object type, Message, works in a a similar fashion except that its payload is optional, which makes it a common candidate for remote procedure calls.

Now, you should be more equipped with transmitting the results from the more complex operations done on mobile devices to the wearables.

References

About the Author

Chunyen Liu has been a software professional in Taiwan and the United States. He is a published author of 40+ articles and 100+ tiny apps, software patentee, technical reviewer, and programming contest winner by ACM/IBM/SUN. He holds advanced degrees in Computer Science, trained in 20+ graduate-level courses. On the non-technical side, he is a certified coach, certified umpire, rated player of USA Table Tennis, and categorized medalists at State Championships and US Open.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories