Android Wear / MobileCompanion / WatchFace

Something O’Clock – Part 3

On 1st April 2016 I published Something O’Clock, a watch face app for Android Wear, to Play Store. The app is lighthearted in nature (because of the date of publication), it allows the user to set the time to “beer o’clock”, or “sleep o’clock”, or even “burger o’clock”. Although the app itself is quite lighthearted the code behind it is worthy of study and, in this series we’ll take a look at various aspects of developing custom watch faces for Android Wear. In this article we’ll look creating a companion app to run on the phone connected to the Wear device.

The heart of the companion app is a pretty simple Activity which hosts a RecyclerView instance containing the list of possible words. In order to stay on topic I’m not going to give an explanation of the RecyclerView implementation, but the only aspect of it worthy of explanation is how we obtain the list of words to display. We want the same set of words to be available to both phone & wear apps, so I have created a common module upon which both the wear and mobile modes depend. This common module contains the list of words as a string array resource. This makes our code easier to maintain because we only have to change things in one place and the changes will get picked up buy both mobile and wear modules when we build.

There is something worth a slight digression: The string array itself contains references to strings for each value:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  
  <string-array name="default_words">
    <item>@string/something</item>
    <item>@string/food</item>
    <item>@string/burrito</item>
    <item>@string/pizza</item>
    <item>@string/burger</item>
    <item>@string/chicken</item>
    <item>@string/booze</item>
    <item>@string/beer</item>
    <item>@string/wine</item>
    <item>@string/whisky</item>
    <item>@string/go_home</item>
    <item>@string/sleep</item>
  </string-array>

</resources>

The strings themselves are defined separately:

<resources>
  <string name="something">something</string>
  <string name="food">food</string>
  <string name="burrito">burrito</string>
  <string name="pizza">pizza</string>
  <string name="burger">burger</string>
  <string name="chicken">chicken</string>
  <string name="booze">booze</string>
  <string name="beer">beer</string>
  <string name="wine">wine</string>
  <string name="whisky">whisky</string>
  <string name="go_home">go home</string>
  <string name="sleep">sleep</string>
</resources>

The reason for this is to future-proof things a little. If we ever wanted to translate the app in to different languages, all that would be required would be to translate strings.xml and not the array itself. The benefit here can be seen from how I have implemented an alternate spelling for “whisky” if the locale us set to US:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="whisky">whiskey</string>
</resources>

I only need to override the one word to get the desired behaviour. Moreover for me, as a developer who only speaks English, the array itslef would be more difficult for me to understand of the translations were directly in there, but by using indirection in this way the arrays definition remains understandable to me.

So, back to the mobile companion app, the first thing worthy of mention is that the wear watchface app and its mobile companion app must share the same package name. Also, the mobile companion app needs declaring a little differently in the Manifest in order to associate it with the watchface within the Wear app on the mobile device:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="com.stylingandroid.something.oclock">

  <!-- Required to act as a custom watch face. -->
  <uses-permission android:name="android.permission.WAKE_LOCK" />

  <application
    android:allowBackup="false"
    android:fullBackupOnly="false"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
    <activity
      android:name="com.stylingandroid.something.oclock.SettingsActivity"
      android:label="@string/app_name">
      <intent-filter>
        <action android:name="com.stylingandroid.customoclock.CONFIGURATION" />

        <category android:name="com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>

    </activity>
  </application>

</manifest>

Registering using this intent-filter adds the ‘cog’ to the watchface within the Wear mobile app which will launch our settings:

wear app

The next thing that we need to do is set up the connection between the wear and mobile apps. In this case we’ll use the Wear Data API which is part of the Play Services Wearable library. We need to include the relevant dependency in our build.gradle for the mobile app:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "${buildTools}"

    defaultConfig {
        applicationId "com.stylingandroid.something.oclock"
        minSdkVersion 18
        targetSdkVersion 23
        versionCode appVersionCode
        versionName "${appVersionName}"
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    lintOptions {
        abortOnError false
    }
}

dependencies {
    compile project(':common')
    compile "com.android.support:design:${supportLibraryVersion}"
    compile "com.android.support:appcompat-v7:${supportLibraryVersion}"
    compile "com.android.support:recyclerview-v7:${supportLibraryVersion}"
    compile "com.google.android.gms:play-services-wearable:${playServicesVersion}"
    wearApp project(':wear')
}

Nest we have DataClientLoader which is responsible for setting up and tearing down the Play Services connection:

public class DataClientLoader implements GoogleApiClient.OnConnectionFailedListener {
    private static final String WEAR_API_UNAVAILABLE = "Wear API Unavailable";

    private final Set<Callback> callbacks;

    private GoogleApiClient googleApiClient = null;

    public static DataClientLoader newInstance() {
        Set<Callback> callbacks = Collections.newSetFromMap(new WeakHashMap<Callback, Boolean>());
        return new DataClientLoader(callbacks);
    }

    DataClientLoader(Set<Callback> callbacks) {
        this.callbacks = callbacks;
    }

    public void loadDataClient(FragmentActivity activity, Callback callback) {
        callbacks.add(callback);
        if (googleApiClient != null) {
            return;
        }
        createConnection(activity);
    }

    public void closeDataClient(Callback callback) {
        callbacks.remove(callback);
        googleApiClient.disconnect();
    }
    .
    .
    .
    public interface Callback {
        void connected(DataClient dataClient);

        void suspended(int reason);

        void failed(String reason);
    }
}

This is the public API of this class. It will do everything asynchronously and there’s a Callback interface which the client must implement which we’ll call once the connection is complete.

