TextClock.java revision f7d5e0a53e168f2acc17b098bdd4b927fa1b1d6c
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.content.BroadcastReceiver;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.content.res.TypedArray;
25import android.database.ContentObserver;
26import android.net.Uri;
27import android.os.Handler;
28import android.os.SystemClock;
29import android.provider.Settings;
30import android.text.format.DateFormat;
31import android.util.AttributeSet;
32import android.view.RemotableViewMethod;
33
34import com.android.internal.R;
35
36import java.util.Calendar;
37import java.util.TimeZone;
38
39import libcore.icu.LocaleData;
40
41import static android.view.ViewDebug.ExportedProperty;
42import static android.widget.RemoteViews.*;
43
44/**
45 * <p><code>TextClock</code> can display the current date and/or time as
46 * a formatted string.</p>
47 *
48 * <p>This view honors the 24-hour format system setting. As such, it is
49 * possible and recommended to provide two different formatting patterns:
50 * one to display the date/time in 24-hour mode and one to display the
51 * date/time in 12-hour mode. Most callers will want to use the defaults,
52 * though, which will be appropriate for the user's locale.</p>
53 *
54 * <p>It is possible to determine whether the system is currently in
55 * 24-hour mode by calling {@link #is24HourModeEnabled()}.</p>
56 *
57 * <p>The rules used by this widget to decide how to format the date and
58 * time are the following:</p>
59 * <ul>
60 *     <li>In 24-hour mode:
61 *         <ul>
62 *             <li>Use the value returned by {@link #getFormat24Hour()} when non-null</li>
63 *             <li>Otherwise, use the value returned by {@link #getFormat12Hour()} when non-null</li>
64 *             <li>Otherwise, use a default value appropriate for the user's locale, such as {@code h:mm a}</li>
65 *         </ul>
66 *     </li>
67 *     <li>In 12-hour mode:
68 *         <ul>
69 *             <li>Use the value returned by {@link #getFormat12Hour()} when non-null</li>
70 *             <li>Otherwise, use the value returned by {@link #getFormat24Hour()} when non-null</li>
71 *             <li>Otherwise, use a default value appropriate for the user's locale, such as {@code HH:mm}</li>
72 *         </ul>
73 *     </li>
74 * </ul>
75 *
76 * <p>The {@link CharSequence} instances used as formatting patterns when calling either
77 * {@link #setFormat24Hour(CharSequence)} or {@link #setFormat12Hour(CharSequence)} can
78 * contain styling information. To do so, use a {@link android.text.Spanned} object.
79 * Note that if you customize these strings, it is your responsibility to supply strings
80 * appropriate for formatting dates and/or times in the user's locale.</p>
81 *
82 * @attr ref android.R.styleable#TextClock_format12Hour
83 * @attr ref android.R.styleable#TextClock_format24Hour
84 * @attr ref android.R.styleable#TextClock_timeZone
85 */
86@RemoteView
87public class TextClock extends TextView {
88    /**
89     * The default formatting pattern in 12-hour mode. This pattern is used
90     * if {@link #setFormat12Hour(CharSequence)} is called with a null pattern
91     * or if no pattern was specified when creating an instance of this class.
92     *
93     * This default pattern shows only the time, hours and minutes, and an am/pm
94     * indicator.
95     *
96     * @see #setFormat12Hour(CharSequence)
97     * @see #getFormat12Hour()
98     *
99     * @deprecated Let the system use locale-appropriate defaults instead.
100     */
101    public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
102
103    /**
104     * The default formatting pattern in 24-hour mode. This pattern is used
105     * if {@link #setFormat24Hour(CharSequence)} is called with a null pattern
106     * or if no pattern was specified when creating an instance of this class.
107     *
108     * This default pattern shows only the time, hours and minutes.
109     *
110     * @see #setFormat24Hour(CharSequence)
111     * @see #getFormat24Hour()
112     *
113     * @deprecated Let the system use locale-appropriate defaults instead.
114     */
115    public static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
116
117    private CharSequence mFormat12;
118    private CharSequence mFormat24;
119
120    @ExportedProperty
121    private CharSequence mFormat;
122    @ExportedProperty
123    private boolean mHasSeconds;
124
125    private boolean mAttached;
126
127    private Calendar mTime;
128    private String mTimeZone;
129
130    private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) {
131        @Override
132        public void onChange(boolean selfChange) {
133            chooseFormat();
134            onTimeChanged();
135        }
136
137        @Override
138        public void onChange(boolean selfChange, Uri uri) {
139            chooseFormat();
140            onTimeChanged();
141        }
142    };
143
144    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
145        @Override
146        public void onReceive(Context context, Intent intent) {
147            if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
148                final String timeZone = intent.getStringExtra("time-zone");
149                createTime(timeZone);
150            }
151            onTimeChanged();
152        }
153    };
154
155    private final Runnable mTicker = new Runnable() {
156        public void run() {
157            onTimeChanged();
158
159            long now = SystemClock.uptimeMillis();
160            long next = now + (1000 - now % 1000);
161
162            getHandler().postAtTime(mTicker, next);
163        }
164    };
165
166    /**
167     * Creates a new clock using the default patterns for the current locale.
168     *
169     * @param context The Context the view is running in, through which it can
170     *        access the current theme, resources, etc.
171     */
172    @SuppressWarnings("UnusedDeclaration")
173    public TextClock(Context context) {
174        super(context);
175        init();
176    }
177
178    /**
179     * Creates a new clock inflated from XML. This object's properties are
180     * intialized from the attributes specified in XML.
181     *
182     * This constructor uses a default style of 0, so the only attribute values
183     * applied are those in the Context's Theme and the given AttributeSet.
184     *
185     * @param context The Context the view is running in, through which it can
186     *        access the current theme, resources, etc.
187     * @param attrs The attributes of the XML tag that is inflating the view
188     */
189    @SuppressWarnings("UnusedDeclaration")
190    public TextClock(Context context, AttributeSet attrs) {
191        this(context, attrs, 0);
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     * @param context The Context the view is running in, through which it can
199     *        access the current theme, resources, etc.
200     * @param attrs The attributes of the XML tag that is inflating the view
201     * @param defStyle The default style to apply to this view. If 0, no style
202     *        will be applied (beyond what is included in the theme). This may
203     *        either be an attribute resource, whose value will be retrieved
204     *        from the current theme, or an explicit style resource
205     */
206    public TextClock(Context context, AttributeSet attrs, int defStyle) {
207        super(context, attrs, defStyle);
208
209        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextClock, defStyle, 0);
210        try {
211            mFormat12 = a.getText(R.styleable.TextClock_format12Hour);
212            mFormat24 = a.getText(R.styleable.TextClock_format24Hour);
213            mTimeZone = a.getString(R.styleable.TextClock_timeZone);
214        } finally {
215            a.recycle();
216        }
217
218        init();
219    }
220
221    private void init() {
222        if (mFormat12 == null || mFormat24 == null) {
223            LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
224            if (mFormat12 == null) {
225                mFormat12 = ld.timeFormat_hm;
226            }
227            if (mFormat24 == null) {
228                mFormat24 = ld.timeFormat_Hm;
229            }
230        }
231
232        createTime(mTimeZone);
233        // Wait until onAttachedToWindow() to handle the ticker
234        chooseFormat(false);
235    }
236
237    private void createTime(String timeZone) {
238        if (timeZone != null) {
239            mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
240        } else {
241            mTime = Calendar.getInstance();
242        }
243    }
244
245    /**
246     * Returns the formatting pattern used to display the date and/or time
247     * in 12-hour mode. The formatting pattern syntax is described in
248     * {@link DateFormat}.
249     *
250     * @return A {@link CharSequence} or null.
251     *
252     * @see #setFormat12Hour(CharSequence)
253     * @see #is24HourModeEnabled()
254     */
255    @ExportedProperty
256    public CharSequence getFormat12Hour() {
257        return mFormat12;
258    }
259
260    /**
261     * <p>Specifies the formatting pattern used to display the date and/or time
262     * in 12-hour mode. The formatting pattern syntax is described in
263     * {@link DateFormat}.</p>
264     *
265     * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
266     * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
267     * are set to null, the default pattern for the current locale will be used
268     * instead.</p>
269     *
270     * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
271     * you supply a format string generated by
272     * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
273     * takes care of generating a format string adapted to the desired locale.</p>
274     *
275     *
276     * @param format A date/time formatting pattern as described in {@link DateFormat}
277     *
278     * @see #getFormat12Hour()
279     * @see #is24HourModeEnabled()
280     * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
281     * @see DateFormat
282     *
283     * @attr ref android.R.styleable#TextClock_format12Hour
284     */
285    @RemotableViewMethod
286    public void setFormat12Hour(CharSequence format) {
287        mFormat12 = format;
288
289        chooseFormat();
290        onTimeChanged();
291    }
292
293    /**
294     * Returns the formatting pattern used to display the date and/or time
295     * in 24-hour mode. The formatting pattern syntax is described in
296     * {@link DateFormat}.
297     *
298     * @return A {@link CharSequence} or null.
299     *
300     * @see #setFormat24Hour(CharSequence)
301     * @see #is24HourModeEnabled()
302     */
303    @ExportedProperty
304    public CharSequence getFormat24Hour() {
305        return mFormat24;
306    }
307
308    /**
309     * <p>Specifies the formatting pattern used to display the date and/or time
310     * in 24-hour mode. The formatting pattern syntax is described in
311     * {@link DateFormat}.</p>
312     *
313     * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
314     * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
315     * are set to null, the default pattern for the current locale will be used
316     * instead.</p>
317     *
318     * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
319     * you supply a format string generated by
320     * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
321     * takes care of generating a format string adapted to the desired locale.</p>
322     *
323     * @param format A date/time formatting pattern as described in {@link DateFormat}
324     *
325     * @see #getFormat24Hour()
326     * @see #is24HourModeEnabled()
327     * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
328     * @see DateFormat
329     *
330     * @attr ref android.R.styleable#TextClock_format24Hour
331     */
332    @RemotableViewMethod
333    public void setFormat24Hour(CharSequence format) {
334        mFormat24 = format;
335
336        chooseFormat();
337        onTimeChanged();
338    }
339
340    /**
341     * Indicates whether the system is currently using the 24-hour mode.
342     *
343     * When the system is in 24-hour mode, this view will use the pattern
344     * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern
345     * returned by {@link #getFormat12Hour()} is used instead.
346     *
347     * If either one of the formats is null, the other format is used. If
348     * both formats are null, the default formats for the current locale are used.
349     *
350     * @return true if time should be displayed in 24-hour format, false if it
351     *         should be displayed in 12-hour format.
352     *
353     * @see #setFormat12Hour(CharSequence)
354     * @see #getFormat12Hour()
355     * @see #setFormat24Hour(CharSequence)
356     * @see #getFormat24Hour()
357     */
358    public boolean is24HourModeEnabled() {
359        return DateFormat.is24HourFormat(getContext());
360    }
361
362    /**
363     * Indicates which time zone is currently used by this view.
364     *
365     * @return The ID of the current time zone or null if the default time zone,
366     *         as set by the user, must be used
367     *
368     * @see TimeZone
369     * @see java.util.TimeZone#getAvailableIDs()
370     * @see #setTimeZone(String)
371     */
372    public String getTimeZone() {
373        return mTimeZone;
374    }
375
376    /**
377     * Sets the specified time zone to use in this clock. When the time zone
378     * is set through this method, system time zone changes (when the user
379     * sets the time zone in settings for instance) will be ignored.
380     *
381     * @param timeZone The desired time zone's ID as specified in {@link TimeZone}
382     *                 or null to user the time zone specified by the user
383     *                 (system time zone)
384     *
385     * @see #getTimeZone()
386     * @see java.util.TimeZone#getAvailableIDs()
387     * @see TimeZone#getTimeZone(String)
388     *
389     * @attr ref android.R.styleable#TextClock_timeZone
390     */
391    @RemotableViewMethod
392    public void setTimeZone(String timeZone) {
393        mTimeZone = timeZone;
394
395        createTime(timeZone);
396        onTimeChanged();
397    }
398
399    /**
400     * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
401     * depending on whether the user has selected 24-hour format.
402     *
403     * Calling this method does not schedule or unschedule the time ticker.
404     */
405    private void chooseFormat() {
406        chooseFormat(true);
407    }
408
409    /**
410     * Returns the current format string. Always valid after constructor has
411     * finished, and will never be {@code null}.
412     *
413     * @hide
414     */
415    public CharSequence getFormat() {
416        return mFormat;
417    }
418
419    /**
420     * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
421     * depending on whether the user has selected 24-hour format.
422     *
423     * @param handleTicker true if calling this method should schedule/unschedule the
424     *                     time ticker, false otherwise
425     */
426    private void chooseFormat(boolean handleTicker) {
427        final boolean format24Requested = is24HourModeEnabled();
428
429        LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
430
431        if (format24Requested) {
432            mFormat = abc(mFormat24, mFormat12, ld.timeFormat_Hm);
433        } else {
434            mFormat = abc(mFormat12, mFormat24, ld.timeFormat_hm);
435        }
436
437        boolean hadSeconds = mHasSeconds;
438        mHasSeconds = DateFormat.hasSeconds(mFormat);
439
440        if (handleTicker && mAttached && hadSeconds != mHasSeconds) {
441            if (hadSeconds) getHandler().removeCallbacks(mTicker);
442            else mTicker.run();
443        }
444    }
445
446    /**
447     * Returns a if not null, else return b if not null, else return c.
448     */
449    private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) {
450        return a == null ? (b == null ? c : b) : a;
451    }
452
453    @Override
454    protected void onAttachedToWindow() {
455        super.onAttachedToWindow();
456
457        if (!mAttached) {
458            mAttached = true;
459
460            registerReceiver();
461            registerObserver();
462
463            createTime(mTimeZone);
464
465            if (mHasSeconds) {
466                mTicker.run();
467            } else {
468                onTimeChanged();
469            }
470        }
471    }
472
473    @Override
474    protected void onDetachedFromWindow() {
475        super.onDetachedFromWindow();
476
477        if (mAttached) {
478            unregisterReceiver();
479            unregisterObserver();
480
481            getHandler().removeCallbacks(mTicker);
482
483            mAttached = false;
484        }
485    }
486
487    private void registerReceiver() {
488        final IntentFilter filter = new IntentFilter();
489
490        filter.addAction(Intent.ACTION_TIME_TICK);
491        filter.addAction(Intent.ACTION_TIME_CHANGED);
492        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
493
494        getContext().registerReceiver(mIntentReceiver, filter, null, getHandler());
495    }
496
497    private void registerObserver() {
498        final ContentResolver resolver = getContext().getContentResolver();
499        resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver);
500    }
501
502    private void unregisterReceiver() {
503        getContext().unregisterReceiver(mIntentReceiver);
504    }
505
506    private void unregisterObserver() {
507        final ContentResolver resolver = getContext().getContentResolver();
508        resolver.unregisterContentObserver(mFormatChangeObserver);
509    }
510
511    private void onTimeChanged() {
512        mTime.setTimeInMillis(System.currentTimeMillis());
513        setText(DateFormat.format(mFormat, mTime));
514    }
515}
516