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