Android Wear / Data API / MobileCompanion / WatchFace

Something O’Clock – Part 6

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 concluding article we’ll look at how we update the watch-face UI in response to configuration changes.

round_wear_screenshotPreviously we have looked at how the mobile companion app enables the use to select different words from a list, this data then gets transmitted to the wear app using the Google Play Service DataApi framework. The wear app then persists this data locally, also using the DataApi with some built in data integrity protection. So now that we have the data stored locally, how do we trigger the UI update? The trick is to actually register our watch-face Engine component to listen for changes to the data we’re interested in. The first part of that process is to connect to the Play Services Wearable API:

public class SomethingOClockFace extends CanvasWatchFaceService {

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

    private class Engine extends CanvasWatchFaceService.Engine {
        private boolean ambient;
        private InsetCalculator insetCalculator;
        private TextLayout textLayout;

        private GoogleApiClient googleApiClient;

        private boolean lowBitAmbient;

        private int activeBackgroundColour;
        private int ambientBackgroundColour;

        @Override
        public void onCreate(SurfaceHolder holder) {
            super.onCreate(holder);

            Context context = SomethingOClockFace.this;
            setWatchFaceStyle(new WatchFaceStyle.Builder(SomethingOClockFace.this)
                                      .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
                                      .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
                                      .setShowSystemUiTime(false)
                                      .setAcceptsTapEvents(false)
                                      .build());

            activeBackgroundColour = ContextCompat.getColor(context, R.color.background);
            ambientBackgroundColour = Color.BLACK;
            int textColour = ContextCompat.getColor(context, R.color.digital_text);

            textLayout = TextLayout.newInstance(textColour);

            googleApiClient = new GoogleApiClient.Builder(SomethingOClockFace.this)
                    .addConnectionCallbacks(googleConnectionCallbacks)
                    .addApi(Wearable.API)
                    .build();
        }

        @Override
        public void onVisibilityChanged(boolean visible) {
            super.onVisibilityChanged(visible);

            if (visible) {
                googleApiClient.connect();
            } else {
                if (googleApiClient != null && googleApiClient.isConnected()) {
                    Wearable.DataApi.removeListener(googleApiClient, dataListener);
                    googleApiClient.disconnect();
                }
            }
        }
        .
        .
        .
    }
}

In onCreate() we build the GoogleApiClient object but we don’t actually connect to it yet. The appropriate place to do this is onVisibilityChanged() because the lifecycle of a watch face is somewhat different to a standard service, so we start listening for data changes when we’re visible, but stop listening when we’re not.

When we build the GoogleApiClient we pass in a GoogleApiClient.ConnectionCallbacks instance which will receive callbacks once we’re connected:

public class SomethingOClockFace extends CanvasWatchFaceService {

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

    private class Engine extends CanvasWatchFaceService.Engine {
        .
        .
        .
        private GoogleApiClient.ConnectionCallbacks googleConnectionCallbacks = new GoogleApiClient.ConnectionCallbacks() {
            @Override
            public void onConnected(@Nullable Bundle bundle) {
                final LocalDataMap localDataMap = LocalDataMap.newInstance(SomethingOClockFace.this, googleApiClient);
                fetchConfigDataMap(localDataMap);
                localDataMap.getLocalStorageUri(new ResultCallback<UriResult>() {
                    @Override
                    public void onResult(@NonNull UriResult uriResult) {
                        Wearable.DataApi.addListener(googleApiClient, dataListener, uriResult.getUri(), DataApi.FILTER_LITERAL);
                    }
                });
            }

            @Override
            public void onConnectionSuspended(int i) {
                //NO-OP
            }
        };

        public void fetchConfigDataMap(LocalDataMap localDataMap) {
            localDataMap.fetchConfig(new ResultCallback() {
                @Override
                public void onResult(@NonNull DataMapResult dataMapResult) {
                    if (dataMapResult.getStatus().isSuccess()) {
                        DataMap dataMap = dataMapResult.getDataMap();
                        decode(dataMap);
                    }
                }
            });
        }

        private DataApi.DataListener dataListener = new DataApi.DataListener() {
            @Override
            public void onDataChanged(DataEventBuffer dataEventBuffer) {
                for (DataEvent event : dataEventBuffer) {
                    Uri uri = event.getDataItem().getUri();
                    if (uri.getPath().equals(LocalDataMap.PATH_WORDS)) {
                        decode(event.getDataItem());
                    }
                }
            }
        };
        .
        .
        .
    }
}

Once we’re connected to Play Services, we create a LocalDataMap instance, we first fetch the local config and then look up the Uri for the local data, just as we did in the previous article. Once we have this Uri we register a data listener which will get called whenever the data is updated – similar to a ContentObserver. Hopefully the benefits of having this local data storage are now becoming apparent – we can have different components updating this data, but by marshalling it through this local storage we only need to register for updates from a single data source.

Both the fetchConfigDataMap() method and the onDataChanged() method of our DataListener use some common methods to decode the selected word from the data, and then update the UI:

public class SomethingOClockFace extends CanvasWatchFaceService {

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

    private class Engine extends CanvasWatchFaceService.Engine {
        .
        .
        .
        private void decode(DataItem dataItem) {
            Uri uri = dataItem.getUri();
            String path = uri.getPath();
            if (path.equals(LocalDataMap.PATH_WORDS)) {
                DataMap dataMap = LocalDataMap.dataMapFrom(dataItem);
                decode(dataMap);
            }
        }

        private void decode(DataMap dataMap) {
            String word = dataMap.getString(CommonData.KEY_WORD);
            updateWord(word);
        }

        private void updateWord(String word) {
            textLayout.setTimeText(word);
            invalidate();
        }
    }
}

When we update the UI, we change the time text in our textLayout and call invalidate() to force a redraw. The only thing remaining is to update TextLayout to add the appropriate method:

public class TextLayout {
   .
   .
   .
    public void setTimeText(String timeText) {
        lines.remove(0);
        lines.add(0, timeText);
        invalidateLayout();
    }
   .
   .
   .
}

All of the code we looked at earlier in the series to size and position the text dynamically will now kick in and everything will be sized and positioned appropriately.

That’s it, when we select a new word from the list in the mobile companion app, then the watch face updates accordingly. Apologies that I am unable to include a video of this, but video capture from wear over adb wasn’t working for me – you’ll either have to just take my work for it, or try it yourself. Here are some static images of different words in action:

beer

whisky

sleep

Although there is some scope for further enhancements such as having a companion app on the wear device itself, and allowing custom test strings to be used, we’ll leave it here for now as we’ve already gone in to quite a bit of depth. If anyone would be interested in me covering these, then please let me know and perhaps we’ll have a future series where we cover these enhancements.

So, for now, that concludes our dive in to the world of Android Wear custom watch-faces and companion apps.

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.