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