Home | History | Annotate | Download | only in com.example.android.wearable.watchface
      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
     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
    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
    216         public void onDestroy() {
    217             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
    218             super.onDestroy();
    219         }
    220 
    221         private Paint createTextPaint(int defaultInteractiveColor) {
    222             return createTextPaint(defaultInteractiveColor, NORMAL_TYPEFACE);
    223         }
    224 
    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
    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 
    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 
    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 
    279         private void unregisterReceiver() {
    280             if (!mRegisteredReceiver) {
    281                 return;
    282             }
    283             mRegisteredReceiver = false;
    284             DigitalWatchFaceService.this.unregisterReceiver(mReceiver);
    285         }
    286 
    287         @Override
    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
    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
    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
    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 
    371         private void adjustPaintColorToCurrentMode(Paint paint, int interactiveColor,
    372                                                    int ambientColor) {
    373             paint.setColor(isInAmbientMode() ? ambientColor : interactiveColor);
    374         }
    375 
    376         @Override
    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 
    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 
    411         private void updatePaintIfInteractive(Paint paint, int interactiveColor) {
    412             if (!isInAmbientMode() && paint != null) {
    413                 paint.setColor(interactiveColor);
    414             }
    415         }
    416 
    417         private void setInteractiveBackgroundColor(int color) {
    418             mInteractiveBackgroundColor = color;
    419             updatePaintIfInteractive(mBackgroundPaint, color);
    420         }
    421 
    422         private void setInteractiveHourDigitsColor(int color) {
    423             mInteractiveHourDigitsColor = color;
    424             updatePaintIfInteractive(mHourPaint, color);
    425         }
    426 
    427         private void setInteractiveMinuteDigitsColor(int color) {
    428             mInteractiveMinuteDigitsColor = color;
    429             updatePaintIfInteractive(mMinutePaint, color);
    430         }
    431 
    432         private void setInteractiveSecondDigitsColor(int color) {
    433             mInteractiveSecondDigitsColor = color;
    434             updatePaintIfInteractive(mSecondPaint, color);
    435         }
    436 
    437         private String formatTwoDigitNumber(int hour) {
    438             return String.format("%02d", hour);
    439         }
    440 
    441         private String getAmPmString(int amPm) {
    442             return amPm == Calendar.AM ? mAmString : mPmString;
    443         }
    444 
    445         @Override
    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