1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.wearable.watchface;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.content.res.Resources;
24 import android.graphics.Canvas;
25 import android.graphics.Paint;
26 import android.graphics.Rect;
27 import android.graphics.Typeface;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.Message;
31 import android.support.v4.content.ContextCompat;
32 import android.support.wearable.watchface.CanvasWatchFaceService;
33 import android.support.wearable.watchface.WatchFaceService;
34 import android.support.wearable.watchface.WatchFaceStyle;
35 import android.text.format.DateFormat;
36 import android.util.Log;
37 import android.view.SurfaceHolder;
38 import android.view.WindowInsets;
39 
40 import com.google.android.gms.common.ConnectionResult;
41 import com.google.android.gms.common.api.GoogleApiClient;
42 import com.google.android.gms.wearable.DataApi;
43 import com.google.android.gms.wearable.DataEvent;
44 import com.google.android.gms.wearable.DataEventBuffer;
45 import com.google.android.gms.wearable.DataItem;
46 import com.google.android.gms.wearable.DataMap;
47 import com.google.android.gms.wearable.DataMapItem;
48 import com.google.android.gms.wearable.Wearable;
49 
50 import java.text.SimpleDateFormat;
51 import java.util.Calendar;
52 import java.util.Date;
53 import java.util.Locale;
54 import java.util.TimeZone;
55 import java.util.concurrent.TimeUnit;
56 
57 /**
58  * Sample digital watch face with blinking colons and seconds. In ambient mode, the seconds are
59  * replaced with an AM/PM indicator and the colons don't blink. On devices with low-bit ambient
60  * mode, the text is drawn without anti-aliasing in ambient mode. On devices which require burn-in
61  * protection, the hours are drawn in normal rather than bold. The time is drawn with less contrast
62  * and without seconds in mute mode.
63  */
64 public class DigitalWatchFaceService extends CanvasWatchFaceService {
65     private static final String TAG = "DigitalWatchFaceService";
66 
67     private static final Typeface BOLD_TYPEFACE =
68             Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
69     private static final Typeface NORMAL_TYPEFACE =
70             Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
71 
72     /**
73      * Update rate in milliseconds for normal (not ambient and not mute) mode. We update twice
74      * a second to blink the colons.
75      */
76     private static final long NORMAL_UPDATE_RATE_MS = 500;
77 
78     /**
79      * Update rate in milliseconds for mute mode. We update every minute, like in ambient mode.
80      */
81     private static final long MUTE_UPDATE_RATE_MS = TimeUnit.MINUTES.toMillis(1);
82 
83     @Override
onCreateEngine()84     public Engine onCreateEngine() {
85         return new Engine();
86     }
87 
88     private class Engine extends CanvasWatchFaceService.Engine implements DataApi.DataListener,
89             GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
90         static final String COLON_STRING = ":";
91 
92         /** Alpha value for drawing time when in mute mode. */
93         static final int MUTE_ALPHA = 100;
94 
95         /** Alpha value for drawing time when not in mute mode. */
96         static final int NORMAL_ALPHA = 255;
97 
98         static final int MSG_UPDATE_TIME = 0;
99 
100         /** How often {@link #mUpdateTimeHandler} ticks in milliseconds. */
101         long mInteractiveUpdateRateMs = NORMAL_UPDATE_RATE_MS;
102 
103         /** Handler to update the time periodically in interactive mode. */
104         final Handler mUpdateTimeHandler = new Handler() {
105             @Override
106             public void handleMessage(Message message) {
107                 switch (message.what) {
108                     case MSG_UPDATE_TIME:
109                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
110                             Log.v(TAG, "updating time");
111                         }
112                         invalidate();
113                         if (shouldTimerBeRunning()) {
114                             long timeMs = System.currentTimeMillis();
115                             long delayMs =
116                                     mInteractiveUpdateRateMs - (timeMs % mInteractiveUpdateRateMs);
117                             mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
118                         }
119                         break;
120                 }
121             }
122         };
123 
124         GoogleApiClient mGoogleApiClient = new GoogleApiClient.Builder(DigitalWatchFaceService.this)
125                 .addConnectionCallbacks(this)
126                 .addOnConnectionFailedListener(this)
127                 .addApi(Wearable.API)
128                 .build();
129 
130         /**
131          * Handles time zone and locale changes.
132          */
133         final BroadcastReceiver mReceiver = new BroadcastReceiver() {
134             @Override
135             public void onReceive(Context context, Intent intent) {
136                 mCalendar.setTimeZone(TimeZone.getDefault());
137                 initFormats();
138                 invalidate();
139             }
140         };
141 
142         /**
143          * Unregistering an unregistered receiver throws an exception. Keep track of the
144          * registration state to prevent that.
145          */
146         boolean mRegisteredReceiver = false;
147 
148         Paint mBackgroundPaint;
149         Paint mDatePaint;
150         Paint mHourPaint;
151         Paint mMinutePaint;
152         Paint mSecondPaint;
153         Paint mAmPmPaint;
154         Paint mColonPaint;
155         float mColonWidth;
156         boolean mMute;
157 
158         Calendar mCalendar;
159         Date mDate;
160         SimpleDateFormat mDayOfWeekFormat;
161         java.text.DateFormat mDateFormat;
162 
163         boolean mShouldDrawColons;
164         float mXOffset;
165         float mYOffset;
166         float mLineHeight;
167         String mAmString;
168         String mPmString;
169         int mInteractiveBackgroundColor =
170                 DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND;
171         int mInteractiveHourDigitsColor =
172                 DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS;
173         int mInteractiveMinuteDigitsColor =
174                 DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS;
175         int mInteractiveSecondDigitsColor =
176                 DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS;
177 
178         /**
179          * Whether the display supports fewer bits for each color in ambient mode. When true, we
180          * disable anti-aliasing in ambient mode.
181          */
182         boolean mLowBitAmbient;
183 
184         @Override
onCreate(SurfaceHolder holder)185         public void onCreate(SurfaceHolder holder) {
186             if (Log.isLoggable(TAG, Log.DEBUG)) {
187                 Log.d(TAG, "onCreate");
188             }
189             super.onCreate(holder);
190 
191             setWatchFaceStyle(new WatchFaceStyle.Builder(DigitalWatchFaceService.this)
192                     .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
193                     .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
194                     .setShowSystemUiTime(false)
195                     .build());
196             Resources resources = DigitalWatchFaceService.this.getResources();
197             mYOffset = resources.getDimension(R.dimen.digital_y_offset);
198             mLineHeight = resources.getDimension(R.dimen.digital_line_height);
199             mAmString = resources.getString(R.string.digital_am);
200             mPmString = resources.getString(R.string.digital_pm);
201 
202             mBackgroundPaint = new Paint();
203             mBackgroundPaint.setColor(mInteractiveBackgroundColor);
204             mDatePaint = createTextPaint(
205                     ContextCompat.getColor(getApplicationContext(), R.color.digital_date));
206             mHourPaint = createTextPaint(mInteractiveHourDigitsColor, BOLD_TYPEFACE);
207             mMinutePaint = createTextPaint(mInteractiveMinuteDigitsColor);
208             mSecondPaint = createTextPaint(mInteractiveSecondDigitsColor);
209             mAmPmPaint = createTextPaint(
210                     ContextCompat.getColor(getApplicationContext(), R.color.digital_am_pm));
211             mColonPaint = createTextPaint(
212                     ContextCompat.getColor(getApplicationContext(), R.color.digital_colons));
213 
214             mCalendar = Calendar.getInstance();
215             mDate = new Date();
216             initFormats();
217         }
218 
219         @Override
onDestroy()220         public void onDestroy() {
221             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
222             super.onDestroy();
223         }
224 
createTextPaint(int defaultInteractiveColor)225         private Paint createTextPaint(int defaultInteractiveColor) {
226             return createTextPaint(defaultInteractiveColor, NORMAL_TYPEFACE);
227         }
228 
createTextPaint(int defaultInteractiveColor, Typeface typeface)229         private Paint createTextPaint(int defaultInteractiveColor, Typeface typeface) {
230             Paint paint = new Paint();
231             paint.setColor(defaultInteractiveColor);
232             paint.setTypeface(typeface);
233             paint.setAntiAlias(true);
234             return paint;
235         }
236 
237         @Override
onVisibilityChanged(boolean visible)238         public void onVisibilityChanged(boolean visible) {
239             if (Log.isLoggable(TAG, Log.DEBUG)) {
240                 Log.d(TAG, "onVisibilityChanged: " + visible);
241             }
242             super.onVisibilityChanged(visible);
243 
244             if (visible) {
245                 mGoogleApiClient.connect();
246 
247                 registerReceiver();
248 
249                 // Update time zone and date formats, in case they changed while we weren't visible.
250                 mCalendar.setTimeZone(TimeZone.getDefault());
251                 initFormats();
252             } else {
253                 unregisterReceiver();
254 
255                 if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
256                     Wearable.DataApi.removeListener(mGoogleApiClient, this);
257                     mGoogleApiClient.disconnect();
258                 }
259             }
260 
261             // Whether the timer should be running depends on whether we're visible (as well as
262             // whether we're in ambient mode), so we may need to start or stop the timer.
263             updateTimer();
264         }
265 
initFormats()266         private void initFormats() {
267             mDayOfWeekFormat = new SimpleDateFormat("EEEE", Locale.getDefault());
268             mDayOfWeekFormat.setCalendar(mCalendar);
269             mDateFormat = DateFormat.getDateFormat(DigitalWatchFaceService.this);
270             mDateFormat.setCalendar(mCalendar);
271         }
272 
registerReceiver()273         private void registerReceiver() {
274             if (mRegisteredReceiver) {
275                 return;
276             }
277             mRegisteredReceiver = true;
278             IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
279             filter.addAction(Intent.ACTION_LOCALE_CHANGED);
280             DigitalWatchFaceService.this.registerReceiver(mReceiver, filter);
281         }
282 
unregisterReceiver()283         private void unregisterReceiver() {
284             if (!mRegisteredReceiver) {
285                 return;
286             }
287             mRegisteredReceiver = false;
288             DigitalWatchFaceService.this.unregisterReceiver(mReceiver);
289         }
290 
291         @Override
onApplyWindowInsets(WindowInsets insets)292         public void onApplyWindowInsets(WindowInsets insets) {
293             if (Log.isLoggable(TAG, Log.DEBUG)) {
294                 Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square"));
295             }
296             super.onApplyWindowInsets(insets);
297 
298             // Load resources that have alternate values for round watches.
299             Resources resources = DigitalWatchFaceService.this.getResources();
300             boolean isRound = insets.isRound();
301             mXOffset = resources.getDimension(isRound
302                     ? R.dimen.digital_x_offset_round : R.dimen.digital_x_offset);
303             float textSize = resources.getDimension(isRound
304                     ? R.dimen.digital_text_size_round : R.dimen.digital_text_size);
305             float amPmSize = resources.getDimension(isRound
306                     ? R.dimen.digital_am_pm_size_round : R.dimen.digital_am_pm_size);
307 
308             mDatePaint.setTextSize(resources.getDimension(R.dimen.digital_date_text_size));
309             mHourPaint.setTextSize(textSize);
310             mMinutePaint.setTextSize(textSize);
311             mSecondPaint.setTextSize(textSize);
312             mAmPmPaint.setTextSize(amPmSize);
313             mColonPaint.setTextSize(textSize);
314 
315             mColonWidth = mColonPaint.measureText(COLON_STRING);
316         }
317 
318         @Override
onPropertiesChanged(Bundle properties)319         public void onPropertiesChanged(Bundle properties) {
320             super.onPropertiesChanged(properties);
321 
322             boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
323             mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : BOLD_TYPEFACE);
324 
325             mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
326 
327             if (Log.isLoggable(TAG, Log.DEBUG)) {
328                 Log.d(TAG, "onPropertiesChanged: burn-in protection = " + burnInProtection
329                         + ", low-bit ambient = " + mLowBitAmbient);
330             }
331         }
332 
333         @Override
onTimeTick()334         public void onTimeTick() {
335             super.onTimeTick();
336             if (Log.isLoggable(TAG, Log.DEBUG)) {
337                 Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
338             }
339             invalidate();
340         }
341 
342         @Override
onAmbientModeChanged(boolean inAmbientMode)343         public void onAmbientModeChanged(boolean inAmbientMode) {
344             super.onAmbientModeChanged(inAmbientMode);
345             if (Log.isLoggable(TAG, Log.DEBUG)) {
346                 Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
347             }
348             adjustPaintColorToCurrentMode(mBackgroundPaint, mInteractiveBackgroundColor,
349                     DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND);
350             adjustPaintColorToCurrentMode(mHourPaint, mInteractiveHourDigitsColor,
351                     DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS);
352             adjustPaintColorToCurrentMode(mMinutePaint, mInteractiveMinuteDigitsColor,
353                     DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS);
354             // Actually, the seconds are not rendered in the ambient mode, so we could pass just any
355             // value as ambientColor here.
356             adjustPaintColorToCurrentMode(mSecondPaint, mInteractiveSecondDigitsColor,
357                     DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS);
358 
359             if (mLowBitAmbient) {
360                 boolean antiAlias = !inAmbientMode;
361                 mDatePaint.setAntiAlias(antiAlias);
362                 mHourPaint.setAntiAlias(antiAlias);
363                 mMinutePaint.setAntiAlias(antiAlias);
364                 mSecondPaint.setAntiAlias(antiAlias);
365                 mAmPmPaint.setAntiAlias(antiAlias);
366                 mColonPaint.setAntiAlias(antiAlias);
367             }
368             invalidate();
369 
370             // Whether the timer should be running depends on whether we're in ambient mode (as well
371             // as whether we're visible), so we may need to start or stop the timer.
372             updateTimer();
373         }
374 
adjustPaintColorToCurrentMode(Paint paint, int interactiveColor, int ambientColor)375         private void adjustPaintColorToCurrentMode(Paint paint, int interactiveColor,
376                                                    int ambientColor) {
377             paint.setColor(isInAmbientMode() ? ambientColor : interactiveColor);
378         }
379 
380         @Override
onInterruptionFilterChanged(int interruptionFilter)381         public void onInterruptionFilterChanged(int interruptionFilter) {
382             if (Log.isLoggable(TAG, Log.DEBUG)) {
383                 Log.d(TAG, "onInterruptionFilterChanged: " + interruptionFilter);
384             }
385             super.onInterruptionFilterChanged(interruptionFilter);
386 
387             boolean inMuteMode = interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE;
388             // We only need to update once a minute in mute mode.
389             setInteractiveUpdateRateMs(inMuteMode ? MUTE_UPDATE_RATE_MS : NORMAL_UPDATE_RATE_MS);
390 
391             if (mMute != inMuteMode) {
392                 mMute = inMuteMode;
393                 int alpha = inMuteMode ? MUTE_ALPHA : NORMAL_ALPHA;
394                 mDatePaint.setAlpha(alpha);
395                 mHourPaint.setAlpha(alpha);
396                 mMinutePaint.setAlpha(alpha);
397                 mColonPaint.setAlpha(alpha);
398                 mAmPmPaint.setAlpha(alpha);
399                 invalidate();
400             }
401         }
402 
setInteractiveUpdateRateMs(long updateRateMs)403         public void setInteractiveUpdateRateMs(long updateRateMs) {
404             if (updateRateMs == mInteractiveUpdateRateMs) {
405                 return;
406             }
407             mInteractiveUpdateRateMs = updateRateMs;
408 
409             // Stop and restart the timer so the new update rate takes effect immediately.
410             if (shouldTimerBeRunning()) {
411                 updateTimer();
412             }
413         }
414 
updatePaintIfInteractive(Paint paint, int interactiveColor)415         private void updatePaintIfInteractive(Paint paint, int interactiveColor) {
416             if (!isInAmbientMode() && paint != null) {
417                 paint.setColor(interactiveColor);
418             }
419         }
420 
setInteractiveBackgroundColor(int color)421         private void setInteractiveBackgroundColor(int color) {
422             mInteractiveBackgroundColor = color;
423             updatePaintIfInteractive(mBackgroundPaint, color);
424         }
425 
setInteractiveHourDigitsColor(int color)426         private void setInteractiveHourDigitsColor(int color) {
427             mInteractiveHourDigitsColor = color;
428             updatePaintIfInteractive(mHourPaint, color);
429         }
430 
setInteractiveMinuteDigitsColor(int color)431         private void setInteractiveMinuteDigitsColor(int color) {
432             mInteractiveMinuteDigitsColor = color;
433             updatePaintIfInteractive(mMinutePaint, color);
434         }
435 
setInteractiveSecondDigitsColor(int color)436         private void setInteractiveSecondDigitsColor(int color) {
437             mInteractiveSecondDigitsColor = color;
438             updatePaintIfInteractive(mSecondPaint, color);
439         }
440 
formatTwoDigitNumber(int hour)441         private String formatTwoDigitNumber(int hour) {
442             return String.format("%02d", hour);
443         }
444 
getAmPmString(int amPm)445         private String getAmPmString(int amPm) {
446             return amPm == Calendar.AM ? mAmString : mPmString;
447         }
448 
449         @Override
onDraw(Canvas canvas, Rect bounds)450         public void onDraw(Canvas canvas, Rect bounds) {
451             long now = System.currentTimeMillis();
452             mCalendar.setTimeInMillis(now);
453             mDate.setTime(now);
454             boolean is24Hour = DateFormat.is24HourFormat(DigitalWatchFaceService.this);
455 
456             // Show colons for the first half of each second so the colons blink on when the time
457             // updates.
458             mShouldDrawColons = (System.currentTimeMillis() % 1000) < 500;
459 
460             // Draw the background.
461             canvas.drawRect(0, 0, bounds.width(), bounds.height(), mBackgroundPaint);
462 
463             // Draw the hours.
464             float x = mXOffset;
465             String hourString;
466             if (is24Hour) {
467                 hourString = formatTwoDigitNumber(mCalendar.get(Calendar.HOUR_OF_DAY));
468             } else {
469                 int hour = mCalendar.get(Calendar.HOUR);
470                 if (hour == 0) {
471                     hour = 12;
472                 }
473                 hourString = String.valueOf(hour);
474             }
475             canvas.drawText(hourString, x, mYOffset, mHourPaint);
476             x += mHourPaint.measureText(hourString);
477 
478             // In ambient and mute modes, always draw the first colon. Otherwise, draw the
479             // first colon for the first half of each second.
480             if (isInAmbientMode() || mMute || mShouldDrawColons) {
481                 canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
482             }
483             x += mColonWidth;
484 
485             // Draw the minutes.
486             String minuteString = formatTwoDigitNumber(mCalendar.get(Calendar.MINUTE));
487             canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
488             x += mMinutePaint.measureText(minuteString);
489 
490             // In unmuted interactive mode, draw a second blinking colon followed by the seconds.
491             // Otherwise, if we're in 12-hour mode, draw AM/PM
492             if (!isInAmbientMode() && !mMute) {
493                 if (mShouldDrawColons) {
494                     canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
495                 }
496                 x += mColonWidth;
497                 canvas.drawText(formatTwoDigitNumber(
498                         mCalendar.get(Calendar.SECOND)), x, mYOffset, mSecondPaint);
499             } else if (!is24Hour) {
500                 x += mColonWidth;
501                 canvas.drawText(getAmPmString(
502                         mCalendar.get(Calendar.AM_PM)), x, mYOffset, mAmPmPaint);
503             }
504 
505             // Only render the day of week and date if there is no peek card, so they do not bleed
506             // into each other in ambient mode.
507             if (getPeekCardPosition().isEmpty()) {
508                 // Day of week
509                 canvas.drawText(
510                         mDayOfWeekFormat.format(mDate),
511                         mXOffset, mYOffset + mLineHeight, mDatePaint);
512                 // Date
513                 canvas.drawText(
514                         mDateFormat.format(mDate),
515                         mXOffset, mYOffset + mLineHeight * 2, mDatePaint);
516             }
517         }
518 
519         /**
520          * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
521          * or stops it if it shouldn't be running but currently is.
522          */
523         private void updateTimer() {
524             if (Log.isLoggable(TAG, Log.DEBUG)) {
525                 Log.d(TAG, "updateTimer");
526             }
527             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
528             if (shouldTimerBeRunning()) {
529                 mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
530             }
531         }
532 
533         /**
534          * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
535          * only run when we're visible and in interactive mode.
536          */
537         private boolean shouldTimerBeRunning() {
538             return isVisible() && !isInAmbientMode();
539         }
540 
541         private void updateConfigDataItemAndUiOnStartup() {
542             DigitalWatchFaceUtil.fetchConfigDataMap(mGoogleApiClient,
543                     new DigitalWatchFaceUtil.FetchConfigDataMapCallback() {
544                         @Override
545                         public void onConfigDataMapFetched(DataMap startupConfig) {
546                             // If the DataItem hasn't been created yet or some keys are missing,
547                             // use the default values.
548                             setDefaultValuesForMissingConfigKeys(startupConfig);
549                             DigitalWatchFaceUtil.putConfigDataItem(mGoogleApiClient, startupConfig);
550 
551                             updateUiForConfigDataMap(startupConfig);
552                         }
553                     }
554             );
555         }
556 
557         private void setDefaultValuesForMissingConfigKeys(DataMap config) {
558             addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_BACKGROUND_COLOR,
559                     DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND);
560             addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_HOURS_COLOR,
561                     DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS);
562             addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_MINUTES_COLOR,
563                     DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS);
564             addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_SECONDS_COLOR,
565                     DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS);
566         }
567 
568         private void addIntKeyIfMissing(DataMap config, String key, int color) {
569             if (!config.containsKey(key)) {
570                 config.putInt(key, color);
571             }
572         }
573 
574         @Override // DataApi.DataListener
575         public void onDataChanged(DataEventBuffer dataEvents) {
576             for (DataEvent dataEvent : dataEvents) {
577                 if (dataEvent.getType() != DataEvent.TYPE_CHANGED) {
578                     continue;
579                 }
580 
581                 DataItem dataItem = dataEvent.getDataItem();
582                 if (!dataItem.getUri().getPath().equals(
583                         DigitalWatchFaceUtil.PATH_WITH_FEATURE)) {
584                     continue;
585                 }
586 
587                 DataMapItem dataMapItem = DataMapItem.fromDataItem(dataItem);
588                 DataMap config = dataMapItem.getDataMap();
589                 if (Log.isLoggable(TAG, Log.DEBUG)) {
590                     Log.d(TAG, "Config DataItem updated:" + config);
591                 }
592                 updateUiForConfigDataMap(config);
593             }
594         }
595 
596         private void updateUiForConfigDataMap(final DataMap config) {
597             boolean uiUpdated = false;
598             for (String configKey : config.keySet()) {
599                 if (!config.containsKey(configKey)) {
600                     continue;
601                 }
602                 int color = config.getInt(configKey);
603                 if (Log.isLoggable(TAG, Log.DEBUG)) {
604                     Log.d(TAG, "Found watch face config key: " + configKey + " -> "
605                             + Integer.toHexString(color));
606                 }
607                 if (updateUiForKey(configKey, color)) {
608                     uiUpdated = true;
609                 }
610             }
611             if (uiUpdated) {
612                 invalidate();
613             }
614         }
615 
616         /**
617          * Updates the color of a UI item according to the given {@code configKey}. Does nothing if
618          * {@code configKey} isn't recognized.
619          *
620          * @return whether UI has been updated
621          */
622         private boolean updateUiForKey(String configKey, int color) {
623             if (configKey.equals(DigitalWatchFaceUtil.KEY_BACKGROUND_COLOR)) {
624                 setInteractiveBackgroundColor(color);
625             } else if (configKey.equals(DigitalWatchFaceUtil.KEY_HOURS_COLOR)) {
626                 setInteractiveHourDigitsColor(color);
627             } else if (configKey.equals(DigitalWatchFaceUtil.KEY_MINUTES_COLOR)) {
628                 setInteractiveMinuteDigitsColor(color);
629             } else if (configKey.equals(DigitalWatchFaceUtil.KEY_SECONDS_COLOR)) {
630                 setInteractiveSecondDigitsColor(color);
631             } else {
632                 Log.w(TAG, "Ignoring unknown config key: " + configKey);
633                 return false;
634             }
635             return true;
636         }
637 
638         @Override  // GoogleApiClient.ConnectionCallbacks
639         public void onConnected(Bundle connectionHint) {
640             if (Log.isLoggable(TAG, Log.DEBUG)) {
641                 Log.d(TAG, "onConnected: " + connectionHint);
642             }
643             Wearable.DataApi.addListener(mGoogleApiClient, Engine.this);
644             updateConfigDataItemAndUiOnStartup();
645         }
646 
647         @Override  // GoogleApiClient.ConnectionCallbacks
648         public void onConnectionSuspended(int cause) {
649             if (Log.isLoggable(TAG, Log.DEBUG)) {
650                 Log.d(TAG, "onConnectionSuspended: " + cause);
651             }
652         }
653 
654         @Override  // GoogleApiClient.OnConnectionFailedListener
655         public void onConnectionFailed(ConnectionResult result) {
656             if (Log.isLoggable(TAG, Log.DEBUG)) {
657                 Log.d(TAG, "onConnectionFailed: " + result);
658             }
659         }
660     }
661 }
662