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