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