1 /* 2 * Copyright (C) 2006 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 android.widget; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.AppGlobals; 22 import android.compat.annotation.UnsupportedAppUsage; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.content.res.ColorStateList; 28 import android.content.res.TypedArray; 29 import android.graphics.BlendMode; 30 import android.graphics.Canvas; 31 import android.graphics.drawable.Drawable; 32 import android.graphics.drawable.Icon; 33 import android.text.format.DateUtils; 34 import android.util.AttributeSet; 35 import android.util.Log; 36 import android.view.RemotableViewMethod; 37 import android.view.View; 38 import android.view.inspector.InspectableProperty; 39 import android.widget.RemoteViews.RemoteView; 40 41 import java.time.Clock; 42 import java.time.DateTimeException; 43 import java.time.Duration; 44 import java.time.Instant; 45 import java.time.LocalTime; 46 import java.time.ZoneId; 47 import java.time.ZonedDateTime; 48 import java.util.Formatter; 49 import java.util.Locale; 50 51 /** 52 * This widget display an analogic clock with two hands for hours and 53 * minutes. 54 * 55 * @attr ref android.R.styleable#AnalogClock_dial 56 * @attr ref android.R.styleable#AnalogClock_hand_hour 57 * @attr ref android.R.styleable#AnalogClock_hand_minute 58 * @attr ref android.R.styleable#AnalogClock_hand_second 59 * @attr ref android.R.styleable#AnalogClock_timeZone 60 * @deprecated This widget is no longer supported. 61 */ 62 @RemoteView 63 @Deprecated 64 public class AnalogClock extends View { 65 private static final String LOG_TAG = "AnalogClock"; 66 67 /** How many times per second that the seconds hand advances. */ 68 private final int mSecondsHandFps; 69 70 private Clock mClock; 71 @Nullable 72 private ZoneId mTimeZone; 73 74 @UnsupportedAppUsage 75 private Drawable mHourHand; 76 private final TintInfo mHourHandTintInfo = new TintInfo(); 77 @UnsupportedAppUsage 78 private Drawable mMinuteHand; 79 private final TintInfo mMinuteHandTintInfo = new TintInfo(); 80 @Nullable 81 private Drawable mSecondHand; 82 private final TintInfo mSecondHandTintInfo = new TintInfo(); 83 @UnsupportedAppUsage 84 private Drawable mDial; 85 private final TintInfo mDialTintInfo = new TintInfo(); 86 87 private int mDialWidth; 88 private int mDialHeight; 89 90 private boolean mVisible; 91 92 private float mSeconds; 93 private float mMinutes; 94 private float mHour; 95 private boolean mChanged; 96 AnalogClock(Context context)97 public AnalogClock(Context context) { 98 this(context, null); 99 } 100 AnalogClock(Context context, AttributeSet attrs)101 public AnalogClock(Context context, AttributeSet attrs) { 102 this(context, attrs, 0); 103 } 104 AnalogClock(Context context, AttributeSet attrs, int defStyleAttr)105 public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) { 106 this(context, attrs, defStyleAttr, 0); 107 } 108 AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)109 public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 110 super(context, attrs, defStyleAttr, defStyleRes); 111 112 mSecondsHandFps = AppGlobals.getIntCoreSetting( 113 WidgetFlags.KEY_ANALOG_CLOCK_SECONDS_HAND_FPS, 114 context.getResources() 115 .getInteger(com.android.internal.R.integer 116 .config_defaultAnalogClockSecondsHandFps)); 117 118 final TypedArray a = context.obtainStyledAttributes( 119 attrs, com.android.internal.R.styleable.AnalogClock, defStyleAttr, defStyleRes); 120 saveAttributeDataForStyleable(context, com.android.internal.R.styleable.AnalogClock, 121 attrs, a, defStyleAttr, defStyleRes); 122 123 mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial); 124 if (mDial == null) { 125 mDial = context.getDrawable(com.android.internal.R.drawable.clock_dial); 126 } 127 128 ColorStateList dialTintList = a.getColorStateList( 129 com.android.internal.R.styleable.AnalogClock_dialTint); 130 if (dialTintList != null) { 131 mDialTintInfo.mTintList = dialTintList; 132 mDialTintInfo.mHasTintList = true; 133 } 134 BlendMode dialTintMode = Drawable.parseBlendMode( 135 a.getInt(com.android.internal.R.styleable.AnalogClock_dialTintMode, -1), 136 null); 137 if (dialTintMode != null) { 138 mDialTintInfo.mTintBlendMode = dialTintMode; 139 mDialTintInfo.mHasTintBlendMode = true; 140 } 141 if (mDialTintInfo.mHasTintList || mDialTintInfo.mHasTintBlendMode) { 142 mDial = mDialTintInfo.apply(mDial); 143 } 144 145 mHourHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_hour); 146 if (mHourHand == null) { 147 mHourHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_hour); 148 } 149 150 ColorStateList hourHandTintList = a.getColorStateList( 151 com.android.internal.R.styleable.AnalogClock_hand_hourTint); 152 if (hourHandTintList != null) { 153 mHourHandTintInfo.mTintList = hourHandTintList; 154 mHourHandTintInfo.mHasTintList = true; 155 } 156 BlendMode hourHandTintMode = Drawable.parseBlendMode( 157 a.getInt(com.android.internal.R.styleable.AnalogClock_hand_hourTintMode, -1), 158 null); 159 if (hourHandTintMode != null) { 160 mHourHandTintInfo.mTintBlendMode = hourHandTintMode; 161 mHourHandTintInfo.mHasTintBlendMode = true; 162 } 163 if (mHourHandTintInfo.mHasTintList || mHourHandTintInfo.mHasTintBlendMode) { 164 mHourHand = mHourHandTintInfo.apply(mHourHand); 165 } 166 167 mMinuteHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_minute); 168 if (mMinuteHand == null) { 169 mMinuteHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_minute); 170 } 171 172 ColorStateList minuteHandTintList = a.getColorStateList( 173 com.android.internal.R.styleable.AnalogClock_hand_minuteTint); 174 if (minuteHandTintList != null) { 175 mMinuteHandTintInfo.mTintList = minuteHandTintList; 176 mMinuteHandTintInfo.mHasTintList = true; 177 } 178 BlendMode minuteHandTintMode = Drawable.parseBlendMode( 179 a.getInt(com.android.internal.R.styleable.AnalogClock_hand_minuteTintMode, -1), 180 null); 181 if (minuteHandTintMode != null) { 182 mMinuteHandTintInfo.mTintBlendMode = minuteHandTintMode; 183 mMinuteHandTintInfo.mHasTintBlendMode = true; 184 } 185 if (mMinuteHandTintInfo.mHasTintList || mMinuteHandTintInfo.mHasTintBlendMode) { 186 mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); 187 } 188 189 mSecondHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_second); 190 191 ColorStateList secondHandTintList = a.getColorStateList( 192 com.android.internal.R.styleable.AnalogClock_hand_secondTint); 193 if (secondHandTintList != null) { 194 mSecondHandTintInfo.mTintList = secondHandTintList; 195 mSecondHandTintInfo.mHasTintList = true; 196 } 197 BlendMode secondHandTintMode = Drawable.parseBlendMode( 198 a.getInt(com.android.internal.R.styleable.AnalogClock_hand_secondTintMode, -1), 199 null); 200 if (secondHandTintMode != null) { 201 mSecondHandTintInfo.mTintBlendMode = secondHandTintMode; 202 mSecondHandTintInfo.mHasTintBlendMode = true; 203 } 204 if (mSecondHandTintInfo.mHasTintList || mSecondHandTintInfo.mHasTintBlendMode) { 205 mSecondHand = mSecondHandTintInfo.apply(mSecondHand); 206 } 207 208 mTimeZone = toZoneId(a.getString(com.android.internal.R.styleable.AnalogClock_timeZone)); 209 createClock(); 210 211 a.recycle(); 212 213 mDialWidth = mDial.getIntrinsicWidth(); 214 mDialHeight = mDial.getIntrinsicHeight(); 215 } 216 217 /** Sets the dial of the clock to the specified Icon. */ 218 @RemotableViewMethod setDial(@onNull Icon icon)219 public void setDial(@NonNull Icon icon) { 220 mDial = icon.loadDrawable(getContext()); 221 mDialWidth = mDial.getIntrinsicWidth(); 222 mDialHeight = mDial.getIntrinsicHeight(); 223 if (mDialTintInfo.mHasTintList || mDialTintInfo.mHasTintBlendMode) { 224 mDial = mDialTintInfo.apply(mDial); 225 } 226 227 mChanged = true; 228 invalidate(); 229 } 230 231 /** 232 * Applies a tint to the dial drawable. 233 * <p> 234 * Subsequent calls to {@link #setDial(Icon)} will 235 * automatically mutate the drawable and apply the specified tint and tint 236 * mode using {@link Drawable#setTintList(ColorStateList)}. 237 * 238 * @param tint the tint to apply, may be {@code null} to clear tint 239 * 240 * @attr ref android.R.styleable#AnalogClock_dialTint 241 * @see #getDialTintList() 242 * @see Drawable#setTintList(ColorStateList) 243 */ 244 @RemotableViewMethod setDialTintList(@ullable ColorStateList tint)245 public void setDialTintList(@Nullable ColorStateList tint) { 246 mDialTintInfo.mTintList = tint; 247 mDialTintInfo.mHasTintList = true; 248 249 mDial = mDialTintInfo.apply(mDial); 250 } 251 252 /** 253 * @return the tint applied to the dial drawable 254 * @attr ref android.R.styleable#AnalogClock_dialTint 255 * @see #setDialTintList(ColorStateList) 256 */ 257 @InspectableProperty(attributeId = com.android.internal.R.styleable.AnalogClock_dialTint) 258 @Nullable getDialTintList()259 public ColorStateList getDialTintList() { 260 return mDialTintInfo.mTintList; 261 } 262 263 /** 264 * Specifies the blending mode used to apply the tint specified by 265 * {@link #setDialTintList(ColorStateList)}} to the dial drawable. 266 * The default mode is {@link BlendMode#SRC_IN}. 267 * 268 * @param blendMode the blending mode used to apply the tint, may be 269 * {@code null} to clear tint 270 * @attr ref android.R.styleable#AnalogClock_dialTintMode 271 * @see #getDialTintBlendMode() 272 * @see Drawable#setTintBlendMode(BlendMode) 273 */ 274 @RemotableViewMethod setDialTintBlendMode(@ullable BlendMode blendMode)275 public void setDialTintBlendMode(@Nullable BlendMode blendMode) { 276 mDialTintInfo.mTintBlendMode = blendMode; 277 mDialTintInfo.mHasTintBlendMode = true; 278 279 mDial = mDialTintInfo.apply(mDial); 280 } 281 282 /** 283 * @return the blending mode used to apply the tint to the dial drawable 284 * @attr ref android.R.styleable#AnalogClock_dialTintMode 285 * @see #setDialTintBlendMode(BlendMode) 286 */ 287 @InspectableProperty(attributeId = com.android.internal.R.styleable.AnalogClock_dialTintMode) 288 @Nullable getDialTintBlendMode()289 public BlendMode getDialTintBlendMode() { 290 return mDialTintInfo.mTintBlendMode; 291 } 292 293 /** Sets the hour hand of the clock to the specified Icon. */ 294 @RemotableViewMethod setHourHand(@onNull Icon icon)295 public void setHourHand(@NonNull Icon icon) { 296 mHourHand = icon.loadDrawable(getContext()); 297 if (mHourHandTintInfo.mHasTintList || mHourHandTintInfo.mHasTintBlendMode) { 298 mHourHand = mHourHandTintInfo.apply(mHourHand); 299 } 300 301 mChanged = true; 302 invalidate(); 303 } 304 305 /** 306 * Applies a tint to the hour hand drawable. 307 * <p> 308 * Subsequent calls to {@link #setHourHand(Icon)} will 309 * automatically mutate the drawable and apply the specified tint and tint 310 * mode using {@link Drawable#setTintList(ColorStateList)}. 311 * 312 * @param tint the tint to apply, may be {@code null} to clear tint 313 * 314 * @attr ref android.R.styleable#AnalogClock_hand_hourTint 315 * @see #getHourHandTintList() 316 * @see Drawable#setTintList(ColorStateList) 317 */ 318 @RemotableViewMethod setHourHandTintList(@ullable ColorStateList tint)319 public void setHourHandTintList(@Nullable ColorStateList tint) { 320 mHourHandTintInfo.mTintList = tint; 321 mHourHandTintInfo.mHasTintList = true; 322 323 mHourHand = mHourHandTintInfo.apply(mHourHand); 324 } 325 326 /** 327 * @return the tint applied to the hour hand drawable 328 * @attr ref android.R.styleable#AnalogClock_hand_hourTint 329 * @see #setHourHandTintList(ColorStateList) 330 */ 331 @InspectableProperty( 332 attributeId = com.android.internal.R.styleable.AnalogClock_hand_hourTint 333 ) 334 @Nullable getHourHandTintList()335 public ColorStateList getHourHandTintList() { 336 return mHourHandTintInfo.mTintList; 337 } 338 339 /** 340 * Specifies the blending mode used to apply the tint specified by 341 * {@link #setHourHandTintList(ColorStateList)}} to the hour hand drawable. 342 * The default mode is {@link BlendMode#SRC_IN}. 343 * 344 * @param blendMode the blending mode used to apply the tint, may be 345 * {@code null} to clear tint 346 * @attr ref android.R.styleable#AnalogClock_hand_hourTintMode 347 * @see #getHourHandTintBlendMode() 348 * @see Drawable#setTintBlendMode(BlendMode) 349 */ 350 @RemotableViewMethod setHourHandTintBlendMode(@ullable BlendMode blendMode)351 public void setHourHandTintBlendMode(@Nullable BlendMode blendMode) { 352 mHourHandTintInfo.mTintBlendMode = blendMode; 353 mHourHandTintInfo.mHasTintBlendMode = true; 354 355 mHourHand = mHourHandTintInfo.apply(mHourHand); 356 } 357 358 /** 359 * @return the blending mode used to apply the tint to the hour hand drawable 360 * @attr ref android.R.styleable#AnalogClock_hand_hourTintMode 361 * @see #setHourHandTintBlendMode(BlendMode) 362 */ 363 @InspectableProperty( 364 attributeId = com.android.internal.R.styleable.AnalogClock_hand_hourTintMode) 365 @Nullable getHourHandTintBlendMode()366 public BlendMode getHourHandTintBlendMode() { 367 return mHourHandTintInfo.mTintBlendMode; 368 } 369 370 /** Sets the minute hand of the clock to the specified Icon. */ 371 @RemotableViewMethod setMinuteHand(@onNull Icon icon)372 public void setMinuteHand(@NonNull Icon icon) { 373 mMinuteHand = icon.loadDrawable(getContext()); 374 if (mMinuteHandTintInfo.mHasTintList || mMinuteHandTintInfo.mHasTintBlendMode) { 375 mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); 376 } 377 378 mChanged = true; 379 invalidate(); 380 } 381 382 /** 383 * Applies a tint to the minute hand drawable. 384 * <p> 385 * Subsequent calls to {@link #setMinuteHand(Icon)} will 386 * automatically mutate the drawable and apply the specified tint and tint 387 * mode using {@link Drawable#setTintList(ColorStateList)}. 388 * 389 * @param tint the tint to apply, may be {@code null} to clear tint 390 * 391 * @attr ref android.R.styleable#AnalogClock_hand_minuteTint 392 * @see #getMinuteHandTintList() 393 * @see Drawable#setTintList(ColorStateList) 394 */ 395 @RemotableViewMethod setMinuteHandTintList(@ullable ColorStateList tint)396 public void setMinuteHandTintList(@Nullable ColorStateList tint) { 397 mMinuteHandTintInfo.mTintList = tint; 398 mMinuteHandTintInfo.mHasTintList = true; 399 400 mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); 401 } 402 403 /** 404 * @return the tint applied to the minute hand drawable 405 * @attr ref android.R.styleable#AnalogClock_hand_minuteTint 406 * @see #setMinuteHandTintList(ColorStateList) 407 */ 408 @InspectableProperty( 409 attributeId = com.android.internal.R.styleable.AnalogClock_hand_minuteTint 410 ) 411 @Nullable getMinuteHandTintList()412 public ColorStateList getMinuteHandTintList() { 413 return mMinuteHandTintInfo.mTintList; 414 } 415 416 /** 417 * Specifies the blending mode used to apply the tint specified by 418 * {@link #setMinuteHandTintList(ColorStateList)}} to the minute hand drawable. 419 * The default mode is {@link BlendMode#SRC_IN}. 420 * 421 * @param blendMode the blending mode used to apply the tint, may be 422 * {@code null} to clear tint 423 * @attr ref android.R.styleable#AnalogClock_hand_minuteTintMode 424 * @see #getMinuteHandTintBlendMode() 425 * @see Drawable#setTintBlendMode(BlendMode) 426 */ 427 @RemotableViewMethod setMinuteHandTintBlendMode(@ullable BlendMode blendMode)428 public void setMinuteHandTintBlendMode(@Nullable BlendMode blendMode) { 429 mMinuteHandTintInfo.mTintBlendMode = blendMode; 430 mMinuteHandTintInfo.mHasTintBlendMode = true; 431 432 mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); 433 } 434 435 /** 436 * @return the blending mode used to apply the tint to the minute hand drawable 437 * @attr ref android.R.styleable#AnalogClock_hand_minuteTintMode 438 * @see #setMinuteHandTintBlendMode(BlendMode) 439 */ 440 @InspectableProperty( 441 attributeId = com.android.internal.R.styleable.AnalogClock_hand_minuteTintMode) 442 @Nullable getMinuteHandTintBlendMode()443 public BlendMode getMinuteHandTintBlendMode() { 444 return mMinuteHandTintInfo.mTintBlendMode; 445 } 446 447 /** 448 * Sets the second hand of the clock to the specified Icon, or hides the second hand if it is 449 * null. 450 */ 451 @RemotableViewMethod setSecondHand(@ullable Icon icon)452 public void setSecondHand(@Nullable Icon icon) { 453 mSecondHand = icon == null ? null : icon.loadDrawable(getContext()); 454 if (mSecondHandTintInfo.mHasTintList || mSecondHandTintInfo.mHasTintBlendMode) { 455 mSecondHand = mSecondHandTintInfo.apply(mSecondHand); 456 } 457 // Re-run the tick runnable immediately as the presence or absence of a seconds hand affects 458 // the next time we need to tick the clock. 459 mTick.run(); 460 461 mChanged = true; 462 invalidate(); 463 } 464 465 /** 466 * Applies a tint to the second hand drawable. 467 * <p> 468 * Subsequent calls to {@link #setSecondHand(Icon)} will 469 * automatically mutate the drawable and apply the specified tint and tint 470 * mode using {@link Drawable#setTintList(ColorStateList)}. 471 * 472 * @param tint the tint to apply, may be {@code null} to clear tint 473 * 474 * @attr ref android.R.styleable#AnalogClock_hand_secondTint 475 * @see #getSecondHandTintList() 476 * @see Drawable#setTintList(ColorStateList) 477 */ 478 @RemotableViewMethod setSecondHandTintList(@ullable ColorStateList tint)479 public void setSecondHandTintList(@Nullable ColorStateList tint) { 480 mSecondHandTintInfo.mTintList = tint; 481 mSecondHandTintInfo.mHasTintList = true; 482 483 mSecondHand = mSecondHandTintInfo.apply(mSecondHand); 484 } 485 486 /** 487 * @return the tint applied to the second hand drawable 488 * @attr ref android.R.styleable#AnalogClock_hand_secondTint 489 * @see #setSecondHandTintList(ColorStateList) 490 */ 491 @InspectableProperty( 492 attributeId = com.android.internal.R.styleable.AnalogClock_hand_secondTint 493 ) 494 @Nullable getSecondHandTintList()495 public ColorStateList getSecondHandTintList() { 496 return mSecondHandTintInfo.mTintList; 497 } 498 499 /** 500 * Specifies the blending mode used to apply the tint specified by 501 * {@link #setSecondHandTintList(ColorStateList)}} to the second hand drawable. 502 * The default mode is {@link BlendMode#SRC_IN}. 503 * 504 * @param blendMode the blending mode used to apply the tint, may be 505 * {@code null} to clear tint 506 * @attr ref android.R.styleable#AnalogClock_hand_secondTintMode 507 * @see #getSecondHandTintBlendMode() 508 * @see Drawable#setTintBlendMode(BlendMode) 509 */ 510 @RemotableViewMethod setSecondHandTintBlendMode(@ullable BlendMode blendMode)511 public void setSecondHandTintBlendMode(@Nullable BlendMode blendMode) { 512 mSecondHandTintInfo.mTintBlendMode = blendMode; 513 mSecondHandTintInfo.mHasTintBlendMode = true; 514 515 mSecondHand = mSecondHandTintInfo.apply(mSecondHand); 516 } 517 518 /** 519 * @return the blending mode used to apply the tint to the second hand drawable 520 * @attr ref android.R.styleable#AnalogClock_hand_secondTintMode 521 * @see #setSecondHandTintBlendMode(BlendMode) 522 */ 523 @InspectableProperty( 524 attributeId = com.android.internal.R.styleable.AnalogClock_hand_secondTintMode) 525 @Nullable getSecondHandTintBlendMode()526 public BlendMode getSecondHandTintBlendMode() { 527 return mSecondHandTintInfo.mTintBlendMode; 528 } 529 530 /** 531 * Indicates which time zone is currently used by this view. 532 * 533 * @return The ID of the current time zone or null if the default time zone, 534 * as set by the user, must be used 535 * 536 * @see java.util.TimeZone 537 * @see java.util.TimeZone#getAvailableIDs() 538 * @see #setTimeZone(String) 539 */ 540 @InspectableProperty 541 @Nullable getTimeZone()542 public String getTimeZone() { 543 ZoneId zoneId = mTimeZone; 544 return zoneId == null ? null : zoneId.getId(); 545 } 546 547 /** 548 * Sets the specified time zone to use in this clock. When the time zone 549 * is set through this method, system time zone changes (when the user 550 * sets the time zone in settings for instance) will be ignored. 551 * 552 * @param timeZone The desired time zone's ID as specified in {@link java.util.TimeZone} 553 * or null to user the time zone specified by the user 554 * (system time zone) 555 * 556 * @see #getTimeZone() 557 * @see java.util.TimeZone#getAvailableIDs() 558 * @see java.util.TimeZone#getTimeZone(String) 559 * 560 * @attr ref android.R.styleable#AnalogClock_timeZone 561 */ 562 @RemotableViewMethod setTimeZone(@ullable String timeZone)563 public void setTimeZone(@Nullable String timeZone) { 564 mTimeZone = toZoneId(timeZone); 565 566 createClock(); 567 onTimeChanged(); 568 } 569 570 @Override onVisibilityAggregated(boolean isVisible)571 public void onVisibilityAggregated(boolean isVisible) { 572 super.onVisibilityAggregated(isVisible); 573 574 if (isVisible) { 575 onVisible(); 576 } else { 577 onInvisible(); 578 } 579 } 580 581 @Override onAttachedToWindow()582 protected void onAttachedToWindow() { 583 super.onAttachedToWindow(); 584 IntentFilter filter = new IntentFilter(); 585 586 if (!mReceiverAttached) { 587 filter.addAction(Intent.ACTION_TIME_CHANGED); 588 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 589 590 // OK, this is gross but needed. This class is supported by the 591 // remote views mechanism and as a part of that the remote views 592 // can be inflated by a context for another user without the app 593 // having interact users permission - just for loading resources. 594 // For example, when adding widgets from a user profile to the 595 // home screen. Therefore, we register the receiver as the current 596 // user not the one the context is for. 597 getContext().registerReceiverAsUser(mIntentReceiver, 598 android.os.Process.myUserHandle(), filter, null, getHandler()); 599 mReceiverAttached = true; 600 } 601 602 // NOTE: It's safe to do these after registering the receiver since the receiver always runs 603 // in the main thread, therefore the receiver can't run before this method returns. 604 605 // The time zone may have changed while the receiver wasn't registered, so update the clock. 606 createClock(); 607 608 // Make sure we update to the current time 609 onTimeChanged(); 610 } 611 612 @Override onDetachedFromWindow()613 protected void onDetachedFromWindow() { 614 if (mReceiverAttached) { 615 getContext().unregisterReceiver(mIntentReceiver); 616 mReceiverAttached = false; 617 } 618 super.onDetachedFromWindow(); 619 } 620 onVisible()621 private void onVisible() { 622 if (!mVisible) { 623 mVisible = true; 624 mTick.run(); 625 } 626 627 } 628 onInvisible()629 private void onInvisible() { 630 if (mVisible) { 631 removeCallbacks(mTick); 632 mVisible = false; 633 } 634 } 635 636 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)637 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 638 639 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 640 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 641 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 642 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 643 644 float hScale = 1.0f; 645 float vScale = 1.0f; 646 647 if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) { 648 hScale = (float) widthSize / (float) mDialWidth; 649 } 650 651 if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) { 652 vScale = (float )heightSize / (float) mDialHeight; 653 } 654 655 float scale = Math.min(hScale, vScale); 656 657 setMeasuredDimension(resolveSizeAndState((int) (mDialWidth * scale), widthMeasureSpec, 0), 658 resolveSizeAndState((int) (mDialHeight * scale), heightMeasureSpec, 0)); 659 } 660 661 @Override onSizeChanged(int w, int h, int oldw, int oldh)662 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 663 super.onSizeChanged(w, h, oldw, oldh); 664 mChanged = true; 665 } 666 667 @Override onDraw(Canvas canvas)668 protected void onDraw(Canvas canvas) { 669 super.onDraw(canvas); 670 671 boolean changed = mChanged; 672 if (changed) { 673 mChanged = false; 674 } 675 676 int availableWidth = mRight - mLeft; 677 int availableHeight = mBottom - mTop; 678 679 int x = availableWidth / 2; 680 int y = availableHeight / 2; 681 682 final Drawable dial = mDial; 683 int w = dial.getIntrinsicWidth(); 684 int h = dial.getIntrinsicHeight(); 685 686 boolean scaled = false; 687 688 if (availableWidth < w || availableHeight < h) { 689 scaled = true; 690 float scale = Math.min((float) availableWidth / (float) w, 691 (float) availableHeight / (float) h); 692 canvas.save(); 693 canvas.scale(scale, scale, x, y); 694 } 695 696 if (changed) { 697 dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 698 } 699 dial.draw(canvas); 700 701 canvas.save(); 702 canvas.rotate(mHour / 12.0f * 360.0f, x, y); 703 final Drawable hourHand = mHourHand; 704 if (changed) { 705 w = hourHand.getIntrinsicWidth(); 706 h = hourHand.getIntrinsicHeight(); 707 hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 708 } 709 hourHand.draw(canvas); 710 canvas.restore(); 711 712 canvas.save(); 713 canvas.rotate(mMinutes / 60.0f * 360.0f, x, y); 714 715 final Drawable minuteHand = mMinuteHand; 716 if (changed) { 717 w = minuteHand.getIntrinsicWidth(); 718 h = minuteHand.getIntrinsicHeight(); 719 minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 720 } 721 minuteHand.draw(canvas); 722 canvas.restore(); 723 724 final Drawable secondHand = mSecondHand; 725 if (secondHand != null && mSecondsHandFps > 0) { 726 canvas.save(); 727 canvas.rotate(mSeconds / 60.0f * 360.0f, x, y); 728 729 if (changed) { 730 w = secondHand.getIntrinsicWidth(); 731 h = secondHand.getIntrinsicHeight(); 732 secondHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 733 } 734 secondHand.draw(canvas); 735 canvas.restore(); 736 } 737 738 if (scaled) { 739 canvas.restore(); 740 } 741 } 742 743 /** 744 * Return the current Instant to be used for drawing the clockface. Protected to allow 745 * subclasses to override this to show a different time from the system clock. 746 * 747 * @return the Instant to be shown on the clockface 748 * @hide 749 */ now()750 protected Instant now() { 751 return mClock.instant(); 752 } 753 754 /** 755 * @hide 756 */ onTimeChanged()757 protected void onTimeChanged() { 758 Instant now = now(); 759 onTimeChanged(now.atZone(mClock.getZone()).toLocalTime(), now.toEpochMilli()); 760 } 761 onTimeChanged(LocalTime localTime, long nowMillis)762 private void onTimeChanged(LocalTime localTime, long nowMillis) { 763 float previousHour = mHour; 764 float previousMinutes = mMinutes; 765 766 float rawSeconds = localTime.getSecond() + localTime.getNano() / 1_000_000_000f; 767 // We round the fraction of the second so that the seconds hand always occupies the same 768 // n positions between two given numbers, where n is the number of ticks per second. This 769 // ensures the second hand advances by a consistent distance despite our handler callbacks 770 // occurring at inconsistent frequencies. 771 mSeconds = 772 mSecondsHandFps <= 0 773 ? rawSeconds 774 : Math.round(rawSeconds * mSecondsHandFps) / (float) mSecondsHandFps; 775 mMinutes = localTime.getMinute() + mSeconds / 60.0f; 776 mHour = localTime.getHour() + mMinutes / 60.0f; 777 mChanged = true; 778 779 // Update the content description only if the announced hours and minutes have changed. 780 if ((int) previousHour != (int) mHour || (int) previousMinutes != (int) mMinutes) { 781 updateContentDescription(nowMillis); 782 } 783 } 784 785 /** Intent receiver for the time or time zone changing. */ 786 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 787 @Override 788 public void onReceive(Context context, Intent intent) { 789 if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { 790 createClock(); 791 } 792 793 mTick.run(); 794 } 795 }; 796 private boolean mReceiverAttached; 797 798 private final Runnable mTick = new Runnable() { 799 @Override 800 public void run() { 801 removeCallbacks(this); 802 if (!mVisible) { 803 return; 804 } 805 806 Instant now = now(); 807 ZonedDateTime zonedDateTime = now.atZone(mClock.getZone()); 808 LocalTime localTime = zonedDateTime.toLocalTime(); 809 810 long millisUntilNextTick; 811 if (mSecondHand == null || mSecondsHandFps <= 0) { 812 // If there's no second hand, then tick at the start of the next minute. 813 // 814 // This must be done with ZonedDateTime as opposed to LocalDateTime to ensure proper 815 // handling of DST. Also note that because of leap seconds, it should not be assumed 816 // that one minute == 60 seconds. 817 Instant startOfNextMinute = zonedDateTime.plusMinutes(1).withSecond(0).toInstant(); 818 millisUntilNextTick = Duration.between(now, startOfNextMinute).toMillis(); 819 if (millisUntilNextTick <= 0) { 820 // This should never occur, but if it does, then just check the tick again in 821 // one minute to ensure we're always moving forward. 822 millisUntilNextTick = Duration.ofMinutes(1).toMillis(); 823 } 824 } else { 825 // If there is a seconds hand, then determine the next tick point based on the fps. 826 // 827 // How many milliseconds through the second we currently are. 828 long millisOfSecond = Duration.ofNanos(localTime.getNano()).toMillis(); 829 // How many milliseconds there are between tick positions for the seconds hand. 830 double millisPerTick = 1000 / (double) mSecondsHandFps; 831 // How many milliseconds we are past the last tick position. 832 long millisPastLastTick = Math.round(millisOfSecond % millisPerTick); 833 // How many milliseconds there are until the next tick position. 834 millisUntilNextTick = Math.round(millisPerTick - millisPastLastTick); 835 // If we are exactly at the tick position, this could be 0 milliseconds due to 836 // rounding. In this case, advance by the full amount of millis to the next 837 // position. 838 if (millisUntilNextTick <= 0) { 839 millisUntilNextTick = Math.round(millisPerTick); 840 } 841 } 842 843 // Schedule a callback for when the next tick should occur. 844 postDelayed(this, millisUntilNextTick); 845 846 onTimeChanged(localTime, now.toEpochMilli()); 847 848 invalidate(); 849 } 850 }; 851 createClock()852 private void createClock() { 853 ZoneId zoneId = mTimeZone; 854 if (zoneId == null) { 855 mClock = Clock.systemDefaultZone(); 856 } else { 857 mClock = Clock.system(zoneId); 858 } 859 } 860 updateContentDescription(long timeMillis)861 private void updateContentDescription(long timeMillis) { 862 final int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_24HOUR; 863 String contentDescription = 864 DateUtils.formatDateRange( 865 mContext, 866 new Formatter(new StringBuilder(50), Locale.getDefault()), 867 timeMillis /* startMillis */, 868 timeMillis /* endMillis */, 869 flags, 870 getTimeZone()) 871 .toString(); 872 setContentDescription(contentDescription); 873 } 874 875 /** 876 * Tries to parse a {@link ZoneId} from {@code timeZone}, returning null if it is null or there 877 * is an error parsing. 878 */ 879 @Nullable toZoneId(@ullable String timeZone)880 private static ZoneId toZoneId(@Nullable String timeZone) { 881 if (timeZone == null) { 882 return null; 883 } 884 885 try { 886 return ZoneId.of(timeZone); 887 } catch (DateTimeException e) { 888 Log.w(LOG_TAG, "Failed to parse time zone from " + timeZone, e); 889 return null; 890 } 891 } 892 893 private final class TintInfo { 894 boolean mHasTintList; 895 @Nullable ColorStateList mTintList; 896 boolean mHasTintBlendMode; 897 @Nullable BlendMode mTintBlendMode; 898 899 /** 900 * Returns a mutated copy of {@code drawable} with tinting applied, or null if it's null. 901 */ 902 @Nullable apply(@ullable Drawable drawable)903 Drawable apply(@Nullable Drawable drawable) { 904 if (drawable == null) return null; 905 906 Drawable newDrawable = drawable.mutate(); 907 908 if (mHasTintList) { 909 newDrawable.setTintList(mTintList); 910 } 911 912 if (mHasTintBlendMode) { 913 newDrawable.setTintBlendMode(mTintBlendMode); 914 } 915 916 // All drawables should have the same state as the View itself. 917 if (drawable.isStateful()) { 918 newDrawable.setState(getDrawableState()); 919 } 920 921 return newDrawable; 922 } 923 } 924 } 925