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