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 defStyleAttr An attribute in the current theme that contains a
202     *        reference to a style resource that supplies default values for
203     *        the view. Can be 0 to not look for defaults.
204     */
205    public TextClock(Context context, AttributeSet attrs, int defStyleAttr) {
206        this(context, attrs, defStyleAttr, 0);
207    }
208
209    public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
210        super(context, attrs, defStyleAttr, defStyleRes);
211
212        final TypedArray a = context.obtainStyledAttributes(
213                attrs, R.styleable.TextClock, defStyleAttr, defStyleRes);
214        try {
215            mFormat12 = a.getText(R.styleable.TextClock_format12Hour);
216            mFormat24 = a.getText(R.styleable.TextClock_format24Hour);
217            mTimeZone = a.getString(R.styleable.TextClock_timeZone);
218        } finally {
219            a.recycle();
220        }
221
222        init();
223    }
224
225    private void init() {
226        if (mFormat12 == null || mFormat24 == null) {
227            LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
228            if (mFormat12 == null) {
229                mFormat12 = ld.timeFormat12;
230            }
231            if (mFormat24 == null) {
232                mFormat24 = ld.timeFormat24;
233            }
234        }
235
236        createTime(mTimeZone);
237        // Wait until onAttachedToWindow() to handle the ticker
238        chooseFormat(false);
239    }
240
241    private void createTime(String timeZone) {
242        if (timeZone != null) {
243            mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
244        } else {
245            mTime = Calendar.getInstance();
246        }
247    }
248
249    /**
250     * Returns the formatting pattern used to display the date and/or time
251     * in 12-hour mode. The formatting pattern syntax is described in
252     * {@link DateFormat}.
253     *
254     * @return A {@link CharSequence} or null.
255     *
256     * @see #setFormat12Hour(CharSequence)
257     * @see #is24HourModeEnabled()
258     */
259    @ExportedProperty
260    public CharSequence getFormat12Hour() {
261        return mFormat12;
262    }
263
264    /**
265     * <p>Specifies the formatting pattern used to display the date and/or time
266     * in 12-hour mode. The formatting pattern syntax is described in
267     * {@link DateFormat}.</p>
268     *
269     * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
270     * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
271     * are set to null, the default pattern for the current locale will be used
272     * instead.</p>
273     *
274     * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
275     * you supply a format string generated by
276     * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
277     * takes care of generating a format string adapted to the desired locale.</p>
278     *
279     *
280     * @param format A date/time formatting pattern as described in {@link DateFormat}
281     *
282     * @see #getFormat12Hour()
283     * @see #is24HourModeEnabled()
284     * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
285     * @see DateFormat
286     *
287     * @attr ref android.R.styleable#TextClock_format12Hour
288     */
289    @RemotableViewMethod
290    public void setFormat12Hour(CharSequence format) {
291        mFormat12 = format;
292
293        chooseFormat();
294        onTimeChanged();
295    }
296
297    /**
298     * Returns the formatting pattern used to display the date and/or time
299     * in 24-hour mode. The formatting pattern syntax is described in
300     * {@link DateFormat}.
301     *
302     * @return A {@link CharSequence} or null.
303     *
304     * @see #setFormat24Hour(CharSequence)
305     * @see #is24HourModeEnabled()
306     */
307    @ExportedProperty
308    public CharSequence getFormat24Hour() {
309        return mFormat24;
310    }
311
312    /**
313     * <p>Specifies the formatting pattern used to display the date and/or time
314     * in 24-hour mode. The formatting pattern syntax is described in
315     * {@link DateFormat}.</p>
316     *
317     * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
318     * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
319     * are set to null, the default pattern for the current locale will be used
320     * instead.</p>
321     *
322     * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
323     * you supply a format string generated by
324     * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
325     * takes care of generating a format string adapted to the desired locale.</p>
326     *
327     * @param format A date/time formatting pattern as described in {@link DateFormat}
328     *
329     * @see #getFormat24Hour()
330     * @see #is24HourModeEnabled()
331     * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
332     * @see DateFormat
333     *
334     * @attr ref android.R.styleable#TextClock_format24Hour
335     */
336    @RemotableViewMethod
337    public void setFormat24Hour(CharSequence format) {
338        mFormat24 = format;
339
340        chooseFormat();
341        onTimeChanged();
342    }
343
344    /**
345     * Indicates whether the system is currently using the 24-hour mode.
346     *
347     * When the system is in 24-hour mode, this view will use the pattern
348     * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern
349     * returned by {@link #getFormat12Hour()} is used instead.
350     *
351     * If either one of the formats is null, the other format is used. If
352     * both formats are null, the default formats for the current locale are used.
353     *
354     * @return true if time should be displayed in 24-hour format, false if it
355     *         should be displayed in 12-hour format.
356     *
357     * @see #setFormat12Hour(CharSequence)
358     * @see #getFormat12Hour()
359     * @see #setFormat24Hour(CharSequence)
360     * @see #getFormat24Hour()
361     */
362    public boolean is24HourModeEnabled() {
363        return DateFormat.is24HourFormat(getContext());
364    }
365
366    /**
367     * Indicates which time zone is currently used by this view.
368     *
369     * @return The ID of the current time zone or null if the default time zone,
370     *         as set by the user, must be used
371     *
372     * @see TimeZone
373     * @see java.util.TimeZone#getAvailableIDs()
374     * @see #setTimeZone(String)
375     */
376    public String getTimeZone() {
377        return mTimeZone;
378    }
379
380    /**
381     * Sets the specified time zone to use in this clock. When the time zone
382     * is set through this method, system time zone changes (when the user
383     * sets the time zone in settings for instance) will be ignored.
384     *
385     * @param timeZone The desired time zone's ID as specified in {@link TimeZone}
386     *                 or null to user the time zone specified by the user
387     *                 (system time zone)
388     *
389     * @see #getTimeZone()
390     * @see java.util.TimeZone#getAvailableIDs()
391     * @see TimeZone#getTimeZone(String)
392     *
393     * @attr ref android.R.styleable#TextClock_timeZone
394     */
395    @RemotableViewMethod
396    public void setTimeZone(String timeZone) {
397        mTimeZone = timeZone;
398
399        createTime(timeZone);
400        onTimeChanged();
401    }
402
403    /**
404     * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
405     * depending on whether the user has selected 24-hour format.
406     *
407     * Calling this method does not schedule or unschedule the time ticker.
408     */
409    private void chooseFormat() {
410        chooseFormat(true);
411    }
412
413    /**
414     * Returns the current format string. Always valid after constructor has
415     * finished, and will never be {@code null}.
416     *
417     * @hide
418     */
419    public CharSequence getFormat() {
420        return mFormat;
421    }
422
423    /**
424     * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
425     * depending on whether the user has selected 24-hour format.
426     *
427     * @param handleTicker true if calling this method should schedule/unschedule the
428     *                     time ticker, false otherwise
429     */
430    private void chooseFormat(boolean handleTicker) {
431        final boolean format24Requested = is24HourModeEnabled();
432
433        LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
434
435        if (format24Requested) {
436            mFormat = abc(mFormat24, mFormat12, ld.timeFormat24);
437        } else {
438            mFormat = abc(mFormat12, mFormat24, ld.timeFormat12);
439        }
440
441        boolean hadSeconds = mHasSeconds;
442        mHasSeconds = DateFormat.hasSeconds(mFormat);
443
444        if (handleTicker && mAttached && hadSeconds != mHasSeconds) {
445            if (hadSeconds) getHandler().removeCallbacks(mTicker);
446            else mTicker.run();
447        }
448    }
449
450    /**
451     * Returns a if not null, else return b if not null, else return c.
452     */
453    private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) {
454        return a == null ? (b == null ? c : b) : a;
455    }
456
457    @Override
458    protected void onAttachedToWindow() {
459        super.onAttachedToWindow();
460
461        if (!mAttached) {
462            mAttached = true;
463
464            registerReceiver();
465            registerObserver();
466
467            createTime(mTimeZone);
468
469            if (mHasSeconds) {
470                mTicker.run();
471            } else {
472                onTimeChanged();
473            }
474        }
475    }
476
477    @Override
478    protected void onDetachedFromWindow() {
479        super.onDetachedFromWindow();
480
481        if (mAttached) {
482            unregisterReceiver();
483            unregisterObserver();
484
485            getHandler().removeCallbacks(mTicker);
486
487            mAttached = false;
488        }
489    }
490
491    private void registerReceiver() {
492        final IntentFilter filter = new IntentFilter();
493
494        filter.addAction(Intent.ACTION_TIME_TICK);
495        filter.addAction(Intent.ACTION_TIME_CHANGED);
496        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
497
498        getContext().registerReceiver(mIntentReceiver, filter, null, getHandler());
499    }
500
501    private void registerObserver() {
502        final ContentResolver resolver = getContext().getContentResolver();
503        resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver);
504    }
505
506    private void unregisterReceiver() {
507        getContext().unregisterReceiver(mIntentReceiver);
508    }
509
510    private void unregisterObserver() {
511        final ContentResolver resolver = getContext().getContentResolver();
512        resolver.unregisterContentObserver(mFormatChangeObserver);
513    }
514
515    private void onTimeChanged() {
516        mTime.setTimeInMillis(System.currentTimeMillis());
517        setText(DateFormat.format(mFormat, mTime));
518    }
519}
520