/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.wearable.watchface; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.wearable.watchface.CanvasWatchFaceService; import android.support.wearable.watchface.WatchFaceService; import android.support.wearable.watchface.WatchFaceStyle; import android.text.format.DateFormat; import android.util.Log; import android.view.SurfaceHolder; import android.view.WindowInsets; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.DataEvent; import com.google.android.gms.wearable.DataEventBuffer; import com.google.android.gms.wearable.DataItem; import com.google.android.gms.wearable.DataMap; import com.google.android.gms.wearable.DataMapItem; import com.google.android.gms.wearable.Wearable; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.TimeZone; import java.util.concurrent.TimeUnit; /** * Sample digital watch face with blinking colons and seconds. In ambient mode, the seconds are * replaced with an AM/PM indicator and the colons don't blink. On devices with low-bit ambient * mode, the text is drawn without anti-aliasing in ambient mode. On devices which require burn-in * protection, the hours are drawn in normal rather than bold. The time is drawn with less contrast * and without seconds in mute mode. */ public class DigitalWatchFaceService extends CanvasWatchFaceService { private static final String TAG = "DigitalWatchFaceService"; private static final Typeface BOLD_TYPEFACE = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD); private static final Typeface NORMAL_TYPEFACE = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL); /** * Update rate in milliseconds for normal (not ambient and not mute) mode. We update twice * a second to blink the colons. */ private static final long NORMAL_UPDATE_RATE_MS = 500; /** * Update rate in milliseconds for mute mode. We update every minute, like in ambient mode. */ private static final long MUTE_UPDATE_RATE_MS = TimeUnit.MINUTES.toMillis(1); @Override public Engine onCreateEngine() { return new Engine(); } private class Engine extends CanvasWatchFaceService.Engine implements DataApi.DataListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { static final String COLON_STRING = ":"; /** Alpha value for drawing time when in mute mode. */ static final int MUTE_ALPHA = 100; /** Alpha value for drawing time when not in mute mode. */ static final int NORMAL_ALPHA = 255; static final int MSG_UPDATE_TIME = 0; /** How often {@link #mUpdateTimeHandler} ticks in milliseconds. */ long mInteractiveUpdateRateMs = NORMAL_UPDATE_RATE_MS; /** Handler to update the time periodically in interactive mode. */ final Handler mUpdateTimeHandler = new Handler() { @Override public void handleMessage(Message message) { switch (message.what) { case MSG_UPDATE_TIME: if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "updating time"); } invalidate(); if (shouldTimerBeRunning()) { long timeMs = System.currentTimeMillis(); long delayMs = mInteractiveUpdateRateMs - (timeMs % mInteractiveUpdateRateMs); mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs); } break; } } }; GoogleApiClient mGoogleApiClient = new GoogleApiClient.Builder(DigitalWatchFaceService.this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(Wearable.API) .build(); /** * Handles time zone and locale changes. */ final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mCalendar.setTimeZone(TimeZone.getDefault()); initFormats(); invalidate(); } }; /** * Unregistering an unregistered receiver throws an exception. Keep track of the * registration state to prevent that. */ boolean mRegisteredReceiver = false; Paint mBackgroundPaint; Paint mDatePaint; Paint mHourPaint; Paint mMinutePaint; Paint mSecondPaint; Paint mAmPmPaint; Paint mColonPaint; float mColonWidth; boolean mMute; Calendar mCalendar; Date mDate; SimpleDateFormat mDayOfWeekFormat; java.text.DateFormat mDateFormat; boolean mShouldDrawColons; float mXOffset; float mYOffset; float mLineHeight; String mAmString; String mPmString; int mInteractiveBackgroundColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND; int mInteractiveHourDigitsColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS; int mInteractiveMinuteDigitsColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS; int mInteractiveSecondDigitsColor = DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS; /** * Whether the display supports fewer bits for each color in ambient mode. When true, we * disable anti-aliasing in ambient mode. */ boolean mLowBitAmbient; @Override public void onCreate(SurfaceHolder holder) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onCreate"); } super.onCreate(holder); setWatchFaceStyle(new WatchFaceStyle.Builder(DigitalWatchFaceService.this) .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE) .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) .setShowSystemUiTime(false) .build()); Resources resources = DigitalWatchFaceService.this.getResources(); mYOffset = resources.getDimension(R.dimen.digital_y_offset); mLineHeight = resources.getDimension(R.dimen.digital_line_height); mAmString = resources.getString(R.string.digital_am); mPmString = resources.getString(R.string.digital_pm); mBackgroundPaint = new Paint(); mBackgroundPaint.setColor(mInteractiveBackgroundColor); mDatePaint = createTextPaint(resources.getColor(R.color.digital_date)); mHourPaint = createTextPaint(mInteractiveHourDigitsColor, BOLD_TYPEFACE); mMinutePaint = createTextPaint(mInteractiveMinuteDigitsColor); mSecondPaint = createTextPaint(mInteractiveSecondDigitsColor); mAmPmPaint = createTextPaint(resources.getColor(R.color.digital_am_pm)); mColonPaint = createTextPaint(resources.getColor(R.color.digital_colons)); mCalendar = Calendar.getInstance(); mDate = new Date(); initFormats(); } @Override public void onDestroy() { mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); super.onDestroy(); } private Paint createTextPaint(int defaultInteractiveColor) { return createTextPaint(defaultInteractiveColor, NORMAL_TYPEFACE); } private Paint createTextPaint(int defaultInteractiveColor, Typeface typeface) { Paint paint = new Paint(); paint.setColor(defaultInteractiveColor); paint.setTypeface(typeface); paint.setAntiAlias(true); return paint; } @Override public void onVisibilityChanged(boolean visible) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onVisibilityChanged: " + visible); } super.onVisibilityChanged(visible); if (visible) { mGoogleApiClient.connect(); registerReceiver(); // Update time zone and date formats, in case they changed while we weren't visible. mCalendar.setTimeZone(TimeZone.getDefault()); initFormats(); } else { unregisterReceiver(); if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) { Wearable.DataApi.removeListener(mGoogleApiClient, this); mGoogleApiClient.disconnect(); } } // Whether the timer should be running depends on whether we're visible (as well as // whether we're in ambient mode), so we may need to start or stop the timer. updateTimer(); } private void initFormats() { mDayOfWeekFormat = new SimpleDateFormat("EEEE", Locale.getDefault()); mDayOfWeekFormat.setCalendar(mCalendar); mDateFormat = DateFormat.getDateFormat(DigitalWatchFaceService.this); mDateFormat.setCalendar(mCalendar); } private void registerReceiver() { if (mRegisteredReceiver) { return; } mRegisteredReceiver = true; IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); filter.addAction(Intent.ACTION_LOCALE_CHANGED); DigitalWatchFaceService.this.registerReceiver(mReceiver, filter); } private void unregisterReceiver() { if (!mRegisteredReceiver) { return; } mRegisteredReceiver = false; DigitalWatchFaceService.this.unregisterReceiver(mReceiver); } @Override public void onApplyWindowInsets(WindowInsets insets) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square")); } super.onApplyWindowInsets(insets); // Load resources that have alternate values for round watches. Resources resources = DigitalWatchFaceService.this.getResources(); boolean isRound = insets.isRound(); mXOffset = resources.getDimension(isRound ? R.dimen.digital_x_offset_round : R.dimen.digital_x_offset); float textSize = resources.getDimension(isRound ? R.dimen.digital_text_size_round : R.dimen.digital_text_size); float amPmSize = resources.getDimension(isRound ? R.dimen.digital_am_pm_size_round : R.dimen.digital_am_pm_size); mDatePaint.setTextSize(resources.getDimension(R.dimen.digital_date_text_size)); mHourPaint.setTextSize(textSize); mMinutePaint.setTextSize(textSize); mSecondPaint.setTextSize(textSize); mAmPmPaint.setTextSize(amPmSize); mColonPaint.setTextSize(textSize); mColonWidth = mColonPaint.measureText(COLON_STRING); } @Override public void onPropertiesChanged(Bundle properties) { super.onPropertiesChanged(properties); boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false); mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : BOLD_TYPEFACE); mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onPropertiesChanged: burn-in protection = " + burnInProtection + ", low-bit ambient = " + mLowBitAmbient); } } @Override public void onTimeTick() { super.onTimeTick(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode()); } invalidate(); } @Override public void onAmbientModeChanged(boolean inAmbientMode) { super.onAmbientModeChanged(inAmbientMode); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode); } adjustPaintColorToCurrentMode(mBackgroundPaint, mInteractiveBackgroundColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND); adjustPaintColorToCurrentMode(mHourPaint, mInteractiveHourDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS); adjustPaintColorToCurrentMode(mMinutePaint, mInteractiveMinuteDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS); // Actually, the seconds are not rendered in the ambient mode, so we could pass just any // value as ambientColor here. adjustPaintColorToCurrentMode(mSecondPaint, mInteractiveSecondDigitsColor, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS); if (mLowBitAmbient) { boolean antiAlias = !inAmbientMode; mDatePaint.setAntiAlias(antiAlias); mHourPaint.setAntiAlias(antiAlias); mMinutePaint.setAntiAlias(antiAlias); mSecondPaint.setAntiAlias(antiAlias); mAmPmPaint.setAntiAlias(antiAlias); mColonPaint.setAntiAlias(antiAlias); } invalidate(); // Whether the timer should be running depends on whether we're in ambient mode (as well // as whether we're visible), so we may need to start or stop the timer. updateTimer(); } private void adjustPaintColorToCurrentMode(Paint paint, int interactiveColor, int ambientColor) { paint.setColor(isInAmbientMode() ? ambientColor : interactiveColor); } @Override public void onInterruptionFilterChanged(int interruptionFilter) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onInterruptionFilterChanged: " + interruptionFilter); } super.onInterruptionFilterChanged(interruptionFilter); boolean inMuteMode = interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE; // We only need to update once a minute in mute mode. setInteractiveUpdateRateMs(inMuteMode ? MUTE_UPDATE_RATE_MS : NORMAL_UPDATE_RATE_MS); if (mMute != inMuteMode) { mMute = inMuteMode; int alpha = inMuteMode ? MUTE_ALPHA : NORMAL_ALPHA; mDatePaint.setAlpha(alpha); mHourPaint.setAlpha(alpha); mMinutePaint.setAlpha(alpha); mColonPaint.setAlpha(alpha); mAmPmPaint.setAlpha(alpha); invalidate(); } } public void setInteractiveUpdateRateMs(long updateRateMs) { if (updateRateMs == mInteractiveUpdateRateMs) { return; } mInteractiveUpdateRateMs = updateRateMs; // Stop and restart the timer so the new update rate takes effect immediately. if (shouldTimerBeRunning()) { updateTimer(); } } private void updatePaintIfInteractive(Paint paint, int interactiveColor) { if (!isInAmbientMode() && paint != null) { paint.setColor(interactiveColor); } } private void setInteractiveBackgroundColor(int color) { mInteractiveBackgroundColor = color; updatePaintIfInteractive(mBackgroundPaint, color); } private void setInteractiveHourDigitsColor(int color) { mInteractiveHourDigitsColor = color; updatePaintIfInteractive(mHourPaint, color); } private void setInteractiveMinuteDigitsColor(int color) { mInteractiveMinuteDigitsColor = color; updatePaintIfInteractive(mMinutePaint, color); } private void setInteractiveSecondDigitsColor(int color) { mInteractiveSecondDigitsColor = color; updatePaintIfInteractive(mSecondPaint, color); } private String formatTwoDigitNumber(int hour) { return String.format("%02d", hour); } private String getAmPmString(int amPm) { return amPm == Calendar.AM ? mAmString : mPmString; } @Override public void onDraw(Canvas canvas, Rect bounds) { long now = System.currentTimeMillis(); mCalendar.setTimeInMillis(now); mDate.setTime(now); boolean is24Hour = DateFormat.is24HourFormat(DigitalWatchFaceService.this); // Show colons for the first half of each second so the colons blink on when the time // updates. mShouldDrawColons = (System.currentTimeMillis() % 1000) < 500; // Draw the background. canvas.drawRect(0, 0, bounds.width(), bounds.height(), mBackgroundPaint); // Draw the hours. float x = mXOffset; String hourString; if (is24Hour) { hourString = formatTwoDigitNumber(mCalendar.get(Calendar.HOUR_OF_DAY)); } else { int hour = mCalendar.get(Calendar.HOUR); if (hour == 0) { hour = 12; } hourString = String.valueOf(hour); } canvas.drawText(hourString, x, mYOffset, mHourPaint); x += mHourPaint.measureText(hourString); // In ambient and mute modes, always draw the first colon. Otherwise, draw the // first colon for the first half of each second. if (isInAmbientMode() || mMute || mShouldDrawColons) { canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint); } x += mColonWidth; // Draw the minutes. String minuteString = formatTwoDigitNumber(mCalendar.get(Calendar.MINUTE)); canvas.drawText(minuteString, x, mYOffset, mMinutePaint); x += mMinutePaint.measureText(minuteString); // In unmuted interactive mode, draw a second blinking colon followed by the seconds. // Otherwise, if we're in 12-hour mode, draw AM/PM if (!isInAmbientMode() && !mMute) { if (mShouldDrawColons) { canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint); } x += mColonWidth; canvas.drawText(formatTwoDigitNumber( mCalendar.get(Calendar.SECOND)), x, mYOffset, mSecondPaint); } else if (!is24Hour) { x += mColonWidth; canvas.drawText(getAmPmString( mCalendar.get(Calendar.AM_PM)), x, mYOffset, mAmPmPaint); } // Only render the day of week and date if there is no peek card, so they do not bleed // into each other in ambient mode. if (getPeekCardPosition().isEmpty()) { // Day of week canvas.drawText( mDayOfWeekFormat.format(mDate), mXOffset, mYOffset + mLineHeight, mDatePaint); // Date canvas.drawText( mDateFormat.format(mDate), mXOffset, mYOffset + mLineHeight * 2, mDatePaint); } } /** * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently * or stops it if it shouldn't be running but currently is. */ private void updateTimer() { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "updateTimer"); } mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); if (shouldTimerBeRunning()) { mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME); } } /** * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should * only run when we're visible and in interactive mode. */ private boolean shouldTimerBeRunning() { return isVisible() && !isInAmbientMode(); } private void updateConfigDataItemAndUiOnStartup() { DigitalWatchFaceUtil.fetchConfigDataMap(mGoogleApiClient, new DigitalWatchFaceUtil.FetchConfigDataMapCallback() { @Override public void onConfigDataMapFetched(DataMap startupConfig) { // If the DataItem hasn't been created yet or some keys are missing, // use the default values. setDefaultValuesForMissingConfigKeys(startupConfig); DigitalWatchFaceUtil.putConfigDataItem(mGoogleApiClient, startupConfig); updateUiForConfigDataMap(startupConfig); } } ); } private void setDefaultValuesForMissingConfigKeys(DataMap config) { addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_BACKGROUND_COLOR, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND); addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_HOURS_COLOR, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS); addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_MINUTES_COLOR, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS); addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_SECONDS_COLOR, DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS); } private void addIntKeyIfMissing(DataMap config, String key, int color) { if (!config.containsKey(key)) { config.putInt(key, color); } } @Override // DataApi.DataListener public void onDataChanged(DataEventBuffer dataEvents) { for (DataEvent dataEvent : dataEvents) { if (dataEvent.getType() != DataEvent.TYPE_CHANGED) { continue; } DataItem dataItem = dataEvent.getDataItem(); if (!dataItem.getUri().getPath().equals( DigitalWatchFaceUtil.PATH_WITH_FEATURE)) { continue; } DataMapItem dataMapItem = DataMapItem.fromDataItem(dataItem); DataMap config = dataMapItem.getDataMap(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Config DataItem updated:" + config); } updateUiForConfigDataMap(config); } } private void updateUiForConfigDataMap(final DataMap config) { boolean uiUpdated = false; for (String configKey : config.keySet()) { if (!config.containsKey(configKey)) { continue; } int color = config.getInt(configKey); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Found watch face config key: " + configKey + " -> " + Integer.toHexString(color)); } if (updateUiForKey(configKey, color)) { uiUpdated = true; } } if (uiUpdated) { invalidate(); } } /** * Updates the color of a UI item according to the given {@code configKey}. Does nothing if * {@code configKey} isn't recognized. * * @return whether UI has been updated */ private boolean updateUiForKey(String configKey, int color) { if (configKey.equals(DigitalWatchFaceUtil.KEY_BACKGROUND_COLOR)) { setInteractiveBackgroundColor(color); } else if (configKey.equals(DigitalWatchFaceUtil.KEY_HOURS_COLOR)) { setInteractiveHourDigitsColor(color); } else if (configKey.equals(DigitalWatchFaceUtil.KEY_MINUTES_COLOR)) { setInteractiveMinuteDigitsColor(color); } else if (configKey.equals(DigitalWatchFaceUtil.KEY_SECONDS_COLOR)) { setInteractiveSecondDigitsColor(color); } else { Log.w(TAG, "Ignoring unknown config key: " + configKey); return false; } return true; } @Override // GoogleApiClient.ConnectionCallbacks public void onConnected(Bundle connectionHint) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onConnected: " + connectionHint); } Wearable.DataApi.addListener(mGoogleApiClient, Engine.this); updateConfigDataItemAndUiOnStartup(); } @Override // GoogleApiClient.ConnectionCallbacks public void onConnectionSuspended(int cause) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onConnectionSuspended: " + cause); } } @Override // GoogleApiClient.OnConnectionFailedListener public void onConnectionFailed(ConnectionResult result) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onConnectionFailed: " + result); } } } }