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