Home | History | Annotate | Download | only in 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.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
     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
    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
    192         public void onDestroy() {
    193             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
    194             super.onDestroy();
    195         }
    196 
    197         private Paint createTextPaint(int defaultInteractiveColor) {
    198             return createTextPaint(defaultInteractiveColor, NORMAL_TYPEFACE);
    199         }
    200 
    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
    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 
    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 
    247         private void unregisterReceiver() {
    248             if (!mRegisteredTimeZoneReceiver) {
    249                 return;
    250             }
    251             mRegisteredTimeZoneReceiver = false;
    252             DigitalWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
    253         }
    254 
    255         @Override
    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
    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
    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
    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 
    337         private void adjustPaintColorToCurrentMode(Paint paint, int interactiveColor,
    338                 int ambientColor) {
    339             paint.setColor(isInAmbientMode() ? ambientColor : interactiveColor);
    340         }
    341 
    342         @Override
    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 
    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 
    376         private void updatePaintIfInteractive(Paint paint, int interactiveColor) {
    377             if (!isInAmbientMode() && paint != null) {
    378                 paint.setColor(interactiveColor);
    379             }
    380         }
    381 
    382         private void setInteractiveBackgroundColor(int color) {
    383             mInteractiveBackgroundColor = color;
    384             updatePaintIfInteractive(mBackgroundPaint, color);
    385         }
    386 
    387         private void setInteractiveHourDigitsColor(int color) {
    388             mInteractiveHourDigitsColor = color;
    389             updatePaintIfInteractive(mHourPaint, color);
    390         }
    391 
    392         private void setInteractiveMinuteDigitsColor(int color) {
    393             mInteractiveMinuteDigitsColor = color;
    394             updatePaintIfInteractive(mMinutePaint, color);
    395         }
    396 
    397         private void setInteractiveSecondDigitsColor(int color) {
    398             mInteractiveSecondDigitsColor = color;
    399             updatePaintIfInteractive(mSecondPaint, color);
    400         }
    401 
    402         private String formatTwoDigitNumber(int hour) {
    403             return String.format("%02d", hour);
    404         }
    405 
    406         private int convertTo12Hour(int hour) {
    407             int result = hour % 12;
    408             return (result == 0) ? 12 : result;
    409         }
    410 
    411         private String getAmPmString(int hour) {
    412             return (hour < 12) ? mAmString : mPmString;
    413         }
    414 
    415         @Override
    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