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