1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.wearable.watchface;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.graphics.Bitmap;
24 import android.graphics.BitmapFactory;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.ColorMatrix;
28 import android.graphics.ColorMatrixColorFilter;
29 import android.graphics.Paint;
30 import android.graphics.Rect;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.Message;
34 import android.support.v7.graphics.Palette;
35 import android.support.wearable.watchface.CanvasWatchFaceService;
36 import android.support.wearable.watchface.WatchFaceService;
37 import android.support.wearable.watchface.WatchFaceStyle;
38 import android.util.Log;
39 import android.view.SurfaceHolder;
40 
41 import java.util.Calendar;
42 import java.util.TimeZone;
43 import java.util.concurrent.TimeUnit;
44 
45 /**
46  * Sample analog watch face with a ticking second hand. In ambient mode, the second hand isn't
47  * shown. On devices with low-bit ambient mode, the hands are drawn without anti-aliasing in ambient
48  * mode. The watch face is drawn with less contrast in mute mode.
49  *
50  * {@link SweepWatchFaceService} is similar but has a sweep second hand.
51  */
52 public class AnalogWatchFaceService extends CanvasWatchFaceService {
53     private static final String TAG = "AnalogWatchFaceService";
54 
55     /*
56      * Update rate in milliseconds for interactive mode. We update once a second to advance the
57      * second hand.
58      */
59     private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
60 
61     @Override
onCreateEngine()62     public Engine onCreateEngine() {
63         return new Engine();
64     }
65 
66     private class Engine extends CanvasWatchFaceService.Engine {
67         private static final int MSG_UPDATE_TIME = 0;
68 
69         private static final float HOUR_STROKE_WIDTH = 5f;
70         private static final float MINUTE_STROKE_WIDTH = 3f;
71         private static final float SECOND_TICK_STROKE_WIDTH = 2f;
72 
73         private static final float CENTER_GAP_AND_CIRCLE_RADIUS = 4f;
74 
75         private static final int SHADOW_RADIUS = 6;
76 
77         private Calendar mCalendar;
78         private boolean mRegisteredTimeZoneReceiver = false;
79         private boolean mMuteMode;
80 
81         private float mCenterX;
82         private float mCenterY;
83 
84         private float mSecondHandLength;
85         private float sMinuteHandLength;
86         private float sHourHandLength;
87 
88         /* Colors for all hands (hour, minute, seconds, ticks) based on photo loaded. */
89         private int mWatchHandColor;
90         private int mWatchHandHighlightColor;
91         private int mWatchHandShadowColor;
92 
93         private Paint mHourPaint;
94         private Paint mMinutePaint;
95         private Paint mSecondPaint;
96         private Paint mTickAndCirclePaint;
97 
98         private Paint mBackgroundPaint;
99         private Bitmap mBackgroundBitmap;
100         private Bitmap mGrayBackgroundBitmap;
101 
102         private boolean mAmbient;
103         private boolean mLowBitAmbient;
104         private boolean mBurnInProtection;
105 
106         private Rect mPeekCardBounds = new Rect();
107 
108         private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
109             @Override
110             public void onReceive(Context context, Intent intent) {
111                 mCalendar.setTimeZone(TimeZone.getDefault());
112                 invalidate();
113             }
114         };
115 
116         /* Handler to update the time once a second in interactive mode. */
117         private final Handler mUpdateTimeHandler = new Handler() {
118             @Override
119             public void handleMessage(Message message) {
120 
121                 if (Log.isLoggable(TAG, Log.DEBUG)) {
122                     Log.d(TAG, "updating time");
123                 }
124                 invalidate();
125                 if (shouldTimerBeRunning()) {
126                     long timeMs = System.currentTimeMillis();
127                     long delayMs = INTERACTIVE_UPDATE_RATE_MS
128                             - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
129                     mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
130                 }
131 
132             }
133         };
134 
135         @Override
onCreate(SurfaceHolder holder)136         public void onCreate(SurfaceHolder holder) {
137             if (Log.isLoggable(TAG, Log.DEBUG)) {
138                 Log.d(TAG, "onCreate");
139             }
140             super.onCreate(holder);
141 
142             setWatchFaceStyle(new WatchFaceStyle.Builder(AnalogWatchFaceService.this)
143                     .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
144                     .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
145                     .setShowSystemUiTime(false)
146                     .build());
147 
148             mBackgroundPaint = new Paint();
149             mBackgroundPaint.setColor(Color.BLACK);
150             mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg);
151 
152             /* Set defaults for colors */
153             mWatchHandColor = Color.WHITE;
154             mWatchHandHighlightColor = Color.RED;
155             mWatchHandShadowColor = Color.BLACK;
156 
157             mHourPaint = new Paint();
158             mHourPaint.setColor(mWatchHandColor);
159             mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH);
160             mHourPaint.setAntiAlias(true);
161             mHourPaint.setStrokeCap(Paint.Cap.ROUND);
162             mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
163 
164             mMinutePaint = new Paint();
165             mMinutePaint.setColor(mWatchHandColor);
166             mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH);
167             mMinutePaint.setAntiAlias(true);
168             mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
169             mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
170 
171             mSecondPaint = new Paint();
172             mSecondPaint.setColor(mWatchHandHighlightColor);
173             mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
174             mSecondPaint.setAntiAlias(true);
175             mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
176             mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
177 
178             mTickAndCirclePaint = new Paint();
179             mTickAndCirclePaint.setColor(mWatchHandColor);
180             mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
181             mTickAndCirclePaint.setAntiAlias(true);
182             mTickAndCirclePaint.setStyle(Paint.Style.STROKE);
183             mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
184 
185             /* Extract colors from background image to improve watchface style. */
186             Palette.generateAsync(
187                     mBackgroundBitmap,
188                     new Palette.PaletteAsyncListener() {
189                         @Override
190                         public void onGenerated(Palette palette) {
191                             if (palette != null) {
192                                 if (Log.isLoggable(TAG, Log.DEBUG)) {
193                                     Log.d(TAG, "Palette: " + palette);
194                                 }
195 
196                                 mWatchHandHighlightColor = palette.getVibrantColor(Color.RED);
197                                 mWatchHandColor = palette.getLightVibrantColor(Color.WHITE);
198                                 mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK);
199                                 updateWatchHandStyle();
200                             }
201                         }
202                     });
203 
204             mCalendar = Calendar.getInstance();
205         }
206 
207         @Override
onDestroy()208         public void onDestroy() {
209             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
210             super.onDestroy();
211         }
212 
213         @Override
onPropertiesChanged(Bundle properties)214         public void onPropertiesChanged(Bundle properties) {
215             super.onPropertiesChanged(properties);
216             if (Log.isLoggable(TAG, Log.DEBUG)) {
217                 Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
218             }
219 
220             mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
221             mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
222         }
223 
224         @Override
onTimeTick()225         public void onTimeTick() {
226             super.onTimeTick();
227             invalidate();
228         }
229 
230         @Override
onAmbientModeChanged(boolean inAmbientMode)231         public void onAmbientModeChanged(boolean inAmbientMode) {
232             super.onAmbientModeChanged(inAmbientMode);
233             if (Log.isLoggable(TAG, Log.DEBUG)) {
234                 Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
235             }
236             mAmbient = inAmbientMode;
237 
238             updateWatchHandStyle();
239 
240             /* Check and trigger whether or not timer should be running (only in active mode). */
241             updateTimer();
242         }
243 
updateWatchHandStyle()244         private void updateWatchHandStyle(){
245             if (mAmbient){
246                 mHourPaint.setColor(Color.WHITE);
247                 mMinutePaint.setColor(Color.WHITE);
248                 mSecondPaint.setColor(Color.WHITE);
249                 mTickAndCirclePaint.setColor(Color.WHITE);
250 
251                 mHourPaint.setAntiAlias(false);
252                 mMinutePaint.setAntiAlias(false);
253                 mSecondPaint.setAntiAlias(false);
254                 mTickAndCirclePaint.setAntiAlias(false);
255 
256                 mHourPaint.clearShadowLayer();
257                 mMinutePaint.clearShadowLayer();
258                 mSecondPaint.clearShadowLayer();
259                 mTickAndCirclePaint.clearShadowLayer();
260 
261             } else {
262                 mHourPaint.setColor(mWatchHandColor);
263                 mMinutePaint.setColor(mWatchHandColor);
264                 mSecondPaint.setColor(mWatchHandHighlightColor);
265                 mTickAndCirclePaint.setColor(mWatchHandColor);
266 
267                 mHourPaint.setAntiAlias(true);
268                 mMinutePaint.setAntiAlias(true);
269                 mSecondPaint.setAntiAlias(true);
270                 mTickAndCirclePaint.setAntiAlias(true);
271 
272                 mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
273                 mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
274                 mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
275                 mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
276             }
277         }
278 
279         @Override
onInterruptionFilterChanged(int interruptionFilter)280         public void onInterruptionFilterChanged(int interruptionFilter) {
281             super.onInterruptionFilterChanged(interruptionFilter);
282             boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
283 
284             /* Dim display in mute mode. */
285             if (mMuteMode != inMuteMode) {
286                 mMuteMode = inMuteMode;
287                 mHourPaint.setAlpha(inMuteMode ? 100 : 255);
288                 mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
289                 mSecondPaint.setAlpha(inMuteMode ? 80 : 255);
290                 invalidate();
291             }
292         }
293 
294         @Override
onSurfaceChanged(SurfaceHolder holder, int format, int width, int height)295         public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
296             super.onSurfaceChanged(holder, format, width, height);
297 
298             /*
299              * Find the coordinates of the center point on the screen, and ignore the window
300              * insets, so that, on round watches with a "chin", the watch face is centered on the
301              * entire screen, not just the usable portion.
302              */
303             mCenterX = width / 2f;
304             mCenterY = height / 2f;
305 
306             /*
307              * Calculate lengths of different hands based on watch screen size.
308              */
309             mSecondHandLength = (float) (mCenterX * 0.875);
310             sMinuteHandLength = (float) (mCenterX * 0.75);
311             sHourHandLength = (float) (mCenterX * 0.5);
312 
313 
314             /* Scale loaded background image (more efficient) if surface dimensions change. */
315             float scale = ((float) width) / (float) mBackgroundBitmap.getWidth();
316 
317             mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
318                     (int) (mBackgroundBitmap.getWidth() * scale),
319                     (int) (mBackgroundBitmap.getHeight() * scale), true);
320 
321             /*
322              * Create a gray version of the image only if it will look nice on the device in
323              * ambient mode. That means we don't want devices that support burn-in
324              * protection (slight movements in pixels, not great for images going all the way to
325              * edges) and low ambient mode (degrades image quality).
326              *
327              * Also, if your watch face will know about all images ahead of time (users aren't
328              * selecting their own photos for the watch face), it will be more
329              * efficient to create a black/white version (png, etc.) and load that when you need it.
330              */
331             if (!mBurnInProtection && !mLowBitAmbient) {
332                 initGrayBackgroundBitmap();
333             }
334         }
335 
initGrayBackgroundBitmap()336         private void initGrayBackgroundBitmap() {
337             mGrayBackgroundBitmap = Bitmap.createBitmap(
338                     mBackgroundBitmap.getWidth(),
339                     mBackgroundBitmap.getHeight(),
340                     Bitmap.Config.ARGB_8888);
341             Canvas canvas = new Canvas(mGrayBackgroundBitmap);
342             Paint grayPaint = new Paint();
343             ColorMatrix colorMatrix = new ColorMatrix();
344             colorMatrix.setSaturation(0);
345             ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
346             grayPaint.setColorFilter(filter);
347             canvas.drawBitmap(mBackgroundBitmap, 0, 0, grayPaint);
348         }
349 
350         @Override
onDraw(Canvas canvas, Rect bounds)351         public void onDraw(Canvas canvas, Rect bounds) {
352             if (Log.isLoggable(TAG, Log.VERBOSE)) {
353                 Log.v(TAG, "onDraw");
354             }
355             long now = System.currentTimeMillis();
356             mCalendar.setTimeInMillis(now);
357 
358             if (mAmbient && (mLowBitAmbient || mBurnInProtection)) {
359                 canvas.drawColor(Color.BLACK);
360             } else if (mAmbient) {
361                 canvas.drawBitmap(mGrayBackgroundBitmap, 0, 0, mBackgroundPaint);
362             } else {
363                 canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint);
364             }
365 
366             /*
367              * Draw ticks. Usually you will want to bake this directly into the photo, but in
368              * cases where you want to allow users to select their own photos, this dynamically
369              * creates them on top of the photo.
370              */
371             float innerTickRadius = mCenterX - 10;
372             float outerTickRadius = mCenterX;
373             for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
374                 float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
375                 float innerX = (float) Math.sin(tickRot) * innerTickRadius;
376                 float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
377                 float outerX = (float) Math.sin(tickRot) * outerTickRadius;
378                 float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
379                 canvas.drawLine(mCenterX + innerX, mCenterY + innerY,
380                         mCenterX + outerX, mCenterY + outerY, mTickAndCirclePaint);
381             }
382 
383             /*
384              * These calculations reflect the rotation in degrees per unit of time, e.g.,
385              * 360 / 60 = 6 and 360 / 12 = 30.
386              */
387             final float seconds =
388                     (mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f);
389             final float secondsRotation = seconds * 6f;
390 
391             final float minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f;
392 
393             final float hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f;
394             final float hoursRotation = (mCalendar.get(Calendar.HOUR) * 30) + hourHandOffset;
395 
396             /*
397              * Save the canvas state before we can begin to rotate it.
398              */
399             canvas.save();
400 
401             canvas.rotate(hoursRotation, mCenterX, mCenterY);
402             canvas.drawLine(
403                     mCenterX,
404                     mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
405                     mCenterX,
406                     mCenterY - sHourHandLength,
407                     mHourPaint);
408 
409             canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY);
410             canvas.drawLine(
411                     mCenterX,
412                     mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
413                     mCenterX,
414                     mCenterY - sMinuteHandLength,
415                     mMinutePaint);
416 
417             /*
418              * Ensure the "seconds" hand is drawn only when we are in interactive mode.
419              * Otherwise, we only update the watch face once a minute.
420              */
421             if (!mAmbient) {
422                 canvas.rotate(secondsRotation - minutesRotation, mCenterX, mCenterY);
423                 canvas.drawLine(
424                         mCenterX,
425                         mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
426                         mCenterX,
427                         mCenterY - mSecondHandLength,
428                         mSecondPaint);
429 
430             }
431             canvas.drawCircle(
432                     mCenterX,
433                     mCenterY,
434                     CENTER_GAP_AND_CIRCLE_RADIUS,
435                     mTickAndCirclePaint);
436 
437             /* Restore the canvas' original orientation. */
438             canvas.restore();
439 
440             /* Draw rectangle behind peek card in ambient mode to improve readability. */
441             if (mAmbient) {
442                 canvas.drawRect(mPeekCardBounds, mBackgroundPaint);
443             }
444         }
445 
446         @Override
onVisibilityChanged(boolean visible)447         public void onVisibilityChanged(boolean visible) {
448             super.onVisibilityChanged(visible);
449 
450             if (visible) {
451                 registerReceiver();
452                 /* Update time zone in case it changed while we weren't visible. */
453                 mCalendar.setTimeZone(TimeZone.getDefault());
454                 invalidate();
455             } else {
456                 unregisterReceiver();
457             }
458 
459             /* Check and trigger whether or not timer should be running (only in active mode). */
460             updateTimer();
461         }
462 
463         @Override
onPeekCardPositionUpdate(Rect rect)464         public void onPeekCardPositionUpdate(Rect rect) {
465             super.onPeekCardPositionUpdate(rect);
466             mPeekCardBounds.set(rect);
467         }
468 
registerReceiver()469         private void registerReceiver() {
470             if (mRegisteredTimeZoneReceiver) {
471                 return;
472             }
473             mRegisteredTimeZoneReceiver = true;
474             IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
475             AnalogWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
476         }
477 
unregisterReceiver()478         private void unregisterReceiver() {
479             if (!mRegisteredTimeZoneReceiver) {
480                 return;
481             }
482             mRegisteredTimeZoneReceiver = false;
483             AnalogWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
484         }
485 
486         /**
487          * Starts/stops the {@link #mUpdateTimeHandler} timer based on the state of the watch face.
488          */
updateTimer()489         private void updateTimer() {
490             if (Log.isLoggable(TAG, Log.DEBUG)) {
491                 Log.d(TAG, "updateTimer");
492             }
493             mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
494             if (shouldTimerBeRunning()) {
495                 mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
496             }
497         }
498 
499         /**
500          * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer
501          * should only run in active mode.
502          */
shouldTimerBeRunning()503         private boolean shouldTimerBeRunning() {
504             return isVisible() && !mAmbient;
505         }
506     }
507 }
508