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.annotation.UnsupportedAppUsage;
25 import android.app.ActivityManager;
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("time-zone");
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             getHandler().postAtTime(mTicker, next);
197         }
198     };
199 
200     /**
201      * Creates a new clock using the default patterns for the current locale.
202      *
203      * @param context The Context the view is running in, through which it can
204      *        access the current theme, resources, etc.
205      */
206     @SuppressWarnings("UnusedDeclaration")
TextClock(Context context)207     public TextClock(Context context) {
208         super(context);
209         init();
210     }
211 
212     /**
213      * Creates a new clock inflated from XML. This object's properties are
214      * intialized from the attributes specified in XML.
215      *
216      * This constructor uses a default style of 0, so the only attribute values
217      * applied are those in the Context's Theme and the given AttributeSet.
218      *
219      * @param context The Context the view is running in, through which it can
220      *        access the current theme, resources, etc.
221      * @param attrs The attributes of the XML tag that is inflating the view
222      */
223     @SuppressWarnings("UnusedDeclaration")
TextClock(Context context, AttributeSet attrs)224     public TextClock(Context context, AttributeSet attrs) {
225         this(context, attrs, 0);
226     }
227 
228     /**
229      * Creates a new clock inflated from XML. This object's properties are
230      * intialized from the attributes specified in XML.
231      *
232      * @param context The Context the view is running in, through which it can
233      *        access the current theme, resources, etc.
234      * @param attrs The attributes of the XML tag that is inflating the view
235      * @param defStyleAttr An attribute in the current theme that contains a
236      *        reference to a style resource that supplies default values for
237      *        the view. Can be 0 to not look for defaults.
238      */
TextClock(Context context, AttributeSet attrs, int defStyleAttr)239     public TextClock(Context context, AttributeSet attrs, int defStyleAttr) {
240         this(context, attrs, defStyleAttr, 0);
241     }
242 
TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)243     public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
244         super(context, attrs, defStyleAttr, defStyleRes);
245 
246         final TypedArray a = context.obtainStyledAttributes(
247                 attrs, R.styleable.TextClock, defStyleAttr, defStyleRes);
248         saveAttributeDataForStyleable(context, R.styleable.TextClock,
249                 attrs, a, defStyleAttr, defStyleRes);
250         try {
251             mFormat12 = a.getText(R.styleable.TextClock_format12Hour);
252             mFormat24 = a.getText(R.styleable.TextClock_format24Hour);
253             mTimeZone = a.getString(R.styleable.TextClock_timeZone);
254         } finally {
255             a.recycle();
256         }
257 
258         init();
259     }
260 
init()261     private void init() {
262         if (mFormat12 == null || mFormat24 == null) {
263             LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
264             if (mFormat12 == null) {
265                 mFormat12 = ld.timeFormat_hm;
266             }
267             if (mFormat24 == null) {
268                 mFormat24 = ld.timeFormat_Hm;
269             }
270         }
271 
272         createTime(mTimeZone);
273         chooseFormat();
274     }
275 
createTime(String timeZone)276     private void createTime(String timeZone) {
277         if (timeZone != null) {
278             mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
279         } else {
280             mTime = Calendar.getInstance();
281         }
282     }
283 
284     /**
285      * Returns the formatting pattern used to display the date and/or time
286      * in 12-hour mode. The formatting pattern syntax is described in
287      * {@link DateFormat}.
288      *
289      * @return A {@link CharSequence} or null.
290      *
291      * @see #setFormat12Hour(CharSequence)
292      * @see #is24HourModeEnabled()
293      */
294     @InspectableProperty
295     @ExportedProperty
getFormat12Hour()296     public CharSequence getFormat12Hour() {
297         return mFormat12;
298     }
299 
300     /**
301      * <p>Specifies the formatting pattern used to display the date and/or time
302      * in 12-hour mode. The formatting pattern syntax is described in
303      * {@link DateFormat}.</p>
304      *
305      * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
306      * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
307      * are set to null, the default pattern for the current locale will be used
308      * instead.</p>
309      *
310      * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
311      * you supply a format string generated by
312      * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
313      * takes care of generating a format string adapted to the desired locale.</p>
314      *
315      *
316      * @param format A date/time formatting pattern as described in {@link DateFormat}
317      *
318      * @see #getFormat12Hour()
319      * @see #is24HourModeEnabled()
320      * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
321      * @see DateFormat
322      *
323      * @attr ref android.R.styleable#TextClock_format12Hour
324      */
325     @RemotableViewMethod
setFormat12Hour(CharSequence format)326     public void setFormat12Hour(CharSequence format) {
327         mFormat12 = format;
328 
329         chooseFormat();
330         onTimeChanged();
331     }
332 
333     /**
334      * Like setFormat12Hour, but for the content description.
335      * @hide
336      */
setContentDescriptionFormat12Hour(CharSequence format)337     public void setContentDescriptionFormat12Hour(CharSequence format) {
338         mDescFormat12 = format;
339 
340         chooseFormat();
341         onTimeChanged();
342     }
343 
344     /**
345      * Returns the formatting pattern used to display the date and/or time
346      * in 24-hour mode. The formatting pattern syntax is described in
347      * {@link DateFormat}.
348      *
349      * @return A {@link CharSequence} or null.
350      *
351      * @see #setFormat24Hour(CharSequence)
352      * @see #is24HourModeEnabled()
353      */
354     @InspectableProperty
355     @ExportedProperty
getFormat24Hour()356     public CharSequence getFormat24Hour() {
357         return mFormat24;
358     }
359 
360     /**
361      * <p>Specifies the formatting pattern used to display the date and/or time
362      * in 24-hour mode. The formatting pattern syntax is described in
363      * {@link DateFormat}.</p>
364      *
365      * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
366      * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
367      * are set to null, the default pattern for the current locale will be used
368      * instead.</p>
369      *
370      * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
371      * you supply a format string generated by
372      * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
373      * takes care of generating a format string adapted to the desired locale.</p>
374      *
375      * @param format A date/time formatting pattern as described in {@link DateFormat}
376      *
377      * @see #getFormat24Hour()
378      * @see #is24HourModeEnabled()
379      * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
380      * @see DateFormat
381      *
382      * @attr ref android.R.styleable#TextClock_format24Hour
383      */
384     @RemotableViewMethod
setFormat24Hour(CharSequence format)385     public void setFormat24Hour(CharSequence format) {
386         mFormat24 = format;
387 
388         chooseFormat();
389         onTimeChanged();
390     }
391 
392     /**
393      * Like setFormat24Hour, but for the content description.
394      * @hide
395      */
setContentDescriptionFormat24Hour(CharSequence format)396     public void setContentDescriptionFormat24Hour(CharSequence format) {
397         mDescFormat24 = format;
398 
399         chooseFormat();
400         onTimeChanged();
401     }
402 
403     /**
404      * Sets whether this clock should always track the current user and not the user of the
405      * current process. This is used for single instance processes like the systemUI who need
406      * to display time for different users.
407      *
408      * @hide
409      */
setShowCurrentUserTime(boolean showCurrentUserTime)410     public void setShowCurrentUserTime(boolean showCurrentUserTime) {
411         mShowCurrentUserTime = showCurrentUserTime;
412 
413         chooseFormat();
414         onTimeChanged();
415         unregisterObserver();
416         registerObserver();
417     }
418 
419     /**
420      * Update the displayed time if necessary and invalidate the view.
421      * @hide
422      */
refresh()423     public void refresh() {
424         onTimeChanged();
425         invalidate();
426     }
427 
428     /**
429      * Indicates whether the system is currently using the 24-hour mode.
430      *
431      * When the system is in 24-hour mode, this view will use the pattern
432      * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern
433      * returned by {@link #getFormat12Hour()} is used instead.
434      *
435      * If either one of the formats is null, the other format is used. If
436      * both formats are null, the default formats for the current locale are used.
437      *
438      * @return true if time should be displayed in 24-hour format, false if it
439      *         should be displayed in 12-hour format.
440      *
441      * @see #setFormat12Hour(CharSequence)
442      * @see #getFormat12Hour()
443      * @see #setFormat24Hour(CharSequence)
444      * @see #getFormat24Hour()
445      */
446     @InspectableProperty(hasAttributeId = false)
is24HourModeEnabled()447     public boolean is24HourModeEnabled() {
448         if (mShowCurrentUserTime) {
449             return DateFormat.is24HourFormat(getContext(), ActivityManager.getCurrentUser());
450         } else {
451             return DateFormat.is24HourFormat(getContext());
452         }
453     }
454 
455     /**
456      * Indicates which time zone is currently used by this view.
457      *
458      * @return The ID of the current time zone or null if the default time zone,
459      *         as set by the user, must be used
460      *
461      * @see TimeZone
462      * @see java.util.TimeZone#getAvailableIDs()
463      * @see #setTimeZone(String)
464      */
465     @InspectableProperty
getTimeZone()466     public String getTimeZone() {
467         return mTimeZone;
468     }
469 
470     /**
471      * Sets the specified time zone to use in this clock. When the time zone
472      * is set through this method, system time zone changes (when the user
473      * sets the time zone in settings for instance) will be ignored.
474      *
475      * @param timeZone The desired time zone's ID as specified in {@link TimeZone}
476      *                 or null to user the time zone specified by the user
477      *                 (system time zone)
478      *
479      * @see #getTimeZone()
480      * @see java.util.TimeZone#getAvailableIDs()
481      * @see TimeZone#getTimeZone(String)
482      *
483      * @attr ref android.R.styleable#TextClock_timeZone
484      */
485     @RemotableViewMethod
setTimeZone(String timeZone)486     public void setTimeZone(String timeZone) {
487         mTimeZone = timeZone;
488 
489         createTime(timeZone);
490         onTimeChanged();
491     }
492 
493     /**
494      * Returns the current format string. Always valid after constructor has
495      * finished, and will never be {@code null}.
496      *
497      * @hide
498      */
499     @UnsupportedAppUsage
getFormat()500     public CharSequence getFormat() {
501         return mFormat;
502     }
503 
504     /**
505      * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
506      * depending on whether the user has selected 24-hour format.
507      */
chooseFormat()508     private void chooseFormat() {
509         final boolean format24Requested = is24HourModeEnabled();
510 
511         LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
512 
513         if (format24Requested) {
514             mFormat = abc(mFormat24, mFormat12, ld.timeFormat_Hm);
515             mDescFormat = abc(mDescFormat24, mDescFormat12, mFormat);
516         } else {
517             mFormat = abc(mFormat12, mFormat24, ld.timeFormat_hm);
518             mDescFormat = abc(mDescFormat12, mDescFormat24, mFormat);
519         }
520 
521         boolean hadSeconds = mHasSeconds;
522         mHasSeconds = DateFormat.hasSeconds(mFormat);
523 
524         if (mShouldRunTicker && hadSeconds != mHasSeconds) {
525             if (hadSeconds) getHandler().removeCallbacks(mTicker);
526             else mTicker.run();
527         }
528     }
529 
530     /**
531      * Returns a if not null, else return b if not null, else return c.
532      */
abc(CharSequence a, CharSequence b, CharSequence c)533     private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) {
534         return a == null ? (b == null ? c : b) : a;
535     }
536 
537     @Override
onAttachedToWindow()538     protected void onAttachedToWindow() {
539         super.onAttachedToWindow();
540 
541         if (!mRegistered) {
542             mRegistered = true;
543 
544             registerReceiver();
545             registerObserver();
546 
547             createTime(mTimeZone);
548         }
549     }
550 
551     @Override
onVisibilityAggregated(boolean isVisible)552     public void onVisibilityAggregated(boolean isVisible) {
553         super.onVisibilityAggregated(isVisible);
554 
555         if (!mShouldRunTicker && isVisible) {
556             mShouldRunTicker = true;
557             if (mHasSeconds) {
558                 mTicker.run();
559             } else {
560                 onTimeChanged();
561             }
562         } else if (mShouldRunTicker && !isVisible) {
563             mShouldRunTicker = false;
564             getHandler().removeCallbacks(mTicker);
565         }
566     }
567 
568     @Override
onDetachedFromWindow()569     protected void onDetachedFromWindow() {
570         super.onDetachedFromWindow();
571 
572         if (mRegistered) {
573             unregisterReceiver();
574             unregisterObserver();
575 
576             mRegistered = false;
577         }
578     }
579 
580     /**
581      * Used by tests to stop the clock tick from updating the text.
582      * @hide
583      */
584     @TestApi
disableClockTick()585     public void disableClockTick() {
586         mStopTicking = true;
587     }
588 
registerReceiver()589     private void registerReceiver() {
590         final IntentFilter filter = new IntentFilter();
591 
592         filter.addAction(Intent.ACTION_TIME_TICK);
593         filter.addAction(Intent.ACTION_TIME_CHANGED);
594         filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
595 
596         // OK, this is gross but needed. This class is supported by the
597         // remote views mechanism and as a part of that the remote views
598         // can be inflated by a context for another user without the app
599         // having interact users permission - just for loading resources.
600         // For example, when adding widgets from a managed profile to the
601         // home screen. Therefore, we register the receiver as the user
602         // the app is running as not the one the context is for.
603         getContext().registerReceiverAsUser(mIntentReceiver, android.os.Process.myUserHandle(),
604                 filter, null, getHandler());
605     }
606 
registerObserver()607     private void registerObserver() {
608         if (mRegistered) {
609             if (mFormatChangeObserver == null) {
610                 mFormatChangeObserver = new FormatChangeObserver(getHandler());
611             }
612             final ContentResolver resolver = getContext().getContentResolver();
613             Uri uri = Settings.System.getUriFor(Settings.System.TIME_12_24);
614             if (mShowCurrentUserTime) {
615                 resolver.registerContentObserver(uri, true,
616                         mFormatChangeObserver, UserHandle.USER_ALL);
617             } else {
618                 // UserHandle.myUserId() is needed. This class is supported by the
619                 // remote views mechanism and as a part of that the remote views
620                 // can be inflated by a context for another user without the app
621                 // having interact users permission - just for loading resources.
622                 // For example, when adding widgets from a managed profile to the
623                 // home screen. Therefore, we register the ContentObserver with the user
624                 // the app is running (e.g. the launcher) and not the user of the
625                 // context (e.g. the widget's profile).
626                 resolver.registerContentObserver(uri, true,
627                         mFormatChangeObserver, UserHandle.myUserId());
628             }
629         }
630     }
631 
unregisterReceiver()632     private void unregisterReceiver() {
633         getContext().unregisterReceiver(mIntentReceiver);
634     }
635 
unregisterObserver()636     private void unregisterObserver() {
637         if (mFormatChangeObserver != null) {
638             final ContentResolver resolver = getContext().getContentResolver();
639             resolver.unregisterContentObserver(mFormatChangeObserver);
640         }
641     }
642 
643     /**
644      * Update the displayed time if this view and its ancestors and window is visible
645      */
646     @UnsupportedAppUsage
onTimeChanged()647     private void onTimeChanged() {
648         mTime.setTimeInMillis(System.currentTimeMillis());
649         setText(DateFormat.format(mFormat, mTime));
650         setContentDescription(DateFormat.format(mDescFormat, mTime));
651     }
652 
653     /** @hide */
654     @Override
encodeProperties(@onNull ViewHierarchyEncoder stream)655     protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
656         super.encodeProperties(stream);
657 
658         CharSequence s = getFormat12Hour();
659         stream.addProperty("format12Hour", s == null ? null : s.toString());
660 
661         s = getFormat24Hour();
662         stream.addProperty("format24Hour", s == null ? null : s.toString());
663         stream.addProperty("format", mFormat == null ? null : mFormat.toString());
664         stream.addProperty("hasSeconds", mHasSeconds);
665     }
666 }
667