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