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     * @deprecated Let the system use locale-appropriate defaults instead.
99     */
100    public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
101
102    /**
103     * The default formatting pattern in 24-hour mode. This pattern is used
104     * if {@link #setFormat24Hour(CharSequence)} is called with a null pattern
105     * or if no pattern was specified when creating an instance of this class.
106     *
107     * This default pattern shows only the time, hours and minutes.
108     *
109     * @see #setFormat24Hour(CharSequence)
110     * @see #getFormat24Hour()
111     * @deprecated Let the system use locale-appropriate defaults instead.
112     */
113    public static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
114
115    private CharSequence mFormat12;
116    private CharSequence mFormat24;
117
118    @ExportedProperty
119    private CharSequence mFormat;
120    @ExportedProperty
121    private boolean mHasSeconds;
122
123    private boolean mAttached;
124
125    private Calendar mTime;
126    private String mTimeZone;
127
128    private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) {
129        @Override
130        public void onChange(boolean selfChange) {
131            chooseFormat();
132            onTimeChanged();
133        }
134
135        @Override
136        public void onChange(boolean selfChange, Uri uri) {
137            chooseFormat();
138            onTimeChanged();
139        }
140    };
141
142    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
143        @Override
144        public void onReceive(Context context, Intent intent) {
145            if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
146                final String timeZone = intent.getStringExtra("time-zone");
147                createTime(timeZone);
148            }
149            onTimeChanged();
150        }
151    };
152
153    private final Runnable mTicker = new Runnable() {
154        public void run() {
155            onTimeChanged();
156
157            long now = SystemClock.uptimeMillis();
158            long next = now + (1000 - now % 1000);
159
160            getHandler().postAtTime(mTicker, next);
161        }
162    };
163
164    /**
165     * Creates a new clock using the default patterns
166     * {@link #DEFAULT_FORMAT_24_HOUR} and {@link #DEFAULT_FORMAT_12_HOUR}
167     * respectively for the 24-hour and 12-hour modes.
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.timeFormat12;
226            }
227            if (mFormat24 == null) {
228                mFormat24 = ld.timeFormat24;
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     * 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}.
264     *
265     * 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, {@link #DEFAULT_FORMAT_24_HOUR} and
268     * {@link #DEFAULT_FORMAT_12_HOUR} will be used instead.
269     *
270     * @param format A date/time formatting pattern as described in {@link DateFormat}
271     *
272     * @see #getFormat12Hour()
273     * @see #is24HourModeEnabled()
274     * @see #DEFAULT_FORMAT_12_HOUR
275     * @see DateFormat
276     *
277     * @attr ref android.R.styleable#TextClock_format12Hour
278     */
279    @RemotableViewMethod
280    public void setFormat12Hour(CharSequence format) {
281        mFormat12 = format;
282
283        chooseFormat();
284        onTimeChanged();
285    }
286
287    /**
288     * Returns the formatting pattern used to display the date and/or time
289     * in 24-hour mode. The formatting pattern syntax is described in
290     * {@link DateFormat}.
291     *
292     * @return A {@link CharSequence} or null.
293     *
294     * @see #setFormat24Hour(CharSequence)
295     * @see #is24HourModeEnabled()
296     */
297    @ExportedProperty
298    public CharSequence getFormat24Hour() {
299        return mFormat24;
300    }
301
302    /**
303     * Specifies the formatting pattern used to display the date and/or time
304     * in 24-hour mode. The formatting pattern syntax is described in
305     * {@link DateFormat}.
306     *
307     * If this pattern is set to null, {@link #getFormat12Hour()} will be used
308     * even in 24-hour mode. If both 24-hour and 12-hour formatting patterns
309     * are set to null, {@link #DEFAULT_FORMAT_24_HOUR} and
310     * {@link #DEFAULT_FORMAT_12_HOUR} will be used instead.
311     *
312     * @param format A date/time formatting pattern as described in {@link DateFormat}
313     *
314     * @see #getFormat24Hour()
315     * @see #is24HourModeEnabled()
316     * @see #DEFAULT_FORMAT_24_HOUR
317     * @see DateFormat
318     *
319     * @attr ref android.R.styleable#TextClock_format24Hour
320     */
321    @RemotableViewMethod
322    public void setFormat24Hour(CharSequence format) {
323        mFormat24 = format;
324
325        chooseFormat();
326        onTimeChanged();
327    }
328
329    /**
330     * Indicates whether the system is currently using the 24-hour mode.
331     *
332     * When the system is in 24-hour mode, this view will use the pattern
333     * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern
334     * returned by {@link #getFormat12Hour()} is used instead.
335     *
336     * If either one of the formats is null, the other format is used. If
337     * both formats are null, the default values {@link #DEFAULT_FORMAT_12_HOUR}
338     * and {@link #DEFAULT_FORMAT_24_HOUR} are used instead.
339     *
340     * @return true if time should be displayed in 24-hour format, false if it
341     *         should be displayed in 12-hour format.
342     *
343     * @see #setFormat12Hour(CharSequence)
344     * @see #getFormat12Hour()
345     * @see #setFormat24Hour(CharSequence)
346     * @see #getFormat24Hour()
347     */
348    public boolean is24HourModeEnabled() {
349        return DateFormat.is24HourFormat(getContext());
350    }
351
352    /**
353     * Indicates which time zone is currently used by this view.
354     *
355     * @return The ID of the current time zone or null if the default time zone,
356     *         as set by the user, must be used
357     *
358     * @see TimeZone
359     * @see java.util.TimeZone#getAvailableIDs()
360     * @see #setTimeZone(String)
361     */
362    public String getTimeZone() {
363        return mTimeZone;
364    }
365
366    /**
367     * Sets the specified time zone to use in this clock. When the time zone
368     * is set through this method, system time zone changes (when the user
369     * sets the time zone in settings for instance) will be ignored.
370     *
371     * @param timeZone The desired time zone's ID as specified in {@link TimeZone}
372     *                 or null to user the time zone specified by the user
373     *                 (system time zone)
374     *
375     * @see #getTimeZone()
376     * @see java.util.TimeZone#getAvailableIDs()
377     * @see TimeZone#getTimeZone(String)
378     *
379     * @attr ref android.R.styleable#TextClock_timeZone
380     */
381    @RemotableViewMethod
382    public void setTimeZone(String timeZone) {
383        mTimeZone = timeZone;
384
385        createTime(timeZone);
386        onTimeChanged();
387    }
388
389    /**
390     * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
391     * depending on whether the user has selected 24-hour format.
392     *
393     * Calling this method does not schedule or unschedule the time ticker.
394     */
395    private void chooseFormat() {
396        chooseFormat(true);
397    }
398
399    /**
400     * Returns the current format string. Always valid after constructor has
401     * finished, and will never be {@code null}.
402     *
403     * @hide
404     */
405    public CharSequence getFormat() {
406        return mFormat;
407    }
408
409    /**
410     * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
411     * depending on whether the user has selected 24-hour format.
412     *
413     * @param handleTicker true if calling this method should schedule/unschedule the
414     *                     time ticker, false otherwise
415     */
416    private void chooseFormat(boolean handleTicker) {
417        final boolean format24Requested = is24HourModeEnabled();
418
419        LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
420
421        if (format24Requested) {
422            mFormat = abc(mFormat24, mFormat12, ld.timeFormat24);
423        } else {
424            mFormat = abc(mFormat12, mFormat24, ld.timeFormat12);
425        }
426
427        boolean hadSeconds = mHasSeconds;
428        mHasSeconds = DateFormat.hasSeconds(mFormat);
429
430        if (handleTicker && mAttached && hadSeconds != mHasSeconds) {
431            if (hadSeconds) getHandler().removeCallbacks(mTicker);
432            else mTicker.run();
433        }
434    }
435
436    /**
437     * Returns a if not null, else return b if not null, else return c.
438     */
439    private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) {
440        return a == null ? (b == null ? c : b) : a;
441    }
442
443    @Override
444    protected void onAttachedToWindow() {
445        super.onAttachedToWindow();
446
447        if (!mAttached) {
448            mAttached = true;
449
450            registerReceiver();
451            registerObserver();
452
453            createTime(mTimeZone);
454
455            if (mHasSeconds) {
456                mTicker.run();
457            } else {
458                onTimeChanged();
459            }
460        }
461    }
462
463    @Override
464    protected void onDetachedFromWindow() {
465        super.onDetachedFromWindow();
466
467        if (mAttached) {
468            unregisterReceiver();
469            unregisterObserver();
470
471            getHandler().removeCallbacks(mTicker);
472
473            mAttached = false;
474        }
475    }
476
477    private void registerReceiver() {
478        final IntentFilter filter = new IntentFilter();
479
480        filter.addAction(Intent.ACTION_TIME_TICK);
481        filter.addAction(Intent.ACTION_TIME_CHANGED);
482        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
483
484        getContext().registerReceiver(mIntentReceiver, filter, null, getHandler());
485    }
486
487    private void registerObserver() {
488        final ContentResolver resolver = getContext().getContentResolver();
489        resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver);
490    }
491
492    private void unregisterReceiver() {
493        getContext().unregisterReceiver(mIntentReceiver);
494    }
495
496    private void unregisterObserver() {
497        final ContentResolver resolver = getContext().getContentResolver();
498        resolver.unregisterContentObserver(mFormatChangeObserver);
499    }
500
501    private void onTimeChanged() {
502        mTime.setTimeInMillis(System.currentTimeMillis());
503        setText(DateFormat.format(mFormat, mTime));
504    }
505}
506