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