This is part 3 of a 4 part series where we explore the challenges of making wearable apps, by building an Android wear watch face with real-time weather data.In Part 1, we provided an overview and created our first watch face. In Part 2 we took a deeper dive into the watch face. In this part we explore the Wearable Communication APIs and build the real-time weather communication component of our app. Finally, in Part 4, we show how to package and publish a watch face app.

You can review the full source code on Github or download the final watch face from the Google Play Store.

This tutorial is separated to four posts:

Wearable APIs

A key thing to note about wearable devices is that they do not connect directly to the internet. A mobile phone or device must be paired with a wearable. Furthermore, once paired, they can only transfer data only via the Wearable APIs.
Edit: This may be changing soon with wifi available directly on wearable devices see google announcement.
There are three wearable APIs:
  1. Node API : for detecting devices connect or disconnect.
  2. Message API : for sending messages to another device.
  3. Data API: for storing and retrieving data from a device.
As with other Google APIs, we need to use the GoogleApiClient class in order to use Wearable APIs. Figure 1 shows how wearable & mobile apps work with the GoogleApiClient and the wearable APIs. 

Figure 1: Communicating with Wearable APIs

Google Api Client

The first step is to create an instance of GoogleApiClient.
@Overridepublic void onCreate(SurfaceHolder holder) {    mGoogleApiClient = new GoogleApiClient.Builder(WeatherWatchFaceService.this)            .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {                @Override                public void onConnected(Bundle bundle) {                    Log.d(TAG, "GoogleApiClient is Connected");                }                @Override                public void onConnectionSuspended(int i) {                    Log.d(TAG, "GoogleApiClient is ConnectionSuspended");                }})            .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {                @Override                public void onConnectionFailed(ConnectionResult connectionResult) {                    Log.d(TAG, "The connection of GoogleApiClient is failed");                }            })           .addApi(Wearable.API) // tell Google API that we want to use Warable API           .build();}
TIP: If your app also use one of the other API components (such as Google+, Games, or Drive), remember to use a separate instance of GoogleApiClient for the Wearable API, as the connect call will fail unless the Android Wear App has also been installed on the device. For more information, see here.
Once the GoogleApiClient is connected, add listener for each of the Wearable APIs:
@Overridepublic void onConnected(Bundle bundle) { // the part of GoogleApiClient.ConnectionCallbacks    Wearable.NodeApi.addListener(mGoogleApiClient, new NodeApi.NodeListener() {        @Override        public void onPeerConnected(Node node) {            Log.d(TAG, "A node is connected and its id: " + node.getId());        }                                        @Override        public void onPeerDisconnected(Node node) {            Log.d(TAG, "A node is disconnected and its id: " + node.getId());        }    });        Wearable.MessageApi.addListener(mGoogleApiClient, new MessageApi.MessageListener() {        @Override        public void onMessageReceived(MessageEvent messageEvent) {            Log.d(TAG, "You have a message from " + messageEvent.getPath());        }    });    Wearable.DataApi.addListener(mGoogleApiClient, new DataApi.DataListener() {        @Override        public void onDataChanged(DataEventBuffer dataEvents) {            Log.d(TAG, "Your data is changed");        }    });}
Also, remember to remove listeners and disconnect GoogleApiClient if they are no longer needed.
@Overridepublic void onVisibilityChanged(boolean visible) {    if (visible) {        mGoogleApiClient.connect();    } else {        if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {            Wearable.NodeApi.removeListener(mGoogleApiClient, this);            Wearable.MessageApi.removeListener(mGoogleApiClient, this);            Wearable.DataApi.removeListener(mGoogleApiClient, this);            mGoogleApiClient.disconnect();        }    }}
In our example, we connect the Wearable APIs when screen is visible and disconnect from the APIs when screen is invisible.

Node Api

NodeApi has two methods you can use:
  • getLocalNode: Get device’s node id. You will need this id to retrieve data via DataApi
  • getConnectedNodes: Get what devices are connected to this device.
For example,
Wearable.NodeApi.getLocalNode(mGoogleApiClient)        .setResultCallback(new ResultCallback<NodeApi.GetLocalNodeResult>() {            @Override            public void onResult(NodeApi.GetLocalNodeResult result) {                Log.d(TAG, "My node id is " + result.getNode().getId());            }        });Wearable.NodeApi.getConnectedNodes(mGoogleApiClient)        .setResultCallback(new ResultCallback<NodeApi.GetConnectedNodesResult>() {            @Override            public void onResult(NodeApi.GetConnectedNodesResult result) {                for(Node node: result.getNodes()){                    Log.d(TAG, "Node " + node.getId() + " is connected");                }            }        });

Message Api

MessageApi is similar to a Message Queue, you can send messages into a queue and GoogleApiClient will deliver the messages to the other device, but unlike some other message queue technologies, there is no persistence. A active connection is needed between both the wearable and the phone. If the connections is lost, GoogleApiClient doesn’t keep the messages.The following example shows how to send and receive a message containing weather data via Wearable.MessageApi.
// SEND a message// we can use data map to generate a byte array.DataMap config = new DataMap();//Weather Informationconfig.putInt("Temperature", 100);config.putString("Condition", "cloudy");// the parameter of  node id can be empty string "".// the third parameter is message path.Wearable.MessageApi.sendMessage(mGoogleApiClient, mPeerId, "/WeatherInfo", config.toByteArray())        .setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {            @Override            public void onResult(MessageApi.SendMessageResult sendMessageResult) {                Log.d(TAG, "SendMessageStatus: " + sendMessageResult.getStatus());            }        });// RECIVE a message@Overridepublic void onMessageReceived(MessageEvent messageEvent) {    // convert a byte array to DataMap    byte[] rawData = messageEvent.getData();    DataMap dataMap = DataMap.fromByteArray(rawData);        // we have different methods for different messages    if (messageEvent.getPath().equals("/WeatherInfo")) {        fetchInfo(dataMap);    }    if (messageEvent.getPath().equals("/WeatherWatchFace/Config")) {        fetchConfig(dataMap);            }}
Note: The limit of a message is 100 KB.

Data Api

DataApi allows us to store the wears’ data persistently. We use a PutDataMapRequest to save persistent data.
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create("/WeatherWatchFace/Config");DataMap config = putDataMapRequest.getDataMap();config.putInt("TemperatureScale", mTemperatureScale);config.putInt("BackgroundColor", mBackgroundColor);config.putInt("RequireInterval", mRequireInterval);Wearable.DataApi.putDataItem(mGoogleApiClient, putDataMapRequest.asPutDataRequest())        .setResultCallback(new ResultCallback<DataApi.DataItemResult>() {            @Override            public void onResult(DataApi.DataItemResult dataItemResult) {                log("SaveConfig: " + dataItemResult.getStatus() + ", " + dataItemResult.getDataItem().getUri());            }        });
WARNING: When you update data via PutDataMapRequest, be sure to include all the attributes as update replaces all items with the new map, instead of updating individual properties.
When the data has been saved successfully, the path to access it again will look like the following
wear://<NodeId>/<path>    //the path is given when you call PutDataMapRequest.create()
You can’t assign the node Id. DataApi uses the local node id when you call Wearable.DataApi.putDataItem() either on a wear or handheld. If you want to access the data, you have to use this path to access it:
// to get local node idWearable.NodeApi.getLocalNode(mGoogleApiClient).setResultCallback(new ResultCallback<NodeApi.GetLocalNodeResult>() {    @Override    public void onResult(NodeApi.GetLocalNodeResult getLocalNodeResult) {        Uri uri = new Uri.Builder()                .scheme("wear")                .path("/WeatherWatchFace/Config")                .authority(getLocalNodeResult.getNode().getId())                .build();        Wearable.DataApi.getDataItem(mGoogleApiClient, uri)                .setResultCallback(                        new ResultCallback<DataApi.DataItemResult>() {                            @Override                            public void onResult(DataApi.DataItemResult dataItemResult) {                                log("Finish Config: " + dataItemResult.getStatus());                                if (dataItemResult.getStatus().isSuccess() && dataItemResult.getDataItem() != null) {                                    fetchConfig(dataItemResult.getDataItem());                                }                            }                        }                );    }});
You might have multiple wearable devices connected to the the phone that store data with the same path. For that you can use getDataItems() to get data for all devices.
Uri uri = new Uri.Builder()        .scheme("wear")        .path("/WeatherWatchFace/Config")        .build();Wearable.DataApi.getDataItems(mGoogleApiClient, uri)        .setResultCallback(                new ResultCallback<DataItemBuffer>() {                    @Override                    public void onResult(DataItemBuffer dataItems) {                        for(int i=0;i<dataItems.getCount();i++){                            Log.d(TAG,"The data is from: " + dataItems.get(i).getUri().getAuthority());                        }                    }                }        );
Note also, the data for an app is isolated by it's package name, let's says you install another app, you cannot use DataApi get to the data from a different app, even if it shares the same path.

Wearable Listener Service

You can use WearableListenerService to listen Wearable APIs' events when a device boots (either Wear or Handheld). This feature is the same as you add listeners on the wearable app example which is inside of WatchFaceService, but the benefit of using WearableListenerService is that it runs on background independently. Let's say you are using a watch face called A, and you open the config Activity on a phone of another watch face called B. Your config changes will lost because watch face B isn't active so it can't receive any message. Therefore, if we add a WearableListenerService, you can get all messages although a  watch face isn't active.With the following example, we will use this class on a phone app and listen a message(a require) from a wear. When WearableListenerService on a phone app get the message, it will call weather API to get current weather data and send the data to the wear.Step 1: Create a WeatherService class which extends WearableListenerService
public class WeatherService extends WearableListenerService {    @Override    public void onMessageReceived(MessageEvent messageEvent) {                mPeerId = messageEvent.getSourceNodeId();        if (messageEvent.getPath().equals("/WeatherService/Require")) {            // Get Location            mLocationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);            mLocation = mLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);            // Start getting weather data asynchronously            GetWeatherTask task = new GetWeatherTask();            task.execute();        }    }    private class GetWeatherTask extends AsyncTask {        @Override        protected Object doInBackground(Object[] params) {            try {                              WeatherApi api = new OpenWeatherApi();                WeatherInfo info = api.getCurrentWeatherInfo(mLocation.getLatitude(), mLocation.getLongitude());                // Send Data to the wear.                DataMap config = new DataMap();                                config.putInt(KEY_WEATHER_TEMPERATURE, info.getTemperature());                config.putString(KEY_WEATHER_CONDITION, info.getCondition());                config.putLong(KEY_WEATHER_SUNSET, info.getSunset());                config.putLong(KEY_WEATHER_SUNRISE, info.getSunrise());                Wearable.MessageApi.sendMessage(mGoogleApiClient, mPeerId, PATH_WEATHER_INFO, config.toByteArray())                        .setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {                    @Override                    public void onResult(MessageApi.SendMessageResult sendMessageResult) {                        Log.d(TAG, "SendUpdateMessage: " + sendMessageResult.getStatus());                    }                });            } catch (Exception e) {                Log.d(TAG, "Task Fail: " + e);            }            return null;        }    }}
Step 2: Add service element on AndroidManifest.xml
<service android:name=".WeatherService" >    <intent-filter>        <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />    </intent-filter></service>

Weather API

There are several Weather APIs available to use, such as Yahoo Weather API, Open Weather API, or AccuWeather API. We chose Open Weather as it's easy to use and free.
// We create a interface in case we change to another API hostpublic interface WeatherApi {    WeatherInfo getCurrentWeatherInfo(double lon, double lat);}// WeatherInfo includes the narrow data which we needpublic class WeatherInfo {    private String cityName;    private String condition;       private int temperature;    private long sunrise;       private long sunset;    }// Implement OpenWeatherApipublic class OpenWeatherApi implements WeatherApi {    private static final String TAG ="OpenWeatherApi";    private static final String APIURL = "http://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&units=imperial&APPID=%s";    @Inject     private Context context;    @Override    public WeatherInfo getCurrentWeatherInfo(double lat, double lon) {        WeatherInfo w = null;        try {            // ObjectMapper is a RestClient supplied by Jackson            ObjectMapper mapper = new ObjectMapper();            mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);            String key = context.getResources().getString(R.string.openweather_appid);            String url = String.format(APIURL, lat, lon, key);            Log.d(TAG,"ApiUrl: "+url);            OpenWeatherQueryResult result = mapper.readValue(new URL(url), OpenWeatherQueryResult.class);            if ("200".equals(result.getCod())) {                w = new WeatherInfo();                w.setCityName(result.getName());                w.setTemperature((int) result.getMain().getTemp());                w.setSunset(result.getSys().getSunset());                w.setSunrise(result.getSys().getSunrise());                OpenWeatherData[] dataArray = result.getWeather();                if (dataArray != null && dataArray.length > 0) {                    w.setCondition(ConvertCondition(dataArray[0].getId()));                }            }        } catch (Exception e) {            e.printStackTrace();        }        return w;    }    }

Performance Considerations

We considered several approaches for communication between our wearable and handheld apps. The goal here was to preserve battery while providing up to date weather data.The approaches we tried included:
  1. A service that always runs on the phone with regular weather updates and pushes them to the watch face. This is not ideal as the service runs constantly though the wearable device may not even be paired.
  2. Next we tried enabling the service only when a wearable device is connected. i.e. When a wear is paired, get weather information and push it to the wearable device. This, however, meant that the service is running, even if our watch face is not selected. Here the handheld app may send data but there is no receiver.
  3. Finally, we moved the trigger for updating the weather data to the watch face. The timer runs on the watch face (we use the same timer for redrawing). If the weather information is stale, it sends a message to the handheld. The service on the handheld receives the message and gets the latest weather information and updates it on the wearable. Using this approach, the service only runs when the watch face is in use, and the weather data is kept up to date!

Figure 2: Weather Data Communication

  

Configuration Activity

A configuration activity to manage watch face attributes such as color, time format etc can exist on either the wearable or on the phone. The implementation of the activity is exactly like a regular activity, so we don't cover this in detail, the source code is here. We will focus on how to set up the connection between a watch face and it's configuration activity via AndroidManifest.xml.

AndroidManifest.xml on Wear

Add an activity element and add meta data inside of service
<service android:name=".WeatherWatchFaceService">    <!-- the setting is for the config activity on the handhold app-->               <meta-data            android:name="com.google.android.wearable.watchface.wearableConfigurationAction"                        android:value="com.mycompany.wearable.watchface.CONFIG" />        <!-- the setting is for the config activity on the wear app-->               <meta-data            android:name="com.google.android.wearable.watchface.wearableConfigurationAction"            android:value="com.mycompany.wearable.watchface.CONFIG" /></service><activity android:name=".WeatherWatchFaceConfigActivtiy">    <intent-filter>        <!--The value has to match the value of service’s meta-data-->        <action android:name="com.mycompany.wearable.watchface.CONFIG" />        <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />        <category android:name="android.intent.category.DEFAULT" />    </intent-filter></activity>

AndroidManifest.xml on Handheld

Add the activity on AndroidManifest.xml on Handheld.
<activity android:name=".WeatherWatchFaceConfigActivtiy">    <intent-filter>        <!--The value has to match the value of service’s meta-data-->        <action android:name="com.mycompany.wearable.watchface.CONFIG" />        <category android:name="com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION" />        <category android:name="android.intent.category.DEFAULT" />    </intent-filter></activity>
After you deploy the apps, navigate to the change watch face screen, if the watch face has the config activity, a gear icon appears below the watch face on the wearable. On a phone, open the Android Wear app, a settings gear appears as well as a menu option is added for "Settings". see Figure 4.

Figure 4: The config gear and menu on a wearable and on the android wear app.

 Sending and receiving the watch face configuration data is similar to how we sent and received weather data, so we don't duplicate the same code here, you can refer the example above.

Storing data for multiple wearables

We want each wearable device to have its own configuration settings. Let's say one watch face has a blue background and another has an orange background. In this case, we can't just use DataApi to save the data on a phone because when we save the data, as it will use the phone's ID for identification. We want custom settings for each watch face. So we used the following approach:
  1. Use MessageApi to send a 'setting changed' message to the wearable from the phone.
  2. When the wearable receives the message, use the DataApi on the wearable to save the data. Now, the DataApi will use the wearable's ID to save the info and each wearable can use it's own ID to map to it's own settings.

Figure 5: Managing data for multiple devices.

Congratulations! Now the only step left is to publish your watch face on the Google Play Store. We'll cover this in Part 4 of this series.

Recommended Posts

12
September

VP's of Digital Need to Start Embracing Machine Learning

Machine learning isn’t just for billion dollars companies like Google or Amazon or fun projects like developing an...

Read More >

3
August

Electric car highways ๐Ÿ‡ฆ๐Ÿ‡บ, full-body VR, and 3D-printed... Earables? ๐Ÿ‘‚

This week we're talking about full-body VR wearables, human organ factories in New Hampshire, the largest electric vehic...

Read More >

27
July

If you care about the environment, we've got some wins for you ๐Ÿ‘

This week we're talking about robotic eels tracking water pollution, building DNA from scratch๐Ÿ‘จ‍๐Ÿ”ฌ, the first float...

Read More >

Let's solve digital.