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