1 /*
2  * Copyright (C) 2012 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 static android.view.ViewDebug.ExportedProperty;
20 import static android.widget.RemoteViews.RemoteView;
21 
22 import android.annotation.NonNull;
23 import android.annotation.TestApi;
24 import android.app.ActivityManager;
25 import android.compat.annotation.UnsupportedAppUsage;
26 import android.content.BroadcastReceiver;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.res.TypedArray;
32 import android.database.ContentObserver;
33 import android.net.Uri;
34 import android.os.Handler;
35 import android.os.SystemClock;
36 import android.os.UserHandle;
37 import android.provider.Settings;
38 import android.text.format.DateFormat;
39 import android.util.AttributeSet;
40 import android.view.RemotableViewMethod;
41 import android.view.ViewHierarchyEncoder;
42 import android.view.inspector.InspectableProperty;
43 
44 import com.android.internal.R;
45 
46 import libcore.icu.LocaleData;
47 
48 import java.util.Calendar;
49 import java.util.TimeZone;
50 
51 /**
52  * <p><code>TextClock</code> can display the current date and/or time as
53  * a formatted string.</p>
54  *
55  * <p>This view honors the 24-hour format system setting. As such, it is
56  * possible and recommended to provide two different formatting patterns:
57  * one to display the date/time in 24-hour mode and one to display the
58  * date/time in 12-hour mode. Most callers will want to use the defaults,
59  * though, which will be appropriate for the user's locale.</p>
60  *
61  * <p>It is possible to determine whether the system is currently in
62  * 24-hour mode by calling {@link #is24HourModeEnabled()}.</p>
63  *
64  * <p>The rules used by this widget to decide how to format the date and
65  * time are the following:</p>
66  * <ul>
67  *     <li>In 24-hour mode:
68  *         <ul>
69  *             <li>Use the value returned by {@link #getFormat24Hour()} when non-null</li>
70  *             <li>Otherwise, use the value returned by {@link #getFormat12Hour()} when non-null</li>
71  *             <li>Otherwise, use a default value appropriate for the user's locale, such as {@code h:mm a}</li>
72  *         </ul>
73  *     </li>
74  *     <li>In 12-hour mode:
75  *         <ul>
76  *             <li>Use the value returned by {@link #getFormat12Hour()} when non-null</li>
77  *             <li>Otherwise, use the value returned by {@link #getFormat24Hour()} when non-null</li>
78  *             <li>Otherwise, use a default value appropriate for the user's locale, such as {@code HH:mm}</li>
79  *         </ul>
80  *     </li>
81  * </ul>
82  *
83  * <p>The {@link CharSequence} instances used as formatting patterns when calling either
84  * {@link #setFormat24Hour(CharSequence)} or {@link #setFormat12Hour(CharSequence)} can
85  * contain styling information. To do so, use a {@link android.text.Spanned} object.
86  * Note that if you customize these strings, it is your responsibility to supply strings
87  * appropriate for formatting dates and/or times in the user's locale.</p>
88  *
89  * @attr ref android.R.styleable#TextClock_format12Hour
90  * @attr ref android.R.styleable#TextClock_format24Hour
91  * @attr ref android.R.styleable#TextClock_timeZone
92  */
93 @RemoteView
94 public class TextClock extends TextView {
95     /**
96      * The default formatting pattern in 12-hour mode. This pattern is used
97      * if {@link #setFormat12Hour(CharSequence)} is called with a null pattern
98      * or if no pattern was specified when creating an instance of this class.
99      *
100      * This default pattern shows only the time, hours and minutes, and an am/pm
101      * indicator.
102      *
103      * @see #setFormat12Hour(CharSequence)
104      * @see #getFormat12Hour()
105      *
106      * @deprecated Let the system use locale-appropriate defaults instead.
107      */
108     @Deprecated
109     public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
110 
111     /**
112      * The default formatting pattern in 24-hour mode. This pattern is used
113      * if {@link #setFormat24Hour(CharSequence)} is called with a null pattern
114      * or if no pattern was specified when creating an instance of this class.
115      *
116      * This default pattern shows only the time, hours and minutes.
117      *
118      * @see #setFormat24Hour(CharSequence)
119      * @see #getFormat24Hour()
120      *
121      * @deprecated Let the system use locale-appropriate defaults instead.
122      */
123     @Deprecated
124     public static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
125 
126     private CharSequence mFormat12;
127     private CharSequence mFormat24;
128     private CharSequence mDescFormat12;
129     private CharSequence mDescFormat24;
130 
131     @ExportedProperty
132     private CharSequence mFormat;
133     @ExportedProperty
134     private boolean mHasSeconds;
135 
136     private CharSequence mDescFormat;
137 
138     private boolean mRegistered;
139     private boolean mShouldRunTicker;
140 
141     private Calendar mTime;
142     private String mTimeZone;
143 
144     private boolean mShowCurrentUserTime;
145 
146     private ContentObserver mFormatChangeObserver;
147     // Used by tests to stop time change events from triggering the text update
148     private boolean mStopTicking;
149 
150     private class FormatChangeObserver extends ContentObserver {
151 
FormatChangeObserver(Handler handler)152         public FormatChangeObserver(Handler handler) {
153             super(handler);
154         }
155 
156         @Override
onChange(boolean selfChange)157         public void onChange(boolean selfChange) {
158             chooseFormat();
159             onTimeChanged();
160         }
161 
162         @Override
onChange(boolean selfChange, Uri uri)163         public void onChange(boolean selfChange, Uri uri) {
164             chooseFormat();
165             onTimeChanged();
166         }
167     };
168 
169     private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
170         @Override
171         public void onReceive(Context context, Intent intent) {
172             if (mStopTicking) {
173                 return; // Test disabled the clock ticks
174             }
175             if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
176                 final String timeZone = intent.getStringExtra(Intent.EXTRA_TIMEZONE);
177                 createTime(timeZone);
178             } else if (!mShouldRunTicker && (Intent.ACTION_TIME_TICK.equals(intent.getAction())
179                     || Intent.ACTION_TIME_CHANGED.equals(intent.getAction()))) {
180                 return;
181             }
182             onTimeChanged();
183         }
184     };
185 
186     private final Runnable mTicker = new Runnable() {
187         public void run() {
188             if (mStopTicking) {
189                 return; // Test disabled the clock ticks
190             }
191             onTimeChanged();
192 
193             long now = SystemClock.uptimeMillis();
194             long next = now + (1000 - now % 1000);
195 
196             Handler handler = getHandler();
197             if (handler != null) {
198                 handler.postAtTime(mTicker, next);
199             }
200         }
201     };
202 
203     /**
204      * Creates a new clock using the default patterns for the current locale.
205      *
206      * @param context The Context the view is running in, through which it can
207      *        access the current theme, resources, etc.
208      */
209     @SuppressWarnings("UnusedDeclaration")
TextClock(Context context)210     public TextClock(Context context) {
211         super(context);
212         init();
213     }
214 
215     /**
216      * Creates a new clock inflated from XML. This object's properties are
217      * intialized from the attributes specified in XML.
218      *
219      * This constructor uses a default style of 0, so the only attribute values
220      * applied are those in the Context's Theme and the given AttributeSet.
221      *
222      * @param context The Context the view is running in, through which it can
223      *        access the current theme, resources, etc.
224      * @param attrs The attributes of the XML tag that is inflating the view
225      */
226     @SuppressWarnings("UnusedDeclaration")
TextClock(Context context, AttributeSet attrs)227     public TextClock(Context context, AttributeSet attrs) {
228         this(context, attrs, 0);
229     }
230 
231     /**
232      * Creates a new clock inflated from XML. This object's properties are
233      * intialized from the attributes specified in XML.
234      *
235      * @param context The Context the view is running in, through which it can
236      *        access the current theme, resources, etc.
237      * @param attrs The attributes of the XML tag that is inflating the view
238      * @param defStyleAttr An attribute in the current theme that contains a
239      *        reference to a style resource that supplies default values for
240      *        the view. Can be 0 to not look for defaults.
241      */
TextClock(Context context, AttributeSet attrs, int defStyleAttr)242     public TextClock(Context context, AttributeSet attrs, int defStyleAttr) {
243         this(context, attrs, defStyleAttr, 0);
244     }
245 
TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)246     public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
247         super(context, attrs, defStyleAttr, defStyleRes);
248 
249         final TypedArray a = context.obtainStyledAttributes(
250                 attrs, R.styleable.TextClock, defStyleAttr, defStyleRes);
251         saveAttributeDataForStyleable(context, R.styleable.TextClock,
252                 attrs, a, defStyleAttr, defStyleRes);
253         try {
254             mFormat12 = a.getText(R.styleable.TextClock_format12Hour);
255             mFormat24 = a.getText(R.styleable.TextClock_format24Hour);
256             mTimeZone = a.getString(R.styleable.TextClock_timeZone);
257         } finally {
258             a.recycle();
259         }
260 
261         init();
262     }
263 
init()264     private void init() {
265         if (mFormat12 == null || mFormat24 == null) {
266             LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
267             if (mFormat12 == null) {
268                 mFormat12 = ld.timeFormat_hm;
269             }
270             if (mFormat24 == null) {
271                 mFormat24 = ld.timeFormat_Hm;
272             }
273         }
274 
275         createTime(mTimeZone);
276         chooseFormat();
277     }
278 
createTime(String timeZone)279     private void createTime(String timeZone) {
280         if (timeZone != null) {
281             mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
282         } else {
283             mTime = Calendar.getInstance();
284         }
285     }
286 
287     /**
288      * Returns the formatting pattern used to display the date and/or time
289      * in 12-hour mode. The formatting pattern syntax is described in
290      * {@link DateFormat}.
291      *
292      * @return A {@link CharSequence} or null.
293      *
294      * @see #setFormat12Hour(CharSequence)
295      * @see #is24HourModeEnabled()
296      */
297     @InspectableProperty
298     @ExportedProperty
getFormat12Hour()299     public CharSequence getFormat12Hour() {
300         return mFormat12;
301     }
302 
303     /**
304      * <p>Specifies the formatting pattern used to display the date and/or time
305      * in 12-hour mode. The formatting pattern syntax is described in
306      * {@link DateFormat}.</p>
307      *
308      * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
309      * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
310      * are set to null, the default pattern for the current locale will be used
311      * instead.</p>
312      *
313      * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
314      * you supply a format string generated by
315      * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
316      * takes care of generating a format string adapted to the desired locale.</p>
317      *
318      *
319      * @param format A date/time formatting pattern as described in {@link DateFormat}
320      *
321      * @see #getFormat12Hour()
322      * @see #is24HourModeEnabled()
323      * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
324      * @see DateFormat
325      *
326      * @attr ref android.R.styleable#TextClock_format12Hour
327      */
328     @RemotableViewMethod
setFormat12Hour(CharSequence format)329     public void setFormat12Hour(CharSequence format) {
330         mFormat12 = format;
331 
332         chooseFormat();
333         onTimeChanged();
334     }
335 
336     /**
337      * Like setFormat12Hour, but for the content description.
338      * @hide
339      */
setContentDescriptionFormat12Hour(CharSequence format)340     public void setContentDescriptionFormat12Hour(CharSequence format) {
341         mDescFormat12 = format;
342 
343         chooseFormat();
344         onTimeChanged();
345     }
346 
347     /**
348      * Returns the formatting pattern used to display the date and/or time
349      * in 24-hour mode. The formatting pattern syntax is described in
350      * {@link DateFormat}.
351      *
352      * @return A {@link CharSequence} or null.
353      *
354      * @see #setFormat24Hour(CharSequence)
355      * @see #is24HourModeEnabled()
356      */
357     @InspectableProperty
358     @ExportedProperty
getFormat24Hour()359     public CharSequence getFormat24Hour() {
360         return mFormat24;
361     }
362 
363     /**
364      * <p>Specifies the formatting pattern used to display the date and/or time
365      * in 24-hour mode. The formatting pattern syntax is described in
366      * {@link DateFormat}.</p>
367      *
368      * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
369      * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
370      * are set to null, the default pattern for the current locale will be used
371      * instead.</p>
372      *
373      * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
374      * you supply a format string generated by
375      * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
376      * takes care of generating a format string adapted to the desired locale.</p>
377      *
378      * @param format A date/time formatting pattern as described in {@link DateFormat}
379      *
380      * @see #getFormat24Hour()
381      * @see #is24HourModeEnabled()
382      * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
383      * @see DateFormat
384      *
385      * @attr ref android.R.styleable#TextClock_format24Hour
386      */
387     @RemotableViewMethod
setFormat24Hour(CharSequence format)388     public void setFormat24Hour(CharSequence format) {
389         mFormat24 = format;
390 
391         chooseFormat();
392         onTimeChanged();
393     }
394 
395     /**
396      * Like setFormat24Hour, but for the content description.
397      * @hide
398      */
setContentDescriptionFormat24Hour(CharSequence format)399     public void setContentDescriptionFormat24Hour(CharSequence format) {
400         mDescFormat24 = format;
401 
402         chooseFormat();
403         onTimeChanged();
404     }
405 
406     /**
407      * Sets whether this clock should always track the current user and not the user of the
408      * current process. This is used for single instance processes like the systemUI who need
409      * to display time for different users.
410      *
411      * @hide
412      */
setShowCurrentUserTime(boolean showCurrentUserTime)413     public void setShowCurrentUserTime(boolean showCurrentUserTime) {
414         mShowCurrentUserTime = showCurrentUserTime;
415 
416         chooseFormat();
417         onTimeChanged();
418         unregisterObserver();
419         registerObserver();
420     }
421 
422     /**
423      * Update the displayed time if necessary and invalidate the view.
424      */
refreshTime()425     public void refreshTime() {
426         onTimeChanged();
427         invalidate();
428     }
429 
430     /**
431      * Indicates whether the system is currently using the 24-hour mode.
432      *
433      * When the system is in 24-hour mode, this view will use the pattern
434      * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern
435      * returned by {@link #getFormat12Hour()} is used instead.
436      *
437      * If either one of the formats is null, the other format is used. If
438      * both formats are null, the default formats for the current locale are used.
439      *
440      * @return true if time should be displayed in 24-hour format, false if it
441      *         should be displayed in 12-hour format.
442      *
443      * @see #setFormat12Hour(CharSequence)
444      * @see #getFormat12Hour()
445      * @see #setFormat24Hour(CharSequence)
446      * @see #getFormat24Hour()
447      */
448     @InspectableProperty(hasAttributeId = false)
is24HourModeEnabled()449     public boolean is24HourModeEnabled() {
450         if (mShowCurrentUserTime) {
451             return DateFormat.is24HourFormat(getContext(), ActivityManager.getCurrentUser());
452         } else {
453             return DateFormat.is24HourFormat(getContext());
454         }
455     }
456 
457     /**
458      * Indicates which time zone is currently used by this view.
459      *
460      * @return The ID of the current time zone or null if the default time zone,
461      *         as set by the user, must be used
462      *
463      * @see TimeZone
464      * @see java.util.TimeZone#getAvailableIDs()
465      * @see #setTimeZone(String)
466      */
467     @InspectableProperty
getTimeZone()468     public String getTimeZone() {
469         return mTimeZone;
470     }
471 
472     /**
473      * Sets the specified time zone to use in this clock. When the time zone
474      * is set through this method, system time zone changes (when the user
475      * sets the time zone in settings for instance) will be ignored.
476      *
477      * @param timeZone The desired time zone's ID as specified in {@link TimeZone}
478      *                 or null to user the time zone specified by the user
479      *                 (system time zone)
480      *
481      * @see #getTimeZone()
482      * @see java.util.TimeZone#getAvailableIDs()
483      * @see TimeZone#getTimeZone(String)
484      *
485      * @attr ref android.R.styleable#TextClock_timeZone
486      */
487     @RemotableViewMethod
setTimeZone(String timeZone)488     public void setTimeZone(String timeZone) {
489         mTimeZone = timeZone;
490 
491         createTime(timeZone);
492         onTimeChanged();
493     }
494 
495     /**
496      * Returns the current format string. Always valid after constructor has
497      * finished, and will never be {@code null}.
498      *
499      * @hide
500      */
501     @UnsupportedAppUsage
getFormat()502     public CharSequence getFormat() {
503         return mFormat;
504     }
505 
506     /**
507      * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
508      * depending on whether the user has selected 24-hour format.
509      */
chooseFormat()510     private void chooseFormat() {
511         final boolean format24Requested = is24HourModeEnabled();
512 
513         LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
514 
515         if (format24Requested) {
516             mFormat = abc(mFormat24, mFormat12, ld.timeFormat_Hm);
517             mDescFormat = abc(mDescFormat24, mDescFormat12, mFormat);
518         } else {
519             mFormat = abc(mFormat12, mFormat24, ld.timeFormat_hm);
520             mDescFormat = abc(mDescFormat12, mDescFormat24, mFormat);
521         }
522 
523         boolean hadSeconds = mHasSeconds;
524         mHasSeconds = DateFormat.hasSeconds(mFormat);
525 
526         if (mShouldRunTicker && hadSeconds != mHasSeconds) {
527             if (hadSeconds) getHandler().removeCallbacks(mTicker);
528             else mTicker.run();
529         }
530     }
531 
532     /**
533      * Returns a if not null, else return b if not null, else return c.
534      */
abc(CharSequence a, CharSequence b, CharSequence c)535     private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) {
536         return a == null ? (b == null ? c : b) : a;
537     }
538 
539     @Override
onAttachedToWindow()540     protected void onAttachedToWindow() {
541         super.onAttachedToWindow();
542 
543         if (!mRegistered) {
544             mRegistered = true;
545 
546             registerReceiver();
547             registerObserver();
548 
549             createTime(mTimeZone);
550         }
551     }
552 
553     @Override
onVisibilityAggregated(boolean isVisible)554     public void onVisibilityAggregated(boolean isVisible) {
555         super.onVisibilityAggregated(isVisible);
556 
557         if (!mShouldRunTicker && isVisible) {
558             mShouldRunTicker = true;
559             if (mHasSeconds) {
560                 mTicker.run();
561             } else {
562                 onTimeChanged();
563             }
564         } else if (mShouldRunTicker && !isVisible) {
565             mShouldRunTicker = false;
566             getHandler().removeCallbacks(mTicker);
567         }
568     }
569 
570     @Override
onDetachedFromWindow()571     protected void onDetachedFromWindow() {
572         super.onDetachedFromWindow();
573 
574         if (mRegistered) {
575             unregisterReceiver();
576             unregisterObserver();
577 
578             mRegistered = false;
579         }
580     }
581 
582     /**
583      * Used by tests to stop the clock tick from updating the text.
584      * @hide
585      */
586     @TestApi
disableClockTick()587     public void disableClockTick() {
588         mStopTicking = true;
589     }
590 
registerReceiver()591     private void registerReceiver() {
592         final IntentFilter filter = new IntentFilter();
593 
594         filter.addAction(Intent.ACTION_TIME_TICK);
595         filter.addAction(Intent.ACTION_TIME_CHANGED);
596         filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
597 
598         // OK, this is gross but needed. This class is supported by the
599         // remote views mechanism and as a part of that the remote views
600         // can be inflated by a context for another user without the app
601         // having interact users permission - just for loading resources.
602         // For example, when adding widgets from a managed profile to the
603         // home screen. Therefore, we register the receiver as the user
604         // the app is running as not the one the context is for.
605         getContext().registerReceiverAsUser(mIntentReceiver, android.os.Process.myUserHandle(),
606                 filter, null, getHandler());
607     }
608 
registerObserver()609     private void registerObserver() {
610         if (mRegistered) {
611             if (mFormatChangeObserver == null) {
612                 mFormatChangeObserver = new FormatChangeObserver(getHandler());
613             }
614             final ContentResolver resolver = getContext().getContentResolver();
615             Uri uri = Settings.System.getUriFor(Settings.System.TIME_12_24);
616             if (mShowCurrentUserTime) {
617                 resolver.registerContentObserver(uri, true,
618                         mFormatChangeObserver, UserHandle.USER_ALL);
619             } else {
620                 // UserHandle.myUserId() is needed. This class is supported by the
621                 // remote views mechanism and as a part of that the remote views
622                 // can be inflated by a context for another user without the app
623                 // having interact users permission - just for loading resources.
624                 // For example, when adding widgets from a managed profile to the
625                 // home screen. Therefore, we register the ContentObserver with the user
626                 // the app is running (e.g. the launcher) and not the user of the
627                 // context (e.g. the widget's profile).
628                 resolver.registerContentObserver(uri, true,
629                         mFormatChangeObserver, UserHandle.myUserId());
630             }
631         }
632     }
633 
unregisterReceiver()634     private void unregisterReceiver() {
635         getContext().unregisterReceiver(mIntentReceiver);
636     }
637 
unregisterObserver()638     private void unregisterObserver() {
639         if (mFormatChangeObserver != null) {
640             final ContentResolver resolver = getContext().getContentResolver();
641             resolver.unregisterContentObserver(mFormatChangeObserver);
642         }
643     }
644 
645     /**
646      * Update the displayed time if this view and its ancestors and window is visible
647      */
648     @UnsupportedAppUsage
onTimeChanged()649     private void onTimeChanged() {
650         mTime.setTimeInMillis(System.currentTimeMillis());
651         setText(DateFormat.format(mFormat, mTime));
652         setContentDescription(DateFormat.format(mDescFormat, mTime));
653     }
654 
655     /** @hide */
656     @Override
encodeProperties(@onNull ViewHierarchyEncoder stream)657     protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
658         super.encodeProperties(stream);
659 
660         CharSequence s = getFormat12Hour();
661         stream.addProperty("format12Hour", s == null ? null : s.toString());
662 
663         s = getFormat24Hour();
664         stream.addProperty("format24Hour", s == null ? null : s.toString());
665         stream.addProperty("format", mFormat == null ? null : mFormat.toString());
666         stream.addProperty("hasSeconds", mHasSeconds);
667     }
668 }
669