Home | History | Annotate | Download | only in widget
      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 
     17 package android.widget;
     18 
     19 import static android.view.ViewDebug.ExportedProperty;
     20 import static android.widget.RemoteViews.RemoteView;
     21 
     22 import android.annotation.NonNull;
     23 import android.annotation.TestApi;
     24 import android.app.ActivityManager;
     25 import android.content.BroadcastReceiver;
     26 import android.content.ContentResolver;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.IntentFilter;
     30 import android.content.res.TypedArray;
     31 import android.database.ContentObserver;
     32 import android.net.Uri;
     33 import android.os.Handler;
     34 import android.os.SystemClock;
     35 import android.os.UserHandle;
     36 import android.provider.Settings;
     37 import android.text.format.DateFormat;
     38 import android.util.AttributeSet;
     39 import android.view.RemotableViewMethod;
     40 import android.view.ViewHierarchyEncoder;
     41 
     42 import com.android.internal.R;
     43 
     44 import libcore.icu.LocaleData;
     45 
     46 import java.util.Calendar;
     47 import 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
     92 public 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