blob: bf2762a6d72785ba19e204aee09775047f46cb0a [file] [log] [blame]
Joe Onoratoc83bb732010-01-19 16:32:22 -08001/*
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
17package android.widget;
18
Selim Cinek36d57082016-04-19 18:31:47 -070019import static android.text.format.DateUtils.DAY_IN_MILLIS;
20import static android.text.format.DateUtils.HOUR_IN_MILLIS;
21import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
22import static android.text.format.DateUtils.YEAR_IN_MILLIS;
23import static android.text.format.Time.getJulianDay;
24
Mathew Inwooda85f4eb2018-08-21 16:08:34 +010025import android.annotation.UnsupportedAppUsage;
Dan Sandler4b677132015-07-30 22:33:12 -040026import android.app.ActivityThread;
Selim Cinek36d57082016-04-19 18:31:47 -070027import android.content.BroadcastReceiver;
Joe Onoratoc83bb732010-01-19 16:32:22 -080028import android.content.Context;
29import android.content.Intent;
30import android.content.IntentFilter;
Selim Cinek36d57082016-04-19 18:31:47 -070031import android.content.res.Configuration;
32import android.content.res.TypedArray;
Joe Onoratoc83bb732010-01-19 16:32:22 -080033import android.database.ContentObserver;
Joe Onoratoc83bb732010-01-19 16:32:22 -080034import android.os.Handler;
35import android.text.format.Time;
36import android.util.AttributeSet;
Selim Cinek570bfa22016-05-25 18:44:44 -070037import android.view.accessibility.AccessibilityNodeInfo;
Joe Onoratoc83bb732010-01-19 16:32:22 -080038import android.widget.RemoteViews.RemoteView;
39
Selim Cinek36d57082016-04-19 18:31:47 -070040import com.android.internal.R;
41
Joe Onoratoc83bb732010-01-19 16:32:22 -080042import java.text.DateFormat;
Adam Powell740da472014-11-07 14:51:16 -080043import java.util.ArrayList;
Neil Fuller6d5c4a82018-07-13 19:49:51 +010044import java.util.Calendar;
Joe Onoratoc83bb732010-01-19 16:32:22 -080045import java.util.Date;
Selim Cinek36d57082016-04-19 18:31:47 -070046import java.util.TimeZone;
Joe Onoratoc83bb732010-01-19 16:32:22 -080047
48//
49// TODO
50// - listen for the next threshold time to update the view.
51// - listen for date format pref changed
52// - put the AM/PM in a smaller font
53//
54
55/**
56 * Displays a given time in a convenient human-readable foramt.
57 *
58 * @hide
59 */
60@RemoteView
61public class DateTimeView extends TextView {
Joe Onoratoc83bb732010-01-19 16:32:22 -080062 private static final int SHOW_TIME = 0;
63 private static final int SHOW_MONTH_DAY_YEAR = 1;
64
65 Date mTime;
66 long mTimeMillis;
67
68 int mLastDisplay = -1;
69 DateFormat mLastFormat;
70
Joe Onoratoc83bb732010-01-19 16:32:22 -080071 private long mUpdateTimeMillis;
Adam Powell740da472014-11-07 14:51:16 -080072 private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>();
Selim Cinek36d57082016-04-19 18:31:47 -070073 private String mNowText;
74 private boolean mShowRelativeTime;
Joe Onoratoc83bb732010-01-19 16:32:22 -080075
76 public DateTimeView(Context context) {
Selim Cinek36d57082016-04-19 18:31:47 -070077 this(context, null);
Joe Onoratoc83bb732010-01-19 16:32:22 -080078 }
79
Mathew Inwooda85f4eb2018-08-21 16:08:34 +010080 @UnsupportedAppUsage
Joe Onoratoc83bb732010-01-19 16:32:22 -080081 public DateTimeView(Context context, AttributeSet attrs) {
82 super(context, attrs);
Selim Cinek36d57082016-04-19 18:31:47 -070083 final TypedArray a = context.obtainStyledAttributes(attrs,
84 com.android.internal.R.styleable.DateTimeView, 0,
85 0);
86
87 final int N = a.getIndexCount();
88 for (int i = 0; i < N; i++) {
89 int attr = a.getIndex(i);
90 switch (attr) {
91 case R.styleable.DateTimeView_showRelative:
92 boolean relative = a.getBoolean(i, false);
93 setShowRelativeTime(relative);
94 break;
95 }
96 }
97 a.recycle();
Joe Onoratoc83bb732010-01-19 16:32:22 -080098 }
99
100 @Override
101 protected void onAttachedToWindow() {
Daniel Sandler8d3c2342011-08-09 13:10:20 -0400102 super.onAttachedToWindow();
Adam Powell740da472014-11-07 14:51:16 -0800103 ReceiverInfo ri = sReceiverInfo.get();
104 if (ri == null) {
105 ri = new ReceiverInfo();
106 sReceiverInfo.set(ri);
107 }
108 ri.addView(this);
shawnlinea19d322018-05-02 17:02:54 +0800109 // The view may not be added to the view hierarchy immediately right after setTime()
110 // is called which means it won't get any update from intents before being added.
111 // In such case, the view might show the incorrect relative time after being added to the
112 // view hierarchy until the next update intent comes.
113 // So we update the time here if mShowRelativeTime is enabled to prevent this case.
114 if (mShowRelativeTime) {
115 update();
116 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800117 }
shawnlinea19d322018-05-02 17:02:54 +0800118
Joe Onoratoc83bb732010-01-19 16:32:22 -0800119 @Override
120 protected void onDetachedFromWindow() {
121 super.onDetachedFromWindow();
Adam Powell740da472014-11-07 14:51:16 -0800122 final ReceiverInfo ri = sReceiverInfo.get();
123 if (ri != null) {
124 ri.removeView(this);
125 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800126 }
127
128 @android.view.RemotableViewMethod
Mathew Inwooda85f4eb2018-08-21 16:08:34 +0100129 @UnsupportedAppUsage
Joe Onoratoc83bb732010-01-19 16:32:22 -0800130 public void setTime(long time) {
131 Time t = new Time();
132 t.set(time);
Joe Onoratoc83bb732010-01-19 16:32:22 -0800133 mTimeMillis = t.toMillis(false);
134 mTime = new Date(t.year-1900, t.month, t.monthDay, t.hour, t.minute, 0);
135 update();
136 }
137
Selim Cinek36d57082016-04-19 18:31:47 -0700138 @android.view.RemotableViewMethod
139 public void setShowRelativeTime(boolean showRelativeTime) {
140 mShowRelativeTime = showRelativeTime;
141 updateNowText();
142 update();
143 }
144
Selim Cinekb85f36fd2016-04-20 18:46:36 -0700145 @Override
146 @android.view.RemotableViewMethod
147 public void setVisibility(@Visibility int visibility) {
148 boolean gotVisible = visibility != GONE && getVisibility() == GONE;
149 super.setVisibility(visibility);
150 if (gotVisible) {
151 update();
152 }
153 }
154
Mathew Inwooda85f4eb2018-08-21 16:08:34 +0100155 @UnsupportedAppUsage
Joe Onoratoc83bb732010-01-19 16:32:22 -0800156 void update() {
Selim Cinekb85f36fd2016-04-20 18:46:36 -0700157 if (mTime == null || getVisibility() == GONE) {
Joe Onoratoc83bb732010-01-19 16:32:22 -0800158 return;
159 }
Selim Cinek36d57082016-04-19 18:31:47 -0700160 if (mShowRelativeTime) {
161 updateRelativeTime();
162 return;
163 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800164
165 int display;
166 Date time = mTime;
167
168 Time t = new Time();
169 t.set(mTimeMillis);
170 t.second = 0;
171
172 t.hour -= 12;
173 long twelveHoursBefore = t.toMillis(false);
174 t.hour += 12;
175 long twelveHoursAfter = t.toMillis(false);
176 t.hour = 0;
177 t.minute = 0;
178 long midnightBefore = t.toMillis(false);
179 t.monthDay++;
180 long midnightAfter = t.toMillis(false);
181
182 long nowMillis = System.currentTimeMillis();
183 t.set(nowMillis);
184 t.second = 0;
185 nowMillis = t.normalize(false);
186
187 // Choose the display mode
188 choose_display: {
189 if ((nowMillis >= midnightBefore && nowMillis < midnightAfter)
190 || (nowMillis >= twelveHoursBefore && nowMillis < twelveHoursAfter)) {
191 display = SHOW_TIME;
192 break choose_display;
193 }
194 // Else, show month day and year.
195 display = SHOW_MONTH_DAY_YEAR;
196 break choose_display;
197 }
198
199 // Choose the format
200 DateFormat format;
201 if (display == mLastDisplay && mLastFormat != null) {
202 // use cached format
203 format = mLastFormat;
204 } else {
205 switch (display) {
206 case SHOW_TIME:
207 format = getTimeFormat();
208 break;
209 case SHOW_MONTH_DAY_YEAR:
Narayan Kamath775eca12014-11-18 14:52:04 +0000210 format = DateFormat.getDateInstance(DateFormat.SHORT);
Joe Onoratoc83bb732010-01-19 16:32:22 -0800211 break;
212 default:
213 throw new RuntimeException("unknown display value: " + display);
214 }
215 mLastFormat = format;
216 }
217
218 // Set the text
219 String text = format.format(mTime);
220 setText(text);
221
222 // Schedule the next update
223 if (display == SHOW_TIME) {
224 // Currently showing the time, update at the later of twelve hours after or midnight.
225 mUpdateTimeMillis = twelveHoursAfter > midnightAfter ? twelveHoursAfter : midnightAfter;
226 } else {
227 // Currently showing the date
228 if (mTimeMillis < nowMillis) {
229 // If the time is in the past, don't schedule an update
230 mUpdateTimeMillis = 0;
231 } else {
232 // If hte time is in the future, schedule one at the earlier of twelve hours
233 // before or midnight before.
234 mUpdateTimeMillis = twelveHoursBefore < midnightBefore
235 ? twelveHoursBefore : midnightBefore;
236 }
237 }
Selim Cinek36d57082016-04-19 18:31:47 -0700238 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800239
Selim Cinek36d57082016-04-19 18:31:47 -0700240 private void updateRelativeTime() {
241 long now = System.currentTimeMillis();
242 long duration = Math.abs(now - mTimeMillis);
243 int count;
244 long millisIncrease;
245 boolean past = (now >= mTimeMillis);
246 String result;
247 if (duration < MINUTE_IN_MILLIS) {
248 setText(mNowText);
249 mUpdateTimeMillis = mTimeMillis + MINUTE_IN_MILLIS + 1;
250 return;
251 } else if (duration < HOUR_IN_MILLIS) {
252 count = (int)(duration / MINUTE_IN_MILLIS);
253 result = String.format(getContext().getResources().getQuantityString(past
254 ? com.android.internal.R.plurals.duration_minutes_shortest
255 : com.android.internal.R.plurals.duration_minutes_shortest_future,
256 count),
257 count);
258 millisIncrease = MINUTE_IN_MILLIS;
259 } else if (duration < DAY_IN_MILLIS) {
260 count = (int)(duration / HOUR_IN_MILLIS);
261 result = String.format(getContext().getResources().getQuantityString(past
262 ? com.android.internal.R.plurals.duration_hours_shortest
263 : com.android.internal.R.plurals.duration_hours_shortest_future,
264 count),
265 count);
266 millisIncrease = HOUR_IN_MILLIS;
267 } else if (duration < YEAR_IN_MILLIS) {
268 // In weird cases it can become 0 because of daylight savings
269 TimeZone timeZone = TimeZone.getDefault();
270 count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
271 result = String.format(getContext().getResources().getQuantityString(past
272 ? com.android.internal.R.plurals.duration_days_shortest
273 : com.android.internal.R.plurals.duration_days_shortest_future,
274 count),
275 count);
276 if (past || count != 1) {
277 mUpdateTimeMillis = computeNextMidnight(timeZone);
278 millisIncrease = -1;
279 } else {
280 millisIncrease = DAY_IN_MILLIS;
281 }
282
283 } else {
284 count = (int)(duration / YEAR_IN_MILLIS);
285 result = String.format(getContext().getResources().getQuantityString(past
286 ? com.android.internal.R.plurals.duration_years_shortest
287 : com.android.internal.R.plurals.duration_years_shortest_future,
288 count),
289 count);
290 millisIncrease = YEAR_IN_MILLIS;
291 }
292 if (millisIncrease != -1) {
293 if (past) {
294 mUpdateTimeMillis = mTimeMillis + millisIncrease * (count + 1) + 1;
295 } else {
296 mUpdateTimeMillis = mTimeMillis - millisIncrease * count + 1;
297 }
298 }
299 setText(result);
300 }
301
302 /**
303 * @param timeZone the timezone we are in
304 * @return the timepoint in millis at UTC at midnight in the current timezone
305 */
306 private long computeNextMidnight(TimeZone timeZone) {
307 Calendar c = Calendar.getInstance();
Neil Fuller6d5c4a82018-07-13 19:49:51 +0100308 c.setTimeZone(timeZone);
Selim Cinek36d57082016-04-19 18:31:47 -0700309 c.add(Calendar.DAY_OF_MONTH, 1);
310 c.set(Calendar.HOUR_OF_DAY, 0);
311 c.set(Calendar.MINUTE, 0);
312 c.set(Calendar.SECOND, 0);
313 c.set(Calendar.MILLISECOND, 0);
314 return c.getTimeInMillis();
315 }
316
317 @Override
318 protected void onConfigurationChanged(Configuration newConfig) {
319 super.onConfigurationChanged(newConfig);
320 updateNowText();
321 update();
322 }
323
324 private void updateNowText() {
325 if (!mShowRelativeTime) {
326 return;
327 }
328 mNowText = getContext().getResources().getString(
329 com.android.internal.R.string.now_string_shortest);
330 }
331
332 // Return the date difference for the two times in a given timezone.
333 private static int dayDistance(TimeZone timeZone, long startTime,
334 long endTime) {
335 return getJulianDay(endTime, timeZone.getOffset(endTime) / 1000)
336 - getJulianDay(startTime, timeZone.getOffset(startTime) / 1000);
Joe Onoratoc83bb732010-01-19 16:32:22 -0800337 }
338
339 private DateFormat getTimeFormat() {
Elliott Hughes4caba612013-01-14 15:48:27 -0800340 return android.text.format.DateFormat.getTimeFormat(getContext());
Joe Onoratoc83bb732010-01-19 16:32:22 -0800341 }
342
Adam Powell740da472014-11-07 14:51:16 -0800343 void clearFormatAndUpdate() {
344 mLastFormat = null;
345 update();
Joe Onoratoc83bb732010-01-19 16:32:22 -0800346 }
347
Selim Cinek570bfa22016-05-25 18:44:44 -0700348 @Override
349 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
350 super.onInitializeAccessibilityNodeInfoInternal(info);
351 if (mShowRelativeTime) {
352 // The short version of the time might not be completely understandable and for
353 // accessibility we rather have a longer version.
354 long now = System.currentTimeMillis();
355 long duration = Math.abs(now - mTimeMillis);
356 int count;
357 boolean past = (now >= mTimeMillis);
358 String result;
359 if (duration < MINUTE_IN_MILLIS) {
360 result = mNowText;
361 } else if (duration < HOUR_IN_MILLIS) {
362 count = (int)(duration / MINUTE_IN_MILLIS);
363 result = String.format(getContext().getResources().getQuantityString(past
364 ? com.android.internal.
365 R.plurals.duration_minutes_relative
366 : com.android.internal.
367 R.plurals.duration_minutes_relative_future,
368 count),
369 count);
370 } else if (duration < DAY_IN_MILLIS) {
371 count = (int)(duration / HOUR_IN_MILLIS);
372 result = String.format(getContext().getResources().getQuantityString(past
373 ? com.android.internal.
374 R.plurals.duration_hours_relative
375 : com.android.internal.
376 R.plurals.duration_hours_relative_future,
377 count),
378 count);
379 } else if (duration < YEAR_IN_MILLIS) {
380 // In weird cases it can become 0 because of daylight savings
381 TimeZone timeZone = TimeZone.getDefault();
382 count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
383 result = String.format(getContext().getResources().getQuantityString(past
384 ? com.android.internal.
385 R.plurals.duration_days_relative
386 : com.android.internal.
387 R.plurals.duration_days_relative_future,
388 count),
389 count);
390
391 } else {
392 count = (int)(duration / YEAR_IN_MILLIS);
393 result = String.format(getContext().getResources().getQuantityString(past
394 ? com.android.internal.
395 R.plurals.duration_years_relative
396 : com.android.internal.
397 R.plurals.duration_years_relative_future,
398 count),
399 count);
400 }
401 info.setText(result);
402 }
403 }
404
Jason Monkcd26af72017-01-11 14:32:58 -0500405 /**
406 * @hide
407 */
408 public static void setReceiverHandler(Handler handler) {
409 ReceiverInfo ri = sReceiverInfo.get();
410 if (ri == null) {
411 ri = new ReceiverInfo();
412 sReceiverInfo.set(ri);
413 }
414 ri.setHandler(handler);
415 }
416
Adam Powell740da472014-11-07 14:51:16 -0800417 private static class ReceiverInfo {
418 private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>();
419 private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
420 @Override
421 public void onReceive(Context context, Intent intent) {
422 String action = intent.getAction();
423 if (Intent.ACTION_TIME_TICK.equals(action)) {
424 if (System.currentTimeMillis() < getSoonestUpdateTime()) {
425 // The update() function takes a few milliseconds to run because of
426 // all of the time conversions it needs to do, so we can't do that
427 // every minute.
428 return;
429 }
430 }
431 // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format.
432 updateAll();
433 }
434 };
Joe Onoratoc83bb732010-01-19 16:32:22 -0800435
Adam Powell740da472014-11-07 14:51:16 -0800436 private final ContentObserver mObserver = new ContentObserver(new Handler()) {
437 @Override
438 public void onChange(boolean selfChange) {
439 updateAll();
440 }
441 };
442
Jason Monkcd26af72017-01-11 14:32:58 -0500443 private Handler mHandler = new Handler();
444
Adam Powell740da472014-11-07 14:51:16 -0800445 public void addView(DateTimeView v) {
Jason Monkcd26af72017-01-11 14:32:58 -0500446 synchronized (mAttachedViews) {
447 final boolean register = mAttachedViews.isEmpty();
448 mAttachedViews.add(v);
449 if (register) {
450 register(getApplicationContextIfAvailable(v.getContext()));
451 }
Adam Powell740da472014-11-07 14:51:16 -0800452 }
453 }
454
455 public void removeView(DateTimeView v) {
Jason Monkcd26af72017-01-11 14:32:58 -0500456 synchronized (mAttachedViews) {
Riddle Hsue8f0fe32018-06-05 21:11:59 +0800457 final boolean removed = mAttachedViews.remove(v);
458 // Only unregister once when we remove the last view in the list otherwise we risk
459 // trying to unregister a receiver that is no longer registered.
460 if (removed && mAttachedViews.isEmpty()) {
Jason Monkcd26af72017-01-11 14:32:58 -0500461 unregister(getApplicationContextIfAvailable(v.getContext()));
462 }
Adam Powell740da472014-11-07 14:51:16 -0800463 }
464 }
465
466 void updateAll() {
Jason Monkcd26af72017-01-11 14:32:58 -0500467 synchronized (mAttachedViews) {
468 final int count = mAttachedViews.size();
469 for (int i = 0; i < count; i++) {
470 DateTimeView view = mAttachedViews.get(i);
471 view.post(() -> view.clearFormatAndUpdate());
472 }
Adam Powell740da472014-11-07 14:51:16 -0800473 }
474 }
475
476 long getSoonestUpdateTime() {
477 long result = Long.MAX_VALUE;
Jason Monkcd26af72017-01-11 14:32:58 -0500478 synchronized (mAttachedViews) {
479 final int count = mAttachedViews.size();
480 for (int i = 0; i < count; i++) {
481 final long time = mAttachedViews.get(i).mUpdateTimeMillis;
482 if (time < result) {
483 result = time;
484 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800485 }
486 }
Adam Powell740da472014-11-07 14:51:16 -0800487 return result;
Joe Onoratoc83bb732010-01-19 16:32:22 -0800488 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800489
Dan Sandler7bd8e6a2015-07-30 11:46:10 -0400490 static final Context getApplicationContextIfAvailable(Context context) {
491 final Context ac = context.getApplicationContext();
Dan Sandler4b677132015-07-30 22:33:12 -0400492 return ac != null ? ac : ActivityThread.currentApplication().getApplicationContext();
Dan Sandler7bd8e6a2015-07-30 11:46:10 -0400493 }
494
Adam Powell740da472014-11-07 14:51:16 -0800495 void register(Context context) {
496 final IntentFilter filter = new IntentFilter();
497 filter.addAction(Intent.ACTION_TIME_TICK);
498 filter.addAction(Intent.ACTION_TIME_CHANGED);
499 filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
500 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
Jason Monkcd26af72017-01-11 14:32:58 -0500501 context.registerReceiver(mReceiver, filter, null, mHandler);
Joe Onoratoc83bb732010-01-19 16:32:22 -0800502 }
Adam Powell740da472014-11-07 14:51:16 -0800503
504 void unregister(Context context) {
505 context.unregisterReceiver(mReceiver);
Adam Powell740da472014-11-07 14:51:16 -0800506 }
Jason Monkcd26af72017-01-11 14:32:58 -0500507
508 public void setHandler(Handler handler) {
509 mHandler = handler;
510 synchronized (mAttachedViews) {
511 if (!mAttachedViews.isEmpty()) {
512 unregister(mAttachedViews.get(0).getContext());
513 register(mAttachedViews.get(0).getContext());
514 }
515 }
516 }
Adam Powell740da472014-11-07 14:51:16 -0800517 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800518}