1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.wearable.watchface.watchface;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.content.SharedPreferences;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.Message;
31 import android.support.wearable.complications.ComplicationData;
32 import android.support.wearable.complications.rendering.ComplicationDrawable;
33 import android.support.wearable.watchface.CanvasWatchFaceService;
34 import android.support.wearable.watchface.WatchFaceService;
35 import android.support.wearable.watchface.WatchFaceStyle;
36 import android.util.Log;
37 import android.util.SparseArray;
38 import android.view.SurfaceHolder;
39 import com.example.android.wearable.watchface.R;
40 import com.example.android.wearable.watchface.config.AnalogComplicationConfigRecyclerViewAdapter;
41 import java.util.Calendar;
42 import java.util.TimeZone;
43 import java.util.concurrent.TimeUnit;
44 
45 /** Demonstrates two simple complications in a watch face. */
46 public class AnalogComplicationWatchFaceService extends CanvasWatchFaceService {
47     private static final String TAG = "AnalogWatchFace";
48 
49     // Unique IDs for each complication. The settings activity that supports allowing users
50     // to select their complication data provider requires numbers to be >= 0.
51     private static final int BACKGROUND_COMPLICATION_ID = 0;
52 
53     private static final int LEFT_COMPLICATION_ID = 100;
54     private static final int RIGHT_COMPLICATION_ID = 101;
55 
56     // Background, Left and right complication IDs as array for Complication API.
57     private static final int[] COMPLICATION_IDS = {
58         BACKGROUND_COMPLICATION_ID, LEFT_COMPLICATION_ID, RIGHT_COMPLICATION_ID
59     };
60 
61     // Left and right dial supported types.
62     private static final int[][] COMPLICATION_SUPPORTED_TYPES = {
63         {ComplicationData.TYPE_LARGE_IMAGE},
64         {
65             ComplicationData.TYPE_RANGED_VALUE,
66             ComplicationData.TYPE_ICON,
67             ComplicationData.TYPE_SHORT_TEXT,
68             ComplicationData.TYPE_SMALL_IMAGE
69         },
70         {
71             ComplicationData.TYPE_RANGED_VALUE,
72             ComplicationData.TYPE_ICON,
73             ComplicationData.TYPE_SHORT_TEXT,
74             ComplicationData.TYPE_SMALL_IMAGE
75         }
76     };
77 
78     // Used by {@link AnalogComplicationConfigRecyclerViewAdapter} to check if complication location
79     // is supported in settings config activity.
getComplicationId( AnalogComplicationConfigRecyclerViewAdapter.ComplicationLocation complicationLocation)80     public static int getComplicationId(
81             AnalogComplicationConfigRecyclerViewAdapter.ComplicationLocation complicationLocation) {
82         // Add any other supported locations here.
83         switch (complicationLocation) {
84             case BACKGROUND:
85                 return BACKGROUND_COMPLICATION_ID;
86             case LEFT:
87                 return LEFT_COMPLICATION_ID;
88             case RIGHT:
89                 return RIGHT_COMPLICATION_ID;
90             default:
91                 return -1;
92         }
93     }
94 
95     // Used by {@link AnalogComplicationConfigRecyclerViewAdapter} to retrieve all complication ids.
getComplicationIds()96     public static int[] getComplicationIds() {
97         return COMPLICATION_IDS;
98     }
99 
100     // Used by {@link AnalogComplicationConfigRecyclerViewAdapter} to see which complication types
101     // are supported in the settings config activity.
getSupportedComplicationTypes( AnalogComplicationConfigRecyclerViewAdapter.ComplicationLocation complicationLocation)102     public static int[] getSupportedComplicationTypes(
103             AnalogComplicationConfigRecyclerViewAdapter.ComplicationLocation complicationLocation) {
104         // Add any other supported locations here.
105         switch (complicationLocation) {
106             case BACKGROUND:
107                 return COMPLICATION_SUPPORTED_TYPES[0];
108             case LEFT:
109                 return COMPLICATION_SUPPORTED_TYPES[1];
110             case RIGHT:
111                 return COMPLICATION_SUPPORTED_TYPES[2];
112             default:
113                 return new int[] {};
114         }
115     }
116 
117     /*
118      * Update rate in milliseconds for interactive mode. We update once a second to advance the
119      * second hand.
120      */
121     private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
122 
123     @Override
onCreateEngine()124     public Engine onCreateEngine() {
125         return new Engine();
126     }
127 
128     private class Engine extends CanvasWatchFaceService.Engine {
129         private static final int MSG_UPDATE_TIME = 0;
130 
131         private static final float HOUR_STROKE_WIDTH = 5f;
132         private static final float MINUTE_STROKE_WIDTH = 3f;
133         private static final float SECOND_TICK_STROKE_WIDTH = 2f;
134 
135         private static final float CENTER_GAP_AND_CIRCLE_RADIUS = 4f;
136 
137         private static final int SHADOW_RADIUS = 6;
138 
139         private Calendar mCalendar;
140         private boolean mRegisteredTimeZoneReceiver = false;
141         private boolean mMuteMode;
142 
143         private float mCenterX;
144         private float mCenterY;
145 
146         private float mSecondHandLength;
147         private float mMinuteHandLength;
148         private float mHourHandLength;
149 
150         // Colors for all hands (hour, minute, seconds, ticks) based on photo loaded.
151         private int mWatchHandAndComplicationsColor;
152         private int mWatchHandHighlightColor;
153         private int mWatchHandShadowColor;
154 
155         private int mBackgroundColor;
156 
157         private Paint mHourPaint;
158         private Paint mMinutePaint;
159         private Paint mSecondAndHighlightPaint;
160         private Paint mTickAndCirclePaint;
161 
162         private Paint mBackgroundPaint;
163 
164         /* Maps active complication ids to the data for that complication. Note: Data will only be
165          * present if the user has chosen a provider via the settings activity for the watch face.
166          */
167         private SparseArray<ComplicationData> mActiveComplicationDataSparseArray;
168 
169         /* Maps complication ids to corresponding ComplicationDrawable that renders the
170          * the complication data on the watch face.
171          */
172         private SparseArray<ComplicationDrawable> mComplicationDrawableSparseArray;
173 
174         private boolean mAmbient;
175         private boolean mLowBitAmbient;
176         private boolean mBurnInProtection;
177 
178         // Used to pull user's preferences for background color, highlight color, and visual
179         // indicating there are unread notifications.
180         SharedPreferences mSharedPref;
181 
182         // User's preference for if they want visual shown to indicate unread notifications.
183         private boolean mUnreadNotificationsPreference;
184         private int mNumberOfUnreadNotifications = 0;
185 
186         private final BroadcastReceiver mTimeZoneReceiver =
187                 new BroadcastReceiver() {
188                     @Override
189                     public void onReceive(Context context, Intent intent) {
190                         mCalendar.setTimeZone(TimeZone.getDefault());
191                         invalidate();
192                     }
193                 };
194 
195         // Handler to update the time once a second in interactive mode.
196         private final Handler mUpdateTimeHandler =
197                 new Handler() {
198                     @Override
199                     public void handleMessage(Message message) {
200                         invalidate();
201                         if (shouldTimerBeRunning()) {
202                             long timeMs = System.currentTimeMillis();
203                             long delayMs =
204                                     INTERACTIVE_UPDATE_RATE_MS
205                                             - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
206                             mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
207                         }
208                     }
209                 };
210 
211         @Override
onCreate(SurfaceHolder holder)212         public void onCreate(SurfaceHolder holder) {
213             Log.d(TAG, "onCreate");
214 
215             super.onCreate(holder);
216 
217             // Used throughout watch face to pull user's preferences.
218             Context context = getApplicationContext();
219             mSharedPref =
220                     context.getSharedPreferences(
221                             getString(R.string.analog_complication_preference_file_key),
222                             Context.MODE_PRIVATE);
223 
224             mCalendar = Calendar.getInstance();
225 
226             setWatchFaceStyle(
227                     new WatchFaceStyle.Builder(AnalogComplicationWatchFaceService.this)
228                             .setAcceptsTapEvents(true)
229                             .setHideNotificationIndicator(true)
230                             .build());
231 
232             loadSavedPreferences();
233             initializeComplicationsAndBackground();
234             initializeWatchFace();
235         }
236 
237         // Pulls all user's preferences for watch face appearance.
loadSavedPreferences()238         private void loadSavedPreferences() {
239 
240             String backgroundColorResourceName =
241                     getApplicationContext().getString(R.string.saved_background_color);
242 
243             mBackgroundColor = mSharedPref.getInt(backgroundColorResourceName, Color.BLACK);
244 
245             String markerColorResourceName =
246                     getApplicationContext().getString(R.string.saved_marker_color);
247 
248             // Set defaults for colors
249             mWatchHandHighlightColor = mSharedPref.getInt(markerColorResourceName, Color.RED);
250 
251             if (mBackgroundColor == Color.WHITE) {
252                 mWatchHandAndComplicationsColor = Color.BLACK;
253                 mWatchHandShadowColor = Color.WHITE;
254             } else {
255                 mWatchHandAndComplicationsColor = Color.WHITE;
256                 mWatchHandShadowColor = Color.BLACK;
257             }
258 
259             String unreadNotificationPreferenceResourceName =
260                     getApplicationContext().getString(R.string.saved_unread_notifications_pref);
261 
262             mUnreadNotificationsPreference =
263                     mSharedPref.getBoolean(unreadNotificationPreferenceResourceName, true);
264         }
265 
initializeComplicationsAndBackground()266         private void initializeComplicationsAndBackground() {
267             Log.d(TAG, "initializeComplications()");
268 
269             // Initialize background color (in case background complication is inactive).
270             mBackgroundPaint = new Paint();
271             mBackgroundPaint.setColor(mBackgroundColor);
272 
273             mActiveComplicationDataSparseArray = new SparseArray<>(COMPLICATION_IDS.length);
274 
275             // Creates a ComplicationDrawable for each location where the user can render a
276             // complication on the watch face. In this watch face, we create one for left, right,
277             // and background, but you could add many more.
278             ComplicationDrawable leftComplicationDrawable =
279                     new ComplicationDrawable(getApplicationContext());
280 
281             ComplicationDrawable rightComplicationDrawable =
282                     new ComplicationDrawable(getApplicationContext());
283 
284             ComplicationDrawable backgroundComplicationDrawable =
285                     new ComplicationDrawable(getApplicationContext());
286 
287             // Adds new complications to a SparseArray to simplify setting styles and ambient
288             // properties for all complications, i.e., iterate over them all.
289             mComplicationDrawableSparseArray = new SparseArray<>(COMPLICATION_IDS.length);
290 
291             mComplicationDrawableSparseArray.put(LEFT_COMPLICATION_ID, leftComplicationDrawable);
292             mComplicationDrawableSparseArray.put(RIGHT_COMPLICATION_ID, rightComplicationDrawable);
293             mComplicationDrawableSparseArray.put(
294                     BACKGROUND_COMPLICATION_ID, backgroundComplicationDrawable);
295 
296             setComplicationsActiveAndAmbientColors(mWatchHandHighlightColor);
297             setActiveComplications(COMPLICATION_IDS);
298         }
299 
initializeWatchFace()300         private void initializeWatchFace() {
301 
302             mHourPaint = new Paint();
303             mHourPaint.setColor(mWatchHandAndComplicationsColor);
304             mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH);
305             mHourPaint.setAntiAlias(true);
306             mHourPaint.setStrokeCap(Paint.Cap.ROUND);
307             mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
308 
309             mMinutePaint = new Paint();
310             mMinutePaint.setColor(mWatchHandAndComplicationsColor);
311             mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH);
312             mMinutePaint.setAntiAlias(true);
313             mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
314             mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
315 
316             mSecondAndHighlightPaint = new Paint();
317             mSecondAndHighlightPaint.setColor(mWatchHandHighlightColor);
318             mSecondAndHighlightPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
319             mSecondAndHighlightPaint.setAntiAlias(true);
320             mSecondAndHighlightPaint.setStrokeCap(Paint.Cap.ROUND);
321             mSecondAndHighlightPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
322 
323             mTickAndCirclePaint = new Paint();
324             mTickAndCirclePaint.setColor(mWatchHandAndComplicationsColor);
325             mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
326             mTickAndCirclePaint.setAntiAlias(true);
327             mTickAndCirclePaint.setStyle(Paint.Style.STROKE);
328             mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
329         }
330 
331         /* Sets active/ambient mode colors for all complications.
332          *
333          * Note: With the rest of the watch face, we update the paint colors based on
334          * ambient/active mode callbacks, but because the ComplicationDrawable handles
335          * the active/ambient colors, we only set the colors twice. Once at initialization and
336          * again if the user changes the highlight color via AnalogComplicationConfigActivity.
337          */
setComplicationsActiveAndAmbientColors(int primaryComplicationColor)338         private void setComplicationsActiveAndAmbientColors(int primaryComplicationColor) {
339             int complicationId;
340             ComplicationDrawable complicationDrawable;
341 
342             for (int i = 0; i < COMPLICATION_IDS.length; i++) {
343                 complicationId = COMPLICATION_IDS[i];
344                 complicationDrawable = mComplicationDrawableSparseArray.get(complicationId);
345 
346                 if (complicationId == BACKGROUND_COMPLICATION_ID) {
347                     // It helps for the background color to be black in case the image used for the
348                     // watch face's background takes some time to load.
349                     complicationDrawable.setBackgroundColorActive(Color.BLACK);
350                 } else {
351                     // Active mode colors.
352                     complicationDrawable.setBorderColorActive(primaryComplicationColor);
353                     complicationDrawable.setRangedValuePrimaryColorActive(primaryComplicationColor);
354 
355                     // Ambient mode colors.
356                     complicationDrawable.setBorderColorAmbient(Color.WHITE);
357                     complicationDrawable.setRangedValuePrimaryColorAmbient(Color.WHITE);
358                 }
359             }
360         }
361 
362         @Override
onDestroy()363         public void onDestroy() {
364             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
365             super.onDestroy();
366         }
367 
368         @Override
onPropertiesChanged(Bundle properties)369         public void onPropertiesChanged(Bundle properties) {
370             super.onPropertiesChanged(properties);
371             Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
372 
373             mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
374             mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
375 
376             // Updates complications to properly render in ambient mode based on the
377             // screen's capabilities.
378             ComplicationDrawable complicationDrawable;
379 
380             for (int i = 0; i < COMPLICATION_IDS.length; i++) {
381                 complicationDrawable = mComplicationDrawableSparseArray.get(COMPLICATION_IDS[i]);
382 
383                 complicationDrawable.setLowBitAmbient(mLowBitAmbient);
384                 complicationDrawable.setBurnInProtection(mBurnInProtection);
385             }
386         }
387 
388         /*
389          * Called when there is updated data for a complication id.
390          */
391         @Override
onComplicationDataUpdate( int complicationId, ComplicationData complicationData)392         public void onComplicationDataUpdate(
393                 int complicationId, ComplicationData complicationData) {
394             Log.d(TAG, "onComplicationDataUpdate() id: " + complicationId);
395 
396             // Adds/updates active complication data in the array.
397             mActiveComplicationDataSparseArray.put(complicationId, complicationData);
398 
399             // Updates correct ComplicationDrawable with updated data.
400             ComplicationDrawable complicationDrawable =
401                     mComplicationDrawableSparseArray.get(complicationId);
402             complicationDrawable.setComplicationData(complicationData);
403 
404             invalidate();
405         }
406 
407         @Override
onTapCommand(int tapType, int x, int y, long eventTime)408         public void onTapCommand(int tapType, int x, int y, long eventTime) {
409             Log.d(TAG, "OnTapCommand()");
410             switch (tapType) {
411                 case TAP_TYPE_TAP:
412 
413                     // If your background complication is the first item in your array, you need
414                     // to walk backward through the array to make sure the tap isn't for a
415                     // complication above the background complication.
416                     for (int i = COMPLICATION_IDS.length - 1; i >= 0; i--) {
417                         int complicationId = COMPLICATION_IDS[i];
418                         ComplicationDrawable complicationDrawable =
419                                 mComplicationDrawableSparseArray.get(complicationId);
420 
421                         boolean successfulTap = complicationDrawable.onTap(x, y);
422 
423                         if (successfulTap) {
424                             return;
425                         }
426                     }
427                     break;
428             }
429         }
430 
431         @Override
onTimeTick()432         public void onTimeTick() {
433             super.onTimeTick();
434             invalidate();
435         }
436 
437         @Override
onAmbientModeChanged(boolean inAmbientMode)438         public void onAmbientModeChanged(boolean inAmbientMode) {
439             super.onAmbientModeChanged(inAmbientMode);
440             Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
441 
442             mAmbient = inAmbientMode;
443 
444             updateWatchPaintStyles();
445 
446             // Update drawable complications' ambient state.
447             // Note: ComplicationDrawable handles switching between active/ambient colors, we just
448             // have to inform it to enter ambient mode.
449             ComplicationDrawable complicationDrawable;
450 
451             for (int i = 0; i < COMPLICATION_IDS.length; i++) {
452                 complicationDrawable = mComplicationDrawableSparseArray.get(COMPLICATION_IDS[i]);
453                 complicationDrawable.setInAmbientMode(mAmbient);
454             }
455 
456             // Check and trigger whether or not timer should be running (only in active mode).
457             updateTimer();
458         }
459 
updateWatchPaintStyles()460         private void updateWatchPaintStyles() {
461             if (mAmbient) {
462 
463                 mBackgroundPaint.setColor(Color.BLACK);
464 
465                 mHourPaint.setColor(Color.WHITE);
466                 mMinutePaint.setColor(Color.WHITE);
467                 mSecondAndHighlightPaint.setColor(Color.WHITE);
468                 mTickAndCirclePaint.setColor(Color.WHITE);
469 
470                 mHourPaint.setAntiAlias(false);
471                 mMinutePaint.setAntiAlias(false);
472                 mSecondAndHighlightPaint.setAntiAlias(false);
473                 mTickAndCirclePaint.setAntiAlias(false);
474 
475                 mHourPaint.clearShadowLayer();
476                 mMinutePaint.clearShadowLayer();
477                 mSecondAndHighlightPaint.clearShadowLayer();
478                 mTickAndCirclePaint.clearShadowLayer();
479 
480             } else {
481 
482                 mBackgroundPaint.setColor(mBackgroundColor);
483 
484                 mHourPaint.setColor(mWatchHandAndComplicationsColor);
485                 mMinutePaint.setColor(mWatchHandAndComplicationsColor);
486                 mTickAndCirclePaint.setColor(mWatchHandAndComplicationsColor);
487 
488                 mSecondAndHighlightPaint.setColor(mWatchHandHighlightColor);
489 
490                 mHourPaint.setAntiAlias(true);
491                 mMinutePaint.setAntiAlias(true);
492                 mSecondAndHighlightPaint.setAntiAlias(true);
493                 mTickAndCirclePaint.setAntiAlias(true);
494 
495                 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
496                 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
497                 mSecondAndHighlightPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
498                 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
499             }
500         }
501 
502         @Override
onInterruptionFilterChanged(int interruptionFilter)503         public void onInterruptionFilterChanged(int interruptionFilter) {
504             super.onInterruptionFilterChanged(interruptionFilter);
505             boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
506 
507             /* Dim display in mute mode. */
508             if (mMuteMode != inMuteMode) {
509                 mMuteMode = inMuteMode;
510                 mHourPaint.setAlpha(inMuteMode ? 100 : 255);
511                 mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
512                 mSecondAndHighlightPaint.setAlpha(inMuteMode ? 80 : 255);
513                 invalidate();
514             }
515         }
516 
517         @Override
onSurfaceChanged(SurfaceHolder holder, int format, int width, int height)518         public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
519             super.onSurfaceChanged(holder, format, width, height);
520 
521             /*
522              * Find the coordinates of the center point on the screen, and ignore the window
523              * insets, so that, on round watches with a "chin", the watch face is centered on the
524              * entire screen, not just the usable portion.
525              */
526             mCenterX = width / 2f;
527             mCenterY = height / 2f;
528 
529             /*
530              * Calculate lengths of different hands based on watch screen size.
531              */
532             mSecondHandLength = (float) (mCenterX * 0.875);
533             mMinuteHandLength = (float) (mCenterX * 0.75);
534             mHourHandLength = (float) (mCenterX * 0.5);
535 
536             /*
537              * Calculates location bounds for right and left circular complications. Please note,
538              * we are not demonstrating a long text complication in this watch face.
539              *
540              * We suggest using at least 1/4 of the screen width for circular (or squared)
541              * complications and 2/3 of the screen width for wide rectangular complications for
542              * better readability.
543              */
544 
545             // For most Wear devices, width and height are the same, so we just chose one (width).
546             int sizeOfComplication = width / 4;
547             int midpointOfScreen = width / 2;
548 
549             int horizontalOffset = (midpointOfScreen - sizeOfComplication) / 2;
550             int verticalOffset = midpointOfScreen - (sizeOfComplication / 2);
551 
552             Rect leftBounds =
553                     // Left, Top, Right, Bottom
554                     new Rect(
555                             horizontalOffset,
556                             verticalOffset,
557                             (horizontalOffset + sizeOfComplication),
558                             (verticalOffset + sizeOfComplication));
559 
560             ComplicationDrawable leftComplicationDrawable =
561                     mComplicationDrawableSparseArray.get(LEFT_COMPLICATION_ID);
562             leftComplicationDrawable.setBounds(leftBounds);
563 
564             Rect rightBounds =
565                     // Left, Top, Right, Bottom
566                     new Rect(
567                             (midpointOfScreen + horizontalOffset),
568                             verticalOffset,
569                             (midpointOfScreen + horizontalOffset + sizeOfComplication),
570                             (verticalOffset + sizeOfComplication));
571 
572             ComplicationDrawable rightComplicationDrawable =
573                     mComplicationDrawableSparseArray.get(RIGHT_COMPLICATION_ID);
574             rightComplicationDrawable.setBounds(rightBounds);
575 
576             Rect screenForBackgroundBound =
577                     // Left, Top, Right, Bottom
578                     new Rect(0, 0, width, height);
579 
580             ComplicationDrawable backgroundComplicationDrawable =
581                     mComplicationDrawableSparseArray.get(BACKGROUND_COMPLICATION_ID);
582             backgroundComplicationDrawable.setBounds(screenForBackgroundBound);
583         }
584 
585         @Override
onDraw(Canvas canvas, Rect bounds)586         public void onDraw(Canvas canvas, Rect bounds) {
587             long now = System.currentTimeMillis();
588             mCalendar.setTimeInMillis(now);
589 
590             drawBackground(canvas);
591             drawComplications(canvas, now);
592             drawUnreadNotificationIcon(canvas);
593             drawWatchFace(canvas);
594         }
595 
drawUnreadNotificationIcon(Canvas canvas)596         private void drawUnreadNotificationIcon(Canvas canvas) {
597 
598             if (mUnreadNotificationsPreference && (mNumberOfUnreadNotifications > 0)) {
599 
600                 int width = canvas.getWidth();
601                 int height = canvas.getHeight();
602 
603                 canvas.drawCircle(width / 2, height - 40, 10, mTickAndCirclePaint);
604 
605                 /*
606                  * Ensure center highlight circle is only drawn in interactive mode. This ensures
607                  * we don't burn the screen with a solid circle in ambient mode.
608                  */
609                 if (!mAmbient) {
610                     canvas.drawCircle(width / 2, height - 40, 4, mSecondAndHighlightPaint);
611                 }
612             }
613         }
614 
drawBackground(Canvas canvas)615         private void drawBackground(Canvas canvas) {
616 
617             if (mAmbient && (mLowBitAmbient || mBurnInProtection)) {
618                 canvas.drawColor(Color.BLACK);
619 
620             } else {
621                 canvas.drawColor(mBackgroundColor);
622             }
623         }
624 
drawComplications(Canvas canvas, long currentTimeMillis)625         private void drawComplications(Canvas canvas, long currentTimeMillis) {
626             int complicationId;
627             ComplicationDrawable complicationDrawable;
628 
629             for (int i = 0; i < COMPLICATION_IDS.length; i++) {
630                 complicationId = COMPLICATION_IDS[i];
631                 complicationDrawable = mComplicationDrawableSparseArray.get(complicationId);
632 
633                 complicationDrawable.draw(canvas, currentTimeMillis);
634             }
635         }
636 
drawWatchFace(Canvas canvas)637         private void drawWatchFace(Canvas canvas) {
638             /*
639              * Draw ticks. Usually you will want to bake this directly into the photo, but in
640              * cases where you want to allow users to select their own photos, this dynamically
641              * creates them on top of the photo.
642              */
643             float innerTickRadius = mCenterX - 10;
644             float outerTickRadius = mCenterX;
645             for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
646                 float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
647                 float innerX = (float) Math.sin(tickRot) * innerTickRadius;
648                 float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
649                 float outerX = (float) Math.sin(tickRot) * outerTickRadius;
650                 float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
651                 canvas.drawLine(
652                         mCenterX + innerX,
653                         mCenterY + innerY,
654                         mCenterX + outerX,
655                         mCenterY + outerY,
656                         mTickAndCirclePaint);
657             }
658 
659             /*
660              * These calculations reflect the rotation in degrees per unit of time, e.g.,
661              * 360 / 60 = 6 and 360 / 12 = 30.
662              */
663             final float seconds =
664                     (mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f);
665             final float secondsRotation = seconds * 6f;
666 
667             final float minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f;
668 
669             final float hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f;
670             final float hoursRotation = (mCalendar.get(Calendar.HOUR) * 30) + hourHandOffset;
671 
672             /*
673              * Save the canvas state before we can begin to rotate it.
674              */
675             canvas.save();
676 
677             canvas.rotate(hoursRotation, mCenterX, mCenterY);
678             canvas.drawLine(
679                     mCenterX,
680                     mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
681                     mCenterX,
682                     mCenterY - mHourHandLength,
683                     mHourPaint);
684 
685             canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY);
686             canvas.drawLine(
687                     mCenterX,
688                     mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
689                     mCenterX,
690                     mCenterY - mMinuteHandLength,
691                     mMinutePaint);
692 
693             /*
694              * Ensure the "seconds" hand is drawn only when we are in interactive mode.
695              * Otherwise, we only update the watch face once a minute.
696              */
697             if (!mAmbient) {
698                 canvas.rotate(secondsRotation - minutesRotation, mCenterX, mCenterY);
699                 canvas.drawLine(
700                         mCenterX,
701                         mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
702                         mCenterX,
703                         mCenterY - mSecondHandLength,
704                         mSecondAndHighlightPaint);
705             }
706             canvas.drawCircle(
707                     mCenterX, mCenterY, CENTER_GAP_AND_CIRCLE_RADIUS, mTickAndCirclePaint);
708 
709             /* Restore the canvas' original orientation. */
710             canvas.restore();
711         }
712 
713         @Override
onVisibilityChanged(boolean visible)714         public void onVisibilityChanged(boolean visible) {
715             super.onVisibilityChanged(visible);
716 
717             if (visible) {
718 
719                 // Preferences might have changed since last time watch face was visible.
720                 loadSavedPreferences();
721 
722                 // With the rest of the watch face, we update the paint colors based on
723                 // ambient/active mode callbacks, but because the ComplicationDrawable handles
724                 // the active/ambient colors, we only need to update the complications' colors when
725                 // the user actually makes a change to the highlight color, not when the watch goes
726                 // in and out of ambient mode.
727                 setComplicationsActiveAndAmbientColors(mWatchHandHighlightColor);
728                 updateWatchPaintStyles();
729 
730                 registerReceiver();
731                 // Update time zone in case it changed while we weren't visible.
732                 mCalendar.setTimeZone(TimeZone.getDefault());
733                 invalidate();
734             } else {
735                 unregisterReceiver();
736             }
737 
738             /* Check and trigger whether or not timer should be running (only in active mode). */
739             updateTimer();
740         }
741 
742         @Override
onUnreadCountChanged(int count)743         public void onUnreadCountChanged(int count) {
744             Log.d(TAG, "onUnreadCountChanged(): " + count);
745 
746             if (mUnreadNotificationsPreference) {
747 
748                 if (mNumberOfUnreadNotifications != count) {
749                     mNumberOfUnreadNotifications = count;
750                     invalidate();
751                 }
752             }
753         }
754 
registerReceiver()755         private void registerReceiver() {
756             if (mRegisteredTimeZoneReceiver) {
757                 return;
758             }
759             mRegisteredTimeZoneReceiver = true;
760             IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
761             AnalogComplicationWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
762         }
763 
unregisterReceiver()764         private void unregisterReceiver() {
765             if (!mRegisteredTimeZoneReceiver) {
766                 return;
767             }
768             mRegisteredTimeZoneReceiver = false;
769             AnalogComplicationWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
770         }
771 
772         /**
773          * Starts/stops the {@link #mUpdateTimeHandler} timer based on the state of the watch face.
774          */
updateTimer()775         private void updateTimer() {
776             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
777             if (shouldTimerBeRunning()) {
778                 mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
779             }
780         }
781 
782         /**
783          * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
784          * only run in active mode.
785          */
shouldTimerBeRunning()786         private boolean shouldTimerBeRunning() {
787             return isVisible() && !mAmbient;
788         }
789     }
790 }
791