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