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