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