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.app.PendingIntent;
20 import android.content.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapFactory;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.ColorMatrix;
29 import android.graphics.ColorMatrixColorFilter;
30 import android.graphics.Paint;
31 import android.graphics.Rect;
32 import android.graphics.Typeface;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.Message;
36 import android.support.v7.graphics.Palette;
37 import android.support.wearable.complications.ComplicationData;
38 import android.support.wearable.complications.ComplicationText;
39 import android.support.wearable.watchface.CanvasWatchFaceService;
40 import android.support.wearable.watchface.WatchFaceService;
41 import android.support.wearable.watchface.WatchFaceStyle;
42 import android.text.TextUtils;
43 import android.util.Log;
44 import android.util.SparseArray;
45 import android.view.SurfaceHolder;
46 
47 import java.util.Calendar;
48 import java.util.TimeZone;
49 import java.util.concurrent.TimeUnit;
50 
51 /**
52  * Demonstrates two simple complications in a watch face.
53  */
54 public class ComplicationSimpleWatchFaceService extends CanvasWatchFaceService {
55     private static final String TAG = "SimpleComplicationWF";
56 
57     // Unique IDs for each complication.
58     private static final int LEFT_DIAL_COMPLICATION = 0;
59     private static final int RIGHT_DIAL_COMPLICATION = 1;
60 
61     // Left and right complication IDs as array for Complication API.
62     public static final int[] COMPLICATION_IDS = {LEFT_DIAL_COMPLICATION, RIGHT_DIAL_COMPLICATION};
63 
64     // Left and right dial supported types.
65     public static final int[][] COMPLICATION_SUPPORTED_TYPES = {
66             {ComplicationData.TYPE_SHORT_TEXT},
67             {ComplicationData.TYPE_SHORT_TEXT}
68     };
69 
70     /*
71      * Update rate in milliseconds for interactive mode. We update once a second to advance the
72      * second hand.
73      */
74     private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
75 
76     @Override
onCreateEngine()77     public Engine onCreateEngine() {
78         return new Engine();
79     }
80 
81     private class Engine extends CanvasWatchFaceService.Engine {
82         private static final int MSG_UPDATE_TIME = 0;
83 
84         private static final float COMPLICATION_TEXT_SIZE = 38f;
85         private static final int COMPLICATION_TAP_BUFFER = 40;
86 
87         private static final float HOUR_STROKE_WIDTH = 5f;
88         private static final float MINUTE_STROKE_WIDTH = 3f;
89         private static final float SECOND_TICK_STROKE_WIDTH = 2f;
90 
91         private static final float CENTER_GAP_AND_CIRCLE_RADIUS = 4f;
92 
93         private static final int SHADOW_RADIUS = 6;
94 
95         private Calendar mCalendar;
96         private boolean mRegisteredTimeZoneReceiver = false;
97         private boolean mMuteMode;
98 
99         private int mWidth;
100         private int mHeight;
101         private float mCenterX;
102         private float mCenterY;
103 
104         private float mSecondHandLength;
105         private float mMinuteHandLength;
106         private float mHourHandLength;
107 
108         // Colors for all hands (hour, minute, seconds, ticks) based on photo loaded.
109         private int mWatchHandColor;
110         private int mWatchHandHighlightColor;
111         private int mWatchHandShadowColor;
112 
113         private Paint mHourPaint;
114         private Paint mMinutePaint;
115         private Paint mSecondPaint;
116         private Paint mTickAndCirclePaint;
117 
118         private Paint mBackgroundPaint;
119         private Bitmap mBackgroundBitmap;
120         private Bitmap mGrayBackgroundBitmap;
121 
122         // Variables for painting Complications
123         private Paint mComplicationPaint;
124 
125         /* To properly place each complication, we need their x and y coordinates. While the width
126          * may change from moment to moment based on the time, the height will not change, so we
127          * store it as a local variable and only calculate it only when the surface changes
128          * (onSurfaceChanged()).
129          */
130         private int mComplicationsY;
131 
132         /* Maps active complication ids to the data for that complication. Note: Data will only be
133          * present if the user has chosen a provider via the settings activity for the watch face.
134          */
135         private SparseArray<ComplicationData> mActiveComplicationDataSparseArray;
136 
137         private boolean mAmbient;
138         private boolean mLowBitAmbient;
139         private boolean mBurnInProtection;
140 
141         private Rect mPeekCardBounds = new Rect();
142 
143         private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
144             @Override
145             public void onReceive(Context context, Intent intent) {
146                 mCalendar.setTimeZone(TimeZone.getDefault());
147                 invalidate();
148             }
149         };
150 
151         // Handler to update the time once a second in interactive mode.
152         private final Handler mUpdateTimeHandler = new Handler() {
153             @Override
154             public void handleMessage(Message message) {
155 
156                 if (Log.isLoggable(TAG, Log.DEBUG)) {
157                     Log.d(TAG, "updating time");
158                 }
159                 invalidate();
160                 if (shouldTimerBeRunning()) {
161                     long timeMs = System.currentTimeMillis();
162                     long delayMs = INTERACTIVE_UPDATE_RATE_MS
163                             - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
164                     mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
165                 }
166 
167             }
168         };
169 
170         @Override
onCreate(SurfaceHolder holder)171         public void onCreate(SurfaceHolder holder) {
172             if (Log.isLoggable(TAG, Log.DEBUG)) {
173                 Log.d(TAG, "onCreate");
174             }
175             super.onCreate(holder);
176 
177             mCalendar = Calendar.getInstance();
178 
179             setWatchFaceStyle(new WatchFaceStyle.Builder(ComplicationSimpleWatchFaceService.this)
180                     .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
181                     .setAcceptsTapEvents(true)
182                     .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
183                     .setShowSystemUiTime(false)
184                     .build());
185 
186             initializeBackground();
187             initializeComplication();
188             initializeWatchFace();
189         }
190 
initializeBackground()191         private void initializeBackground() {
192             mBackgroundPaint = new Paint();
193             mBackgroundPaint.setColor(Color.BLACK);
194             mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg);
195         }
196 
initializeComplication()197         private void initializeComplication() {
198             if (Log.isLoggable(TAG, Log.DEBUG)) {
199                 Log.d(TAG, "initializeComplications()");
200             }
201             mActiveComplicationDataSparseArray = new SparseArray<>(COMPLICATION_IDS.length);
202 
203             mComplicationPaint = new Paint();
204             mComplicationPaint.setColor(Color.WHITE);
205             mComplicationPaint.setTextSize(COMPLICATION_TEXT_SIZE);
206             mComplicationPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
207             mComplicationPaint.setAntiAlias(true);
208 
209             setActiveComplications(COMPLICATION_IDS);
210         }
211 
initializeWatchFace()212         private void initializeWatchFace() {
213             /* Set defaults for colors */
214             mWatchHandColor = Color.WHITE;
215             mWatchHandHighlightColor = Color.RED;
216             mWatchHandShadowColor = Color.BLACK;
217 
218             mHourPaint = new Paint();
219             mHourPaint.setColor(mWatchHandColor);
220             mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH);
221             mHourPaint.setAntiAlias(true);
222             mHourPaint.setStrokeCap(Paint.Cap.ROUND);
223             mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
224 
225             mMinutePaint = new Paint();
226             mMinutePaint.setColor(mWatchHandColor);
227             mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH);
228             mMinutePaint.setAntiAlias(true);
229             mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
230             mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
231 
232             mSecondPaint = new Paint();
233             mSecondPaint.setColor(mWatchHandHighlightColor);
234             mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
235             mSecondPaint.setAntiAlias(true);
236             mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
237             mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
238 
239             mTickAndCirclePaint = new Paint();
240             mTickAndCirclePaint.setColor(mWatchHandColor);
241             mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
242             mTickAndCirclePaint.setAntiAlias(true);
243             mTickAndCirclePaint.setStyle(Paint.Style.STROKE);
244             mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
245 
246             // Asynchronous call extract colors from background image to improve watch face style.
247             Palette.from(mBackgroundBitmap).generate(
248                     new Palette.PaletteAsyncListener() {
249                         public void onGenerated(Palette palette) {
250                             /*
251                              * Sometimes, palette is unable to generate a color palette
252                              * so we need to check that we have one.
253                              */
254                             if (palette != null) {
255                                 Log.d("onGenerated", palette.toString());
256                                 mWatchHandColor = palette.getVibrantColor(Color.WHITE);
257                                 mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK);
258                                 updateWatchHandStyle();
259                             }
260                         }
261                     });
262         }
263 
264         @Override
onDestroy()265         public void onDestroy() {
266             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
267             super.onDestroy();
268         }
269 
270         @Override
onPropertiesChanged(Bundle properties)271         public void onPropertiesChanged(Bundle properties) {
272             super.onPropertiesChanged(properties);
273             if (Log.isLoggable(TAG, Log.DEBUG)) {
274                 Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
275             }
276 
277             mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
278             mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
279         }
280 
281         /*
282          * Called when there is updated data for a complication id.
283          */
284         @Override
onComplicationDataUpdate( int complicationId, ComplicationData complicationData)285         public void onComplicationDataUpdate(
286                 int complicationId, ComplicationData complicationData) {
287             Log.d(TAG, "onComplicationDataUpdate() id: " + complicationId);
288 
289             // Adds/updates active complication data in the array.
290             mActiveComplicationDataSparseArray.put(complicationId, complicationData);
291             invalidate();
292         }
293 
294         @Override
onTapCommand(int tapType, int x, int y, long eventTime)295         public void onTapCommand(int tapType, int x, int y, long eventTime) {
296             Log.d(TAG, "OnTapCommand()");
297             switch (tapType) {
298                 case TAP_TYPE_TAP:
299                     int tappedComplicationId = getTappedComplicationId(x, y);
300                     if (tappedComplicationId != -1) {
301                         onComplicationTap(tappedComplicationId);
302                     }
303                     break;
304             }
305         }
306 
307         /*
308          * Determines if tap inside a complication area or returns -1.
309          */
getTappedComplicationId(int x, int y)310         private int getTappedComplicationId(int x, int y) {
311             ComplicationData complicationData;
312             long currentTimeMillis = System.currentTimeMillis();
313 
314             for (int i = 0; i < COMPLICATION_IDS.length; i++) {
315                 complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]);
316 
317                 if ((complicationData != null)
318                         && (complicationData.isActive(currentTimeMillis))
319                         && (complicationData.getType() != ComplicationData.TYPE_NOT_CONFIGURED)
320                         && (complicationData.getType() != ComplicationData.TYPE_EMPTY)) {
321 
322                     Rect complicationBoundingRect = new Rect(0, 0, 0, 0);
323 
324                     switch (COMPLICATION_IDS[i]) {
325                         case LEFT_DIAL_COMPLICATION:
326                             complicationBoundingRect.set(
327                                     0,                                          // left
328                                     mComplicationsY - COMPLICATION_TAP_BUFFER,  // top
329                                     (mWidth / 2),                               // right
330                                     ((int) COMPLICATION_TEXT_SIZE               // bottom
331                                             + mComplicationsY
332                                             + COMPLICATION_TAP_BUFFER));
333                             break;
334 
335                         case RIGHT_DIAL_COMPLICATION:
336                             complicationBoundingRect.set(
337                                     (mWidth / 2),                               // left
338                                     mComplicationsY - COMPLICATION_TAP_BUFFER,  // top
339                                     mWidth,                                     // right
340                                     ((int) COMPLICATION_TEXT_SIZE               // bottom
341                                             + mComplicationsY
342                                             + COMPLICATION_TAP_BUFFER));
343                             break;
344                     }
345 
346                     if (complicationBoundingRect.width() > 0) {
347                         if (complicationBoundingRect.contains(x, y)) {
348                             return COMPLICATION_IDS[i];
349                         }
350                     } else {
351                         Log.e(TAG, "Not a recognized complication id.");
352                     }
353                 }
354             }
355             return -1;
356         }
357 
358         /*
359          * Fires PendingIntent associated with complication (if it has one).
360          */
onComplicationTap(int complicationId)361         private void onComplicationTap(int complicationId) {
362             if (Log.isLoggable(TAG, Log.DEBUG)) {
363                 Log.d(TAG, "onComplicationTap()");
364             }
365             ComplicationData complicationData =
366                     mActiveComplicationDataSparseArray.get(complicationId);
367 
368             if ((complicationData != null) && (complicationData.getTapAction() != null)) {
369                 try {
370                     complicationData.getTapAction().send();
371                 } catch (PendingIntent.CanceledException e) {
372                     Log.e(TAG, "On complication tap action error " + e);
373                 }
374                 invalidate();
375             } else {
376                 if (Log.isLoggable(TAG, Log.DEBUG)) {
377                     Log.d(TAG, "No PendingIntent for complication " + complicationId + ".");
378                 }
379             }
380         }
381 
382         @Override
onTimeTick()383         public void onTimeTick() {
384             super.onTimeTick();
385             invalidate();
386         }
387 
388         @Override
onAmbientModeChanged(boolean inAmbientMode)389         public void onAmbientModeChanged(boolean inAmbientMode) {
390             super.onAmbientModeChanged(inAmbientMode);
391             if (Log.isLoggable(TAG, Log.DEBUG)) {
392                 Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
393             }
394             mAmbient = inAmbientMode;
395 
396             updateWatchHandStyle();
397 
398             // Updates complication style
399             mComplicationPaint.setAntiAlias(!inAmbientMode);
400 
401             // Check and trigger whether or not timer should be running (only in active mode).
402             updateTimer();
403         }
404 
updateWatchHandStyle()405         private void updateWatchHandStyle(){
406             if (mAmbient){
407                 mHourPaint.setColor(Color.WHITE);
408                 mMinutePaint.setColor(Color.WHITE);
409                 mSecondPaint.setColor(Color.WHITE);
410                 mTickAndCirclePaint.setColor(Color.WHITE);
411 
412                 mHourPaint.setAntiAlias(false);
413                 mMinutePaint.setAntiAlias(false);
414                 mSecondPaint.setAntiAlias(false);
415                 mTickAndCirclePaint.setAntiAlias(false);
416 
417                 mHourPaint.clearShadowLayer();
418                 mMinutePaint.clearShadowLayer();
419                 mSecondPaint.clearShadowLayer();
420                 mTickAndCirclePaint.clearShadowLayer();
421 
422             } else {
423                 mHourPaint.setColor(mWatchHandColor);
424                 mMinutePaint.setColor(mWatchHandColor);
425                 mSecondPaint.setColor(mWatchHandHighlightColor);
426                 mTickAndCirclePaint.setColor(mWatchHandColor);
427 
428                 mHourPaint.setAntiAlias(true);
429                 mMinutePaint.setAntiAlias(true);
430                 mSecondPaint.setAntiAlias(true);
431                 mTickAndCirclePaint.setAntiAlias(true);
432 
433                 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
434                 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
435                 mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
436                 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
437             }
438         }
439 
440         @Override
onInterruptionFilterChanged(int interruptionFilter)441         public void onInterruptionFilterChanged(int interruptionFilter) {
442             super.onInterruptionFilterChanged(interruptionFilter);
443             boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
444 
445             /* Dim display in mute mode. */
446             if (mMuteMode != inMuteMode) {
447                 mMuteMode = inMuteMode;
448                 mHourPaint.setAlpha(inMuteMode ? 100 : 255);
449                 mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
450                 mSecondPaint.setAlpha(inMuteMode ? 80 : 255);
451                 invalidate();
452             }
453         }
454 
455         @Override
onSurfaceChanged(SurfaceHolder holder, int format, int width, int height)456         public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
457             super.onSurfaceChanged(holder, format, width, height);
458 
459             // Used for complications
460             mWidth = width;
461             mHeight = height;
462 
463             /*
464              * Find the coordinates of the center point on the screen, and ignore the window
465              * insets, so that, on round watches with a "chin", the watch face is centered on the
466              * entire screen, not just the usable portion.
467              */
468             mCenterX = mWidth / 2f;
469             mCenterY = mHeight / 2f;
470 
471             /*
472              * Since the height of the complications text does not change, we only have to
473              * recalculate when the surface changes.
474              */
475             mComplicationsY = (int) ((mHeight / 2) + (mComplicationPaint.getTextSize() / 2));
476 
477             /*
478              * Calculate lengths of different hands based on watch screen size.
479              */
480             mSecondHandLength = (float) (mCenterX * 0.875);
481             mMinuteHandLength = (float) (mCenterX * 0.75);
482             mHourHandLength = (float) (mCenterX * 0.5);
483 
484 
485             /* Scale loaded background image (more efficient) if surface dimensions change. */
486             float scale = ((float) width) / (float) mBackgroundBitmap.getWidth();
487 
488             mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
489                     (int) (mBackgroundBitmap.getWidth() * scale),
490                     (int) (mBackgroundBitmap.getHeight() * scale), true);
491 
492             /*
493              * Create a gray version of the image only if it will look nice on the device in
494              * ambient mode. That means we don't want devices that support burn-in
495              * protection (slight movements in pixels, not great for images going all the way to
496              * edges) and low ambient mode (degrades image quality).
497              *
498              * Also, if your watch face will know about all images ahead of time (users aren't
499              * selecting their own photos for the watch face), it will be more
500              * efficient to create a black/white version (png, etc.) and load that when you need it.
501              */
502             if (!mBurnInProtection && !mLowBitAmbient) {
503                 initGrayBackgroundBitmap();
504             }
505         }
506 
initGrayBackgroundBitmap()507         private void initGrayBackgroundBitmap() {
508             mGrayBackgroundBitmap = Bitmap.createBitmap(
509                     mBackgroundBitmap.getWidth(),
510                     mBackgroundBitmap.getHeight(),
511                     Bitmap.Config.ARGB_8888);
512             Canvas canvas = new Canvas(mGrayBackgroundBitmap);
513             Paint grayPaint = new Paint();
514             ColorMatrix colorMatrix = new ColorMatrix();
515             colorMatrix.setSaturation(0);
516             ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
517             grayPaint.setColorFilter(filter);
518             canvas.drawBitmap(mBackgroundBitmap, 0, 0, grayPaint);
519         }
520 
521         @Override
onDraw(Canvas canvas, Rect bounds)522         public void onDraw(Canvas canvas, Rect bounds) {
523             if (Log.isLoggable(TAG, Log.DEBUG)) {
524                 Log.d(TAG, "onDraw");
525             }
526             long now = System.currentTimeMillis();
527             mCalendar.setTimeInMillis(now);
528 
529             drawBackground(canvas);
530             drawComplications(canvas, now);
531             drawWatchFace(canvas);
532 
533 
534         }
535 
drawBackground(Canvas canvas)536         private void drawBackground(Canvas canvas) {
537             if (mAmbient && (mLowBitAmbient || mBurnInProtection)) {
538                 canvas.drawColor(Color.BLACK);
539             } else if (mAmbient) {
540                 canvas.drawBitmap(mGrayBackgroundBitmap, 0, 0, mBackgroundPaint);
541             } else {
542                 canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint);
543             }
544         }
545 
drawComplications(Canvas canvas, long currentTimeMillis)546         private void drawComplications(Canvas canvas, long currentTimeMillis) {
547             ComplicationData complicationData;
548 
549             for (int i = 0; i < COMPLICATION_IDS.length; i++) {
550 
551                 complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]);
552 
553                 if ((complicationData != null)
554                         && (complicationData.isActive(currentTimeMillis))
555                         && (complicationData.getType() == ComplicationData.TYPE_SHORT_TEXT)) {
556 
557                     ComplicationText mainText = complicationData.getShortText();
558                     ComplicationText subText = complicationData.getShortTitle();
559 
560                     CharSequence complicationMessage =
561                             mainText.getText(getApplicationContext(), currentTimeMillis);
562 
563                     /* In most cases you would want the subText (Title) under the mainText (Text),
564                      * but to keep it simple for the code lab, we are concatenating them all on one
565                      * line.
566                      */
567                     if (subText != null) {
568                         complicationMessage = TextUtils.concat(
569                                 complicationMessage,
570                                 " ",
571                                 subText.getText(getApplicationContext(), currentTimeMillis));
572                     }
573 
574                     //Log.d(TAG, "Comp id: " + COMPLICATION_IDS[i] + "\t" + complicationMessage);
575                     double textWidth =
576                             mComplicationPaint.measureText(
577                                     complicationMessage,
578                                     0,
579                                     complicationMessage.length());
580 
581                     int complicationsX;
582 
583                     if (COMPLICATION_IDS[i] == LEFT_DIAL_COMPLICATION) {
584                         complicationsX = (int) ((mWidth / 2) - textWidth) / 2;
585                     } else {
586                         // RIGHT_DIAL_COMPLICATION calculations
587                         int offset = (int) ((mWidth / 2) - textWidth) / 2;
588                         complicationsX = (mWidth / 2) + offset;
589                     }
590 
591                     canvas.drawText(
592                             complicationMessage,
593                             0,
594                             complicationMessage.length(),
595                             complicationsX,
596                             mComplicationsY,
597                             mComplicationPaint);
598                 }
599             }
600         }
601 
drawWatchFace(Canvas canvas)602         private void drawWatchFace(Canvas canvas) {
603             /*
604              * Draw ticks. Usually you will want to bake this directly into the photo, but in
605              * cases where you want to allow users to select their own photos, this dynamically
606              * creates them on top of the photo.
607              */
608             float innerTickRadius = mCenterX - 10;
609             float outerTickRadius = mCenterX;
610             for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
611                 float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
612                 float innerX = (float) Math.sin(tickRot) * innerTickRadius;
613                 float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
614                 float outerX = (float) Math.sin(tickRot) * outerTickRadius;
615                 float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
616                 canvas.drawLine(mCenterX + innerX, mCenterY + innerY,
617                         mCenterX + outerX, mCenterY + outerY, mTickAndCirclePaint);
618             }
619 
620             /*
621              * These calculations reflect the rotation in degrees per unit of time, e.g.,
622              * 360 / 60 = 6 and 360 / 12 = 30.
623              */
624             final float seconds =
625                     (mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f);
626             final float secondsRotation = seconds * 6f;
627 
628             final float minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f;
629 
630             final float hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f;
631             final float hoursRotation = (mCalendar.get(Calendar.HOUR) * 30) + hourHandOffset;
632 
633             /*
634              * Save the canvas state before we can begin to rotate it.
635              */
636             canvas.save();
637 
638             canvas.rotate(hoursRotation, mCenterX, mCenterY);
639             canvas.drawLine(
640                     mCenterX,
641                     mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
642                     mCenterX,
643                     mCenterY - mHourHandLength,
644                     mHourPaint);
645 
646             canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY);
647             canvas.drawLine(
648                     mCenterX,
649                     mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
650                     mCenterX,
651                     mCenterY - mMinuteHandLength,
652                     mMinutePaint);
653 
654             /*
655              * Ensure the "seconds" hand is drawn only when we are in interactive mode.
656              * Otherwise, we only update the watch face once a minute.
657              */
658             if (!mAmbient) {
659                 canvas.rotate(secondsRotation - minutesRotation, mCenterX, mCenterY);
660                 canvas.drawLine(
661                         mCenterX,
662                         mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
663                         mCenterX,
664                         mCenterY - mSecondHandLength,
665                         mSecondPaint);
666 
667             }
668             canvas.drawCircle(
669                     mCenterX,
670                     mCenterY,
671                     CENTER_GAP_AND_CIRCLE_RADIUS,
672                     mTickAndCirclePaint);
673 
674             /* Restore the canvas' original orientation. */
675             canvas.restore();
676 
677             /* Draw rectangle behind peek card in ambient mode to improve readability. */
678             if (mAmbient) {
679                 canvas.drawRect(mPeekCardBounds, mBackgroundPaint);
680             }
681         }
682 
683         @Override
onVisibilityChanged(boolean visible)684         public void onVisibilityChanged(boolean visible) {
685             super.onVisibilityChanged(visible);
686 
687             if (visible) {
688                 registerReceiver();
689                 // Update time zone in case it changed while we weren't visible.
690                 mCalendar.setTimeZone(TimeZone.getDefault());
691                 invalidate();
692             } else {
693                 unregisterReceiver();
694             }
695 
696             /* Check and trigger whether or not timer should be running (only in active mode). */
697             updateTimer();
698         }
699 
700         @Override
onPeekCardPositionUpdate(Rect rect)701         public void onPeekCardPositionUpdate(Rect rect) {
702             super.onPeekCardPositionUpdate(rect);
703             mPeekCardBounds.set(rect);
704         }
705 
registerReceiver()706         private void registerReceiver() {
707             if (mRegisteredTimeZoneReceiver) {
708                 return;
709             }
710             mRegisteredTimeZoneReceiver = true;
711             IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
712             ComplicationSimpleWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
713         }
714 
unregisterReceiver()715         private void unregisterReceiver() {
716             if (!mRegisteredTimeZoneReceiver) {
717                 return;
718             }
719             mRegisteredTimeZoneReceiver = false;
720             ComplicationSimpleWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
721         }
722 
723         /**
724          * Starts/stops the {@link #mUpdateTimeHandler} timer based on the state of the watch face.
725          */
updateTimer()726         private void updateTimer() {
727             if (Log.isLoggable(TAG, Log.DEBUG)) {
728                 Log.d(TAG, "updateTimer");
729             }
730             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
731             if (shouldTimerBeRunning()) {
732                 mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
733             }
734         }
735 
736         /**
737          * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer
738          * should only run in active mode.
739          */
shouldTimerBeRunning()740         private boolean shouldTimerBeRunning() {
741             return isVisible() && !mAmbient;
742         }
743     }
744 }