• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
DateTimeView(Context context)77     public DateTimeView(Context context) {
78         this(context, null);
79     }
80 
81     @UnsupportedAppUsage
DateTimeView(Context context, AttributeSet attrs)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
onAttachedToWindow()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
onDetachedFromWindow()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
setTime(long time)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
setShowRelativeTime(boolean showRelativeTime)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)
isShowRelativeTime()152     public boolean isShowRelativeTime() {
153         return mShowRelativeTime;
154     }
155 
156     @Override
157     @android.view.RemotableViewMethod
setVisibility(@isibility int visibility)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
update()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      */
computeNextMidnight(TimeZone timeZone)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
onConfigurationChanged(Configuration newConfig)329     protected void onConfigurationChanged(Configuration newConfig) {
330         super.onConfigurationChanged(newConfig);
331         updateNowText();
332         update();
333     }
334 
updateNowText()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.
dayDistance(TimeZone timeZone, long startTime, long endTime)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 
getTimeFormat()350     private DateFormat getTimeFormat() {
351         return android.text.format.DateFormat.getTimeFormat(getContext());
352     }
353 
clearFormatAndUpdate()354     void clearFormatAndUpdate() {
355         mLastFormat = null;
356         update();
357     }
358 
359     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)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      */
setReceiverHandler(Handler handler)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 
addView(DateTimeView v)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 
removeView(DateTimeView v)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 
updateAll()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 
getSoonestUpdateTime()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 
getApplicationContextIfAvailable(Context context)501         static final Context getApplicationContextIfAvailable(Context context) {
502             final Context ac = context.getApplicationContext();
503             return ac != null ? ac : ActivityThread.currentApplication().getApplicationContext();
504         }
505 
register(Context context)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 
unregister(Context context)515         void unregister(Context context) {
516             context.unregisterReceiver(mReceiver);
517         }
518 
setHandler(Handler handler)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