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