There are some protections in place to prevent multiple simultaneous connections – if one is already in progress we simple add the new client to a list to get notified when the existing connection attempt completes. The inner workings look like this:

public class DataClientLoader implements GoogleApiClient.OnConnectionFailedListener {
    .
    .
    .
    private void createConnection(FragmentActivity activity) {
        String[] defaultWords = CommonData.getTimeStrings(activity);
        ConnectionCallbacks connectionCallbacks = new ConnectionCallbacks(defaultWords);
        googleApiClient = new GoogleApiClient.Builder(activity)
                .addConnectionCallbacks(connectionCallbacks)
                .addOnConnectionFailedListener(this)
                .enableAutoManage(activity, this)
                .addApi(Wearable.API)
                .build();
        googleApiClient.connect();
    }

    private final class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks {
        private final String[] words;

        private ConnectionCallbacks(String[] words) {
            this.words = words;
        }

        @Override
        public void onConnected(@Nullable Bundle bundle) {
            DataClient dataClient = DataClient.newInstance(googleApiClient, words);
            for (Callback callback : callbacks) {
                callback.connected(dataClient);
            }
        }

        @Override
        public void onConnectionSuspended(int i) {
            for (Callback callback : callbacks) {
                callback.suspended(i);
            }
        }
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        String message = connectionResult.getErrorMessage();
        if (TextUtils.isEmpty(message) && connectionResult.getErrorCode() == ConnectionResult.API_UNAVAILABLE) {
            message = WEAR_API_UNAVAILABLE;
        }
        for (Callback callback : callbacks) {
            callback.failed(message);
        }
    }
    .
    .
    .
}

I won’t go in to huge detail here because it is the standard mechanism for connecting to Play Services as is already documented here. We are wrapping it so that our Activity is completely agnostic of the Play Services connection stuff meaning that we could re-use it if we had more than one Activity. When we build the GoogleApiClient using GoogleApiClient.Builder we request the Wearable API and calling enableAutoManage(true) should ensure that Play Services will manage things for us (prompting the user, as required) if the required components are not already installed. Calling googleApiClient.connect(); kicks of the async connection attempt and we’ll get callbacks on our ConnectionCallbacks or onConnectionFailed() once the attempt is complete.

In the connection is successful (and this is a local connection to Play Services on the mobile device, not the connection to the wear app itself) we construct a DataClient object and return it via callback.

The DataClient is responsible for sending data to the wear app:

public class DataClient {

    private final GoogleApiClient googleApiClient;

    private String word;
    private final ArrayList<String> words;

    public static DataClient newInstance(GoogleApiClient googleApiClient, String[] words) {
        ArrayList<String> wordsList = new ArrayList<>(Arrays.asList(words));
        return new DataClient(googleApiClient, wordsList);
    }

    DataClient(GoogleApiClient googleApiClient, ArrayList<String> words) {
        this.googleApiClient = googleApiClient;
        this.words = words;
    }

    public void setWord(@NonNull String newWord, ResultCallback<DataApi.DataItemResult> callback) {
        this.word = newWord;
        if (!words.contains(newWord)) {
            words.add(newWord);
        }
        sendData(callback);
    }

    private void sendData(ResultCallback<DataApi.DataItemResult> callback) {
        PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(CommonData.PATH_WORDS);
        putDataMapRequest.setUrgent();
        DataMap dataMap = putDataMapRequest.getDataMap();
        dataMap.putString(CommonData.KEY_WORD, word);
        dataMap.putStringArrayList(CommonData.KEY_WORDS, words);
        PutDataRequest putDataRequest = putDataMapRequest.asPutDataRequest();
        PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi.putDataItem(googleApiClient, putDataRequest);
        pendingResult.setResultCallback(callback);
    }

}

This is actually pretty simple. Whenever the user taps on a word from the list setWord() will be called. This will asynchronously send the data and the caller will get a callback once the write is complete. Once again, this doesn’t actually require a connection to the wear device – Play Services will queue it and send once a connection is available if one isn’t immediately available. Also, Play Services can route the sed differently depending on the connectivity to the wear device. Im many cases the connection will be over Bluetooth, but if the mobile and wear devices are too far apart, some wear devices support wifi connection and the data can be sent via that mechanism instead. However this is all handled for us by Play Services so we can just fire and forget!

The sendData() method is where all of the work is done. We first create a PutDataMapRequest using a key defined in our common module – we’ll need this same value on the wear side hence putting it in common). Specifying setUrgent() means that Play Services will try and deliver it as soon as possible rather than waiting until the next scheduled batch. We need to do this otherwise the user will have to wait a while before the face updates after they select a new word.

We then build a DataMap (which is a simple key / value pair map) containing the data to be sent – once again using key names from common. We convert this in to a PutDataRequest which simply bundles our DataMap in to a single item. Finally we call putDataItem() which kicks off the async send.

The more observant will have noticed that we send the list of words as well as the selected word. This hints at a future enhancement where we may support customisation of the list of words…

That’s it on the mobile side. When the user taps on a word from the list it gets packed up and sent to the wear app:

However we only have half of the solution here as we now need to receive this data on the wear app and update the watch-face accordingly. In the next article we’ll take a look at that.

The source code for this article is available here.

Many thanks to Daniele Bonaldo, Sebastiano Poggi, Erik Hellman, Hasan Hosgel, Said Tahsin Dane & Murat Yener – my beta testers.

Get it on Google Play

© 2016, Mark Allison. All rights reserved.

Copyright © 2016 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.