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