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