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