Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2010 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.text.format.DateUtils.DAY_IN_MILLIS;
     20 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
     21 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
     22 import static android.text.format.DateUtils.YEAR_IN_MILLIS;
     23 import static android.text.format.Time.getJulianDay;
     24 
     25 import android.annotation.UnsupportedAppUsage;
     26 import android.app.ActivityThread;
     27 import android.content.BroadcastReceiver;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.IntentFilter;
     31 import android.content.res.Configuration;
     32 import android.content.res.TypedArray;
     33 import android.database.ContentObserver;
     34 import android.os.Handler;
     35 import android.text.format.Time;
     36 import android.util.AttributeSet;
     37 import android.view.accessibility.AccessibilityNodeInfo;
     38 import android.view.inspector.InspectableProperty;
     39 import android.widget.RemoteViews.RemoteView;
     40 
     41 import com.android.internal.R;
     42 
     43 import java.text.DateFormat;
     44 import java.util.ArrayList;
     45 import java.util.Calendar;
     46 import java.util.Date;
     47 import java.util.TimeZone;
     48 
     49 //
     50 // TODO
     51 // - listen for the next threshold time to update the view.
     52 // - listen for date format pref changed
     53 // - put the AM/PM in a smaller font
     54 //
     55 
     56 /**
     57  * Displays a given time in a convenient human-readable foramt.
     58  *
     59  * @hide
     60  */
     61 @RemoteView
     62 public class DateTimeView extends TextView {
     63     private static final int SHOW_TIME = 0;
     64     private static final int SHOW_MONTH_DAY_YEAR = 1;
     65 
     66     Date mTime;
     67     long mTimeMillis;
     68 
     69     int mLastDisplay = -1;
     70     DateFormat mLastFormat;
     71 
     72     private long mUpdateTimeMillis;
     73     private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>();
     74     private String mNowText;
     75     private boolean mShowRelativeTime;
     76 
     77     public DateTimeView(Context context) {
     78         this(context, null);
     79     }
     80 
     81     @UnsupportedAppUsage
     82     public DateTimeView(Context context, AttributeSet attrs) {
     83         super(context, attrs);
     84         final TypedArray a = context.obtainStyledAttributes(attrs,
     85                 com.android.internal.R.styleable.DateTimeView, 0,
     86                 0);
     87 
     88         final int N = a.getIndexCount();
     89         for (int i = 0; i < N; i++) {
     90             int attr = a.getIndex(i);
     91             switch (attr) {
     92                 case R.styleable.DateTimeView_showRelative:
     93                     boolean relative = a.getBoolean(i, false);
     94                     setShowRelativeTime(relative);
     95                     break;
     96             }
     97         }
     98         a.recycle();
     99     }
    100 
    101     @Override
    102     protected void onAttachedToWindow() {
    103         super.onAttachedToWindow();
    104         ReceiverInfo ri = sReceiverInfo.get();
    105         if (ri == null) {
    106             ri = new ReceiverInfo();
    107             sReceiverInfo.set(ri);
    108         }
    109         ri.addView(this);
    110         // The view may not be added to the view hierarchy immediately right after setTime()
    111         // is called which means it won't get any update from intents before being added.
    112         // In such case, the view might show the incorrect relative time after being added to the
    113         // view hierarchy until the next update intent comes.
    114         // So we update the time here if mShowRelativeTime is enabled to prevent this case.
    115         if (mShowRelativeTime) {
    116             update();
    117         }
    118     }
    119 
    120     @Override
    121     protected void onDetachedFromWindow() {
    122         super.onDetachedFromWindow();
    123         final ReceiverInfo ri = sReceiverInfo.get();
    124         if (ri != null) {
    125             ri.removeView(this);
    126         }
    127     }
    128 
    129     @android.view.RemotableViewMethod
    130     @UnsupportedAppUsage
    131     public void setTime(long time) {
    132         Time t = new Time();
    133         t.set(time);
    134         mTimeMillis = t.toMillis(false);
    135         mTime = new Date(t.year-1900, t.month, t.monthDay, t.hour, t.minute, 0);
    136         update();
    137     }
    138 
    139     @android.view.RemotableViewMethod
    140     public void setShowRelativeTime(boolean showRelativeTime) {
    141         mShowRelativeTime = showRelativeTime;
    142         updateNowText();
    143         update();
    144     }
    145 
    146     /**
    147      * Returns whether this view shows relative time
    148      *
    149      * @return True if it shows relative time, false otherwise
    150      */
    151     @InspectableProperty(name = "showReleative", hasAttributeId = false)
    152     public boolean isShowRelativeTime() {
    153         return mShowRelativeTime;
    154     }
    155 
    156     @Override
    157     @android.view.RemotableViewMethod
    158     public void setVisibility(@Visibility int visibility) {
    159         boolean gotVisible = visibility != GONE && getVisibility() == GONE;
    160         super.setVisibility(visibility);
    161         if (gotVisible) {
    162             update();
    163         }
    164     }
    165 
    166     @UnsupportedAppUsage
    167     void update() {
    168         if (mTime == null || getVisibility() == GONE) {
    169             return;
    170         }
    171         if (mShowRelativeTime) {
    172             updateRelativeTime();
    173             return;
    174         }
    175 
    176         int display;
    177         Date time = mTime;
    178 
    179         Time t = new Time();
    180         t.set(mTimeMillis);
    181         t.second = 0;
    182 
    183         t.hour -= 12;
    184         long twelveHoursBefore = t.toMillis(false);
    185         t.hour += 12;
    186         long twelveHoursAfter = t.toMillis(false);
    187         t.hour = 0;
    188         t.minute = 0;
    189         long midnightBefore = t.toMillis(false);
    190         t.monthDay++;
    191         long midnightAfter = t.toMillis(false);
    192 
    193         long nowMillis = System.currentTimeMillis();
    194         t.set(nowMillis);
    195         t.second = 0;
    196         nowMillis = t.normalize(false);
    197 
    198         // Choose the display mode
    199         choose_display: {
    200             if ((nowMillis >= midnightBefore && nowMillis < midnightAfter)
    201                     || (nowMillis >= twelveHoursBefore && nowMillis < twelveHoursAfter)) {
    202                 display = SHOW_TIME;
    203                 break choose_display;
    204             }
    205             // Else, show month day and year.
    206             display = SHOW_MONTH_DAY_YEAR;
    207             break choose_display;
    208         }
    209 
    210         // Choose the format
    211         DateFormat format;
    212         if (display == mLastDisplay && mLastFormat != null) {
    213             // use cached format
    214             format = mLastFormat;
    215         } else {
    216             switch (display) {
    217                 case SHOW_TIME:
    218                     format = getTimeFormat();
    219                     break;
    220                 case SHOW_MONTH_DAY_YEAR:
    221                     format = DateFormat.getDateInstance(DateFormat.SHORT);
    222                     break;
    223                 default:
    224                     throw new RuntimeException("unknown display value: " + display);
    225             }
    226             mLastFormat = format;
    227         }
    228 
    229         // Set the text
    230         String text = format.format(mTime);
    231         setText(text);
    232 
    233         // Schedule the next update
    234         if (display == SHOW_TIME) {
    235             // Currently showing the time, update at the later of twelve hours after or midnight.
    236             mUpdateTimeMillis = twelveHoursAfter > midnightAfter ? twelveHoursAfter : midnightAfter;
    237         } else {
    238             // Currently showing the date
    239             if (mTimeMillis < nowMillis) {
    240                 // If the time is in the past, don't schedule an update
    241                 mUpdateTimeMillis = 0;
    242             } else {
    243                 // If hte time is in the future, schedule one at the earlier of twelve hours
    244                 // before or midnight before.
    245                 mUpdateTimeMillis = twelveHoursBefore < midnightBefore
    246                         ? twelveHoursBefore : midnightBefore;
    247             }
    248         }
    249     }
    250 
    251     private void updateRelativeTime() {
    252         long now = System.currentTimeMillis();
    253         long duration = Math.abs(now - mTimeMillis);
    254         int count;
    255         long millisIncrease;
    256         boolean past = (now >= mTimeMillis);
    257         String result;
    258         if (duration < MINUTE_IN_MILLIS) {
    259             setText(mNowText);
    260             mUpdateTimeMillis = mTimeMillis + MINUTE_IN_MILLIS + 1;
    261             return;
    262         } else if (duration < HOUR_IN_MILLIS) {
    263             count = (int)(duration / MINUTE_IN_MILLIS);
    264             result = String.format(getContext().getResources().getQuantityString(past
    265                             ? com.android.internal.R.plurals.duration_minutes_shortest
    266                             : com.android.internal.R.plurals.duration_minutes_shortest_future,
    267                             count),
    268                     count);
    269             millisIncrease = MINUTE_IN_MILLIS;
    270         } else if (duration < DAY_IN_MILLIS) {
    271             count = (int)(duration / HOUR_IN_MILLIS);
    272             result = String.format(getContext().getResources().getQuantityString(past
    273                             ? com.android.internal.R.plurals.duration_hours_shortest
    274                             : com.android.internal.R.plurals.duration_hours_shortest_future,
    275                             count),
    276                     count);
    277             millisIncrease = HOUR_IN_MILLIS;
    278         } else if (duration < YEAR_IN_MILLIS) {
    279             // In weird cases it can become 0 because of daylight savings
    280             TimeZone timeZone = TimeZone.getDefault();
    281             count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
    282             result = String.format(getContext().getResources().getQuantityString(past
    283                             ? com.android.internal.R.plurals.duration_days_shortest
    284                             : com.android.internal.R.plurals.duration_days_shortest_future,
    285                             count),
    286                     count);
    287             if (past || count != 1) {
    288                 mUpdateTimeMillis = computeNextMidnight(timeZone);
    289                 millisIncrease = -1;
    290             } else {
    291                 millisIncrease = DAY_IN_MILLIS;
    292             }
    293 
    294         } else {
    295             count = (int)(duration / YEAR_IN_MILLIS);
    296             result = String.format(getContext().getResources().getQuantityString(past
    297                             ? com.android.internal.R.plurals.duration_years_shortest
    298                             : com.android.internal.R.plurals.duration_years_shortest_future,
    299                             count),
    300                     count);
    301             millisIncrease = YEAR_IN_MILLIS;
    302         }
    303         if (millisIncrease != -1) {
    304             if (past) {
    305                 mUpdateTimeMillis = mTimeMillis + millisIncrease * (count + 1) + 1;
    306             } else {
    307                 mUpdateTimeMillis = mTimeMillis - millisIncrease * count + 1;
    308             }
    309         }
    310         setText(result);
    311     }
    312 
    313     /**
    314      * @param timeZone the timezone we are in
    315      * @return the timepoint in millis at UTC at midnight in the current timezone
    316      */
    317     private long computeNextMidnight(TimeZone timeZone) {
    318         Calendar c = Calendar.getInstance();
    319         c.setTimeZone(timeZone);
    320         c.add(Calendar.DAY_OF_MONTH, 1);
    321         c.set(Calendar.HOUR_OF_DAY, 0);
    322         c.set(Calendar.MINUTE, 0);
    323         c.set(Calendar.SECOND, 0);
    324         c.set(Calendar.MILLISECOND, 0);
    325         return c.getTimeInMillis();
    326     }
    327 
    328     @Override
    329     protected void onConfigurationChanged(Configuration newConfig) {
    330         super.onConfigurationChanged(newConfig);
    331         updateNowText();
    332         update();
    333     }
    334 
    335     private void updateNowText() {
    336         if (!mShowRelativeTime) {
    337             return;
    338         }
    339         mNowText = getContext().getResources().getString(
    340                 com.android.internal.R.string.now_string_shortest);
    341     }
    342 
    343     // Return the date difference for the two times in a given timezone.
    344     private static int dayDistance(TimeZone timeZone, long startTime,
    345             long endTime) {
    346         return getJulianDay(endTime, timeZone.getOffset(endTime) / 1000)
    347                 - getJulianDay(startTime, timeZone.getOffset(startTime) / 1000);
    348     }
    349 
    350     private DateFormat getTimeFormat() {
    351         return android.text.format.DateFormat.getTimeFormat(getContext());
    352     }
    353 
    354     void clearFormatAndUpdate() {
    355         mLastFormat = null;
    356         update();
    357     }
    358 
    359     @Override
    360     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
    361         super.onInitializeAccessibilityNodeInfoInternal(info);
    362         if (mShowRelativeTime) {
    363             // The short version of the time might not be completely understandable and for
    364             // accessibility we rather have a longer version.
    365             long now = System.currentTimeMillis();
    366             long duration = Math.abs(now - mTimeMillis);
    367             int count;
    368             boolean past = (now >= mTimeMillis);
    369             String result;
    370             if (duration < MINUTE_IN_MILLIS) {
    371                 result = mNowText;
    372             } else if (duration < HOUR_IN_MILLIS) {
    373                 count = (int)(duration / MINUTE_IN_MILLIS);
    374                 result = String.format(getContext().getResources().getQuantityString(past
    375                                 ? com.android.internal.
    376                                         R.plurals.duration_minutes_relative
    377                                 : com.android.internal.
    378                                         R.plurals.duration_minutes_relative_future,
    379                         count),
    380                         count);
    381             } else if (duration < DAY_IN_MILLIS) {
    382                 count = (int)(duration / HOUR_IN_MILLIS);
    383                 result = String.format(getContext().getResources().getQuantityString(past
    384                                 ? com.android.internal.
    385                                         R.plurals.duration_hours_relative
    386                                 : com.android.internal.
    387                                         R.plurals.duration_hours_relative_future,
    388                         count),
    389                         count);
    390             } else if (duration < YEAR_IN_MILLIS) {
    391                 // In weird cases it can become 0 because of daylight savings
    392                 TimeZone timeZone = TimeZone.getDefault();
    393                 count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
    394                 result = String.format(getContext().getResources().getQuantityString(past
    395                                 ? com.android.internal.
    396                                         R.plurals.duration_days_relative
    397                                 : com.android.internal.
    398                                         R.plurals.duration_days_relative_future,
    399                         count),
    400                         count);
    401 
    402             } else {
    403                 count = (int)(duration / YEAR_IN_MILLIS);
    404                 result = String.format(getContext().getResources().getQuantityString(past
    405                                 ? com.android.internal.
    406                                         R.plurals.duration_years_relative
    407                                 : com.android.internal.
    408                                         R.plurals.duration_years_relative_future,
    409                         count),
    410                         count);
    411             }
    412             info.setText(result);
    413         }
    414     }
    415 
    416     /**
    417      * @hide
    418      */
    419     public static void setReceiverHandler(Handler handler) {
    420         ReceiverInfo ri = sReceiverInfo.get();
    421         if (ri == null) {
    422             ri = new ReceiverInfo();
    423             sReceiverInfo.set(ri);
    424         }
    425         ri.setHandler(handler);
    426     }
    427 
    428     private static class ReceiverInfo {
    429         private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>();
    430         private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    431             @Override
    432             public void onReceive(Context context, Intent intent) {
    433                 String action = intent.getAction();
    434                 if (Intent.ACTION_TIME_TICK.equals(action)) {
    435                     if (System.currentTimeMillis() < getSoonestUpdateTime()) {
    436                         // The update() function takes a few milliseconds to run because of
    437                         // all of the time conversions it needs to do, so we can't do that
    438                         // every minute.
    439                         return;
    440                     }
    441                 }
    442                 // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format.
    443                 updateAll();
    444             }
    445         };
    446 
    447         private final ContentObserver mObserver = new ContentObserver(new Handler()) {
    448             @Override
    449             public void onChange(boolean selfChange) {
    450                 updateAll();
    451             }
    452         };
    453 
    454         private Handler mHandler = new Handler();
    455 
    456         public void addView(DateTimeView v) {
    457             synchronized (mAttachedViews) {
    458                 final boolean register = mAttachedViews.isEmpty();
    459                 mAttachedViews.add(v);
    460                 if (register) {
    461                     register(getApplicationContextIfAvailable(v.getContext()));
    462                 }
    463             }
    464         }
    465 
    466         public void removeView(DateTimeView v) {
    467             synchronized (mAttachedViews) {
    468                 final boolean removed = mAttachedViews.remove(v);
    469                 // Only unregister once when we remove the last view in the list otherwise we risk
    470                 // trying to unregister a receiver that is no longer registered.
    471                 if (removed && mAttachedViews.isEmpty()) {
    472                     unregister(getApplicationContextIfAvailable(v.getContext()));
    473                 }
    474             }
    475         }
    476 
    477         void updateAll() {
    478             synchronized (mAttachedViews) {
    479                 final int count = mAttachedViews.size();
    480                 for (int i = 0; i < count; i++) {
    481                     DateTimeView view = mAttachedViews.get(i);
    482                     view.post(() -> view.clearFormatAndUpdate());
    483                 }
    484             }
    485         }
    486 
    487         long getSoonestUpdateTime() {
    488             long result = Long.MAX_VALUE;
    489             synchronized (mAttachedViews) {
    490                 final int count = mAttachedViews.size();
    491                 for (int i = 0; i < count; i++) {
    492                     final long time = mAttachedViews.get(i).mUpdateTimeMillis;
    493                     if (time < result) {
    494                         result = time;
    495                     }
    496                 }
    497             }
    498             return result;
    499         }
    500 
    501         static final Context getApplicationContextIfAvailable(Context context) {
    502             final Context ac = context.getApplicationContext();
    503             return ac != null ? ac : ActivityThread.currentApplication().getApplicationContext();
    504         }
    505 
    506         void register(Context context) {
    507             final IntentFilter filter = new IntentFilter();
    508             filter.addAction(Intent.ACTION_TIME_TICK);
    509             filter.addAction(Intent.ACTION_TIME_CHANGED);
    510             filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
    511             filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
    512             context.registerReceiver(mReceiver, filter, null, mHandler);
    513         }
    514 
    515         void unregister(Context context) {
    516             context.unregisterReceiver(mReceiver);
    517         }
    518 
    519         public void setHandler(Handler handler) {
    520             mHandler = handler;
    521             synchronized (mAttachedViews) {
    522                 if (!mAttachedViews.isEmpty()) {
    523                     unregister(mAttachedViews.get(0).getContext());
    524                     register(mAttachedViews.get(0).getContext());
    525                 }
    526             }
    527         }
    528     }
    529 }
    530