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