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