blob: 4db3607f95f915d12e343653c8dd21c0174ccbac [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
Dan Sandler4b677132015-07-30 22:33:12 -040025import android.app.ActivityThread;
Selim Cinek36d57082016-04-19 18:31:47 -070026import android.content.BroadcastReceiver;
Joe Onoratoc83bb732010-01-19 16:32:22 -080027import android.content.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
Selim Cinek36d57082016-04-19 18:31:47 -070030import android.content.res.Configuration;
31import android.content.res.TypedArray;
Joe Onoratoc83bb732010-01-19 16:32:22 -080032import android.database.ContentObserver;
Selim Cinek36d57082016-04-19 18:31:47 -070033import android.icu.util.Calendar;
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;
Joe Onoratoc83bb732010-01-19 16:32:22 -080044import java.util.Date;
Selim Cinek36d57082016-04-19 18:31:47 -070045import java.util.TimeZone;
Joe Onoratoc83bb732010-01-19 16:32:22 -080046
47//
48// TODO
49// - listen for the next threshold time to update the view.
50// - listen for date format pref changed
51// - put the AM/PM in a smaller font
52//
53
54/**
55 * Displays a given time in a convenient human-readable foramt.
56 *
57 * @hide
58 */
59@RemoteView
60public class DateTimeView extends TextView {
Joe Onoratoc83bb732010-01-19 16:32:22 -080061 private static final int SHOW_TIME = 0;
62 private static final int SHOW_MONTH_DAY_YEAR = 1;
63
64 Date mTime;
65 long mTimeMillis;
66
67 int mLastDisplay = -1;
68 DateFormat mLastFormat;
69
Joe Onoratoc83bb732010-01-19 16:32:22 -080070 private long mUpdateTimeMillis;
Adam Powell740da472014-11-07 14:51:16 -080071 private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>();
Selim Cinek36d57082016-04-19 18:31:47 -070072 private String mNowText;
73 private boolean mShowRelativeTime;
Joe Onoratoc83bb732010-01-19 16:32:22 -080074
75 public DateTimeView(Context context) {
Selim Cinek36d57082016-04-19 18:31:47 -070076 this(context, null);
Joe Onoratoc83bb732010-01-19 16:32:22 -080077 }
78
79 public DateTimeView(Context context, AttributeSet attrs) {
80 super(context, attrs);
Selim Cinek36d57082016-04-19 18:31:47 -070081 final TypedArray a = context.obtainStyledAttributes(attrs,
82 com.android.internal.R.styleable.DateTimeView, 0,
83 0);
84
85 final int N = a.getIndexCount();
86 for (int i = 0; i < N; i++) {
87 int attr = a.getIndex(i);
88 switch (attr) {
89 case R.styleable.DateTimeView_showRelative:
90 boolean relative = a.getBoolean(i, false);
91 setShowRelativeTime(relative);
92 break;
93 }
94 }
95 a.recycle();
Joe Onoratoc83bb732010-01-19 16:32:22 -080096 }
97
98 @Override
99 protected void onAttachedToWindow() {
Daniel Sandler8d3c2342011-08-09 13:10:20 -0400100 super.onAttachedToWindow();
Adam Powell740da472014-11-07 14:51:16 -0800101 ReceiverInfo ri = sReceiverInfo.get();
102 if (ri == null) {
103 ri = new ReceiverInfo();
104 sReceiverInfo.set(ri);
105 }
106 ri.addView(this);
Joe Onoratoc83bb732010-01-19 16:32:22 -0800107 }
108
109 @Override
110 protected void onDetachedFromWindow() {
111 super.onDetachedFromWindow();
Adam Powell740da472014-11-07 14:51:16 -0800112 final ReceiverInfo ri = sReceiverInfo.get();
113 if (ri != null) {
114 ri.removeView(this);
115 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800116 }
117
118 @android.view.RemotableViewMethod
119 public void setTime(long time) {
120 Time t = new Time();
121 t.set(time);
Joe Onoratoc83bb732010-01-19 16:32:22 -0800122 mTimeMillis = t.toMillis(false);
123 mTime = new Date(t.year-1900, t.month, t.monthDay, t.hour, t.minute, 0);
124 update();
125 }
126
Selim Cinek36d57082016-04-19 18:31:47 -0700127 @android.view.RemotableViewMethod
128 public void setShowRelativeTime(boolean showRelativeTime) {
129 mShowRelativeTime = showRelativeTime;
130 updateNowText();
131 update();
132 }
133
Selim Cinekb85f36fd2016-04-20 18:46:36 -0700134 @Override
135 @android.view.RemotableViewMethod
136 public void setVisibility(@Visibility int visibility) {
137 boolean gotVisible = visibility != GONE && getVisibility() == GONE;
138 super.setVisibility(visibility);
139 if (gotVisible) {
140 update();
141 }
142 }
143
Joe Onoratoc83bb732010-01-19 16:32:22 -0800144 void update() {
Selim Cinekb85f36fd2016-04-20 18:46:36 -0700145 if (mTime == null || getVisibility() == GONE) {
Joe Onoratoc83bb732010-01-19 16:32:22 -0800146 return;
147 }
Selim Cinek36d57082016-04-19 18:31:47 -0700148 if (mShowRelativeTime) {
149 updateRelativeTime();
150 return;
151 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800152
153 int display;
154 Date time = mTime;
155
156 Time t = new Time();
157 t.set(mTimeMillis);
158 t.second = 0;
159
160 t.hour -= 12;
161 long twelveHoursBefore = t.toMillis(false);
162 t.hour += 12;
163 long twelveHoursAfter = t.toMillis(false);
164 t.hour = 0;
165 t.minute = 0;
166 long midnightBefore = t.toMillis(false);
167 t.monthDay++;
168 long midnightAfter = t.toMillis(false);
169
170 long nowMillis = System.currentTimeMillis();
171 t.set(nowMillis);
172 t.second = 0;
173 nowMillis = t.normalize(false);
174
175 // Choose the display mode
176 choose_display: {
177 if ((nowMillis >= midnightBefore && nowMillis < midnightAfter)
178 || (nowMillis >= twelveHoursBefore && nowMillis < twelveHoursAfter)) {
179 display = SHOW_TIME;
180 break choose_display;
181 }
182 // Else, show month day and year.
183 display = SHOW_MONTH_DAY_YEAR;
184 break choose_display;
185 }
186
187 // Choose the format
188 DateFormat format;
189 if (display == mLastDisplay && mLastFormat != null) {
190 // use cached format
191 format = mLastFormat;
192 } else {
193 switch (display) {
194 case SHOW_TIME:
195 format = getTimeFormat();
196 break;
197 case SHOW_MONTH_DAY_YEAR:
Narayan Kamath775eca12014-11-18 14:52:04 +0000198 format = DateFormat.getDateInstance(DateFormat.SHORT);
Joe Onoratoc83bb732010-01-19 16:32:22 -0800199 break;
200 default:
201 throw new RuntimeException("unknown display value: " + display);
202 }
203 mLastFormat = format;
204 }
205
206 // Set the text
207 String text = format.format(mTime);
208 setText(text);
209
210 // Schedule the next update
211 if (display == SHOW_TIME) {
212 // Currently showing the time, update at the later of twelve hours after or midnight.
213 mUpdateTimeMillis = twelveHoursAfter > midnightAfter ? twelveHoursAfter : midnightAfter;
214 } else {
215 // Currently showing the date
216 if (mTimeMillis < nowMillis) {
217 // If the time is in the past, don't schedule an update
218 mUpdateTimeMillis = 0;
219 } else {
220 // If hte time is in the future, schedule one at the earlier of twelve hours
221 // before or midnight before.
222 mUpdateTimeMillis = twelveHoursBefore < midnightBefore
223 ? twelveHoursBefore : midnightBefore;
224 }
225 }
Selim Cinek36d57082016-04-19 18:31:47 -0700226 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800227
Selim Cinek36d57082016-04-19 18:31:47 -0700228 private void updateRelativeTime() {
229 long now = System.currentTimeMillis();
230 long duration = Math.abs(now - mTimeMillis);
231 int count;
232 long millisIncrease;
233 boolean past = (now >= mTimeMillis);
234 String result;
235 if (duration < MINUTE_IN_MILLIS) {
236 setText(mNowText);
237 mUpdateTimeMillis = mTimeMillis + MINUTE_IN_MILLIS + 1;
238 return;
239 } else if (duration < HOUR_IN_MILLIS) {
240 count = (int)(duration / MINUTE_IN_MILLIS);
241 result = String.format(getContext().getResources().getQuantityString(past
242 ? com.android.internal.R.plurals.duration_minutes_shortest
243 : com.android.internal.R.plurals.duration_minutes_shortest_future,
244 count),
245 count);
246 millisIncrease = MINUTE_IN_MILLIS;
247 } else if (duration < DAY_IN_MILLIS) {
248 count = (int)(duration / HOUR_IN_MILLIS);
249 result = String.format(getContext().getResources().getQuantityString(past
250 ? com.android.internal.R.plurals.duration_hours_shortest
251 : com.android.internal.R.plurals.duration_hours_shortest_future,
252 count),
253 count);
254 millisIncrease = HOUR_IN_MILLIS;
255 } else if (duration < YEAR_IN_MILLIS) {
256 // In weird cases it can become 0 because of daylight savings
257 TimeZone timeZone = TimeZone.getDefault();
258 count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
259 result = String.format(getContext().getResources().getQuantityString(past
260 ? com.android.internal.R.plurals.duration_days_shortest
261 : com.android.internal.R.plurals.duration_days_shortest_future,
262 count),
263 count);
264 if (past || count != 1) {
265 mUpdateTimeMillis = computeNextMidnight(timeZone);
266 millisIncrease = -1;
267 } else {
268 millisIncrease = DAY_IN_MILLIS;
269 }
270
271 } else {
272 count = (int)(duration / YEAR_IN_MILLIS);
273 result = String.format(getContext().getResources().getQuantityString(past
274 ? com.android.internal.R.plurals.duration_years_shortest
275 : com.android.internal.R.plurals.duration_years_shortest_future,
276 count),
277 count);
278 millisIncrease = YEAR_IN_MILLIS;
279 }
280 if (millisIncrease != -1) {
281 if (past) {
282 mUpdateTimeMillis = mTimeMillis + millisIncrease * (count + 1) + 1;
283 } else {
284 mUpdateTimeMillis = mTimeMillis - millisIncrease * count + 1;
285 }
286 }
287 setText(result);
288 }
289
290 /**
291 * @param timeZone the timezone we are in
292 * @return the timepoint in millis at UTC at midnight in the current timezone
293 */
294 private long computeNextMidnight(TimeZone timeZone) {
295 Calendar c = Calendar.getInstance();
296 c.setTimeZone(libcore.icu.DateUtilsBridge.icuTimeZone(timeZone));
297 c.add(Calendar.DAY_OF_MONTH, 1);
298 c.set(Calendar.HOUR_OF_DAY, 0);
299 c.set(Calendar.MINUTE, 0);
300 c.set(Calendar.SECOND, 0);
301 c.set(Calendar.MILLISECOND, 0);
302 return c.getTimeInMillis();
303 }
304
305 @Override
306 protected void onConfigurationChanged(Configuration newConfig) {
307 super.onConfigurationChanged(newConfig);
308 updateNowText();
309 update();
310 }
311
312 private void updateNowText() {
313 if (!mShowRelativeTime) {
314 return;
315 }
316 mNowText = getContext().getResources().getString(
317 com.android.internal.R.string.now_string_shortest);
318 }
319
320 // Return the date difference for the two times in a given timezone.
321 private static int dayDistance(TimeZone timeZone, long startTime,
322 long endTime) {
323 return getJulianDay(endTime, timeZone.getOffset(endTime) / 1000)
324 - getJulianDay(startTime, timeZone.getOffset(startTime) / 1000);
Joe Onoratoc83bb732010-01-19 16:32:22 -0800325 }
326
327 private DateFormat getTimeFormat() {
Elliott Hughes4caba612013-01-14 15:48:27 -0800328 return android.text.format.DateFormat.getTimeFormat(getContext());
Joe Onoratoc83bb732010-01-19 16:32:22 -0800329 }
330
Adam Powell740da472014-11-07 14:51:16 -0800331 void clearFormatAndUpdate() {
332 mLastFormat = null;
333 update();
Joe Onoratoc83bb732010-01-19 16:32:22 -0800334 }
335
Selim Cinek570bfa22016-05-25 18:44:44 -0700336 @Override
337 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
338 super.onInitializeAccessibilityNodeInfoInternal(info);
339 if (mShowRelativeTime) {
340 // The short version of the time might not be completely understandable and for
341 // accessibility we rather have a longer version.
342 long now = System.currentTimeMillis();
343 long duration = Math.abs(now - mTimeMillis);
344 int count;
345 boolean past = (now >= mTimeMillis);
346 String result;
347 if (duration < MINUTE_IN_MILLIS) {
348 result = mNowText;
349 } else if (duration < HOUR_IN_MILLIS) {
350 count = (int)(duration / MINUTE_IN_MILLIS);
351 result = String.format(getContext().getResources().getQuantityString(past
352 ? com.android.internal.
353 R.plurals.duration_minutes_relative
354 : com.android.internal.
355 R.plurals.duration_minutes_relative_future,
356 count),
357 count);
358 } else if (duration < DAY_IN_MILLIS) {
359 count = (int)(duration / HOUR_IN_MILLIS);
360 result = String.format(getContext().getResources().getQuantityString(past
361 ? com.android.internal.
362 R.plurals.duration_hours_relative
363 : com.android.internal.
364 R.plurals.duration_hours_relative_future,
365 count),
366 count);
367 } else if (duration < YEAR_IN_MILLIS) {
368 // In weird cases it can become 0 because of daylight savings
369 TimeZone timeZone = TimeZone.getDefault();
370 count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
371 result = String.format(getContext().getResources().getQuantityString(past
372 ? com.android.internal.
373 R.plurals.duration_days_relative
374 : com.android.internal.
375 R.plurals.duration_days_relative_future,
376 count),
377 count);
378
379 } else {
380 count = (int)(duration / YEAR_IN_MILLIS);
381 result = String.format(getContext().getResources().getQuantityString(past
382 ? com.android.internal.
383 R.plurals.duration_years_relative
384 : com.android.internal.
385 R.plurals.duration_years_relative_future,
386 count),
387 count);
388 }
389 info.setText(result);
390 }
391 }
392
Jason Monkcd26af72017-01-11 14:32:58 -0500393 /**
394 * @hide
395 */
396 public static void setReceiverHandler(Handler handler) {
397 ReceiverInfo ri = sReceiverInfo.get();
398 if (ri == null) {
399 ri = new ReceiverInfo();
400 sReceiverInfo.set(ri);
401 }
402 ri.setHandler(handler);
403 }
404
Adam Powell740da472014-11-07 14:51:16 -0800405 private static class ReceiverInfo {
406 private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>();
407 private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
408 @Override
409 public void onReceive(Context context, Intent intent) {
410 String action = intent.getAction();
411 if (Intent.ACTION_TIME_TICK.equals(action)) {
412 if (System.currentTimeMillis() < getSoonestUpdateTime()) {
413 // The update() function takes a few milliseconds to run because of
414 // all of the time conversions it needs to do, so we can't do that
415 // every minute.
416 return;
417 }
418 }
419 // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format.
420 updateAll();
421 }
422 };
Joe Onoratoc83bb732010-01-19 16:32:22 -0800423
Adam Powell740da472014-11-07 14:51:16 -0800424 private final ContentObserver mObserver = new ContentObserver(new Handler()) {
425 @Override
426 public void onChange(boolean selfChange) {
427 updateAll();
428 }
429 };
430
Jason Monkcd26af72017-01-11 14:32:58 -0500431 private Handler mHandler = new Handler();
432
Adam Powell740da472014-11-07 14:51:16 -0800433 public void addView(DateTimeView v) {
Jason Monkcd26af72017-01-11 14:32:58 -0500434 synchronized (mAttachedViews) {
435 final boolean register = mAttachedViews.isEmpty();
436 mAttachedViews.add(v);
437 if (register) {
438 register(getApplicationContextIfAvailable(v.getContext()));
439 }
Adam Powell740da472014-11-07 14:51:16 -0800440 }
441 }
442
443 public void removeView(DateTimeView v) {
Jason Monkcd26af72017-01-11 14:32:58 -0500444 synchronized (mAttachedViews) {
445 mAttachedViews.remove(v);
446 if (mAttachedViews.isEmpty()) {
447 unregister(getApplicationContextIfAvailable(v.getContext()));
448 }
Adam Powell740da472014-11-07 14:51:16 -0800449 }
450 }
451
452 void updateAll() {
Jason Monkcd26af72017-01-11 14:32:58 -0500453 synchronized (mAttachedViews) {
454 final int count = mAttachedViews.size();
455 for (int i = 0; i < count; i++) {
456 DateTimeView view = mAttachedViews.get(i);
457 view.post(() -> view.clearFormatAndUpdate());
458 }
Adam Powell740da472014-11-07 14:51:16 -0800459 }
460 }
461
462 long getSoonestUpdateTime() {
463 long result = Long.MAX_VALUE;
Jason Monkcd26af72017-01-11 14:32:58 -0500464 synchronized (mAttachedViews) {
465 final int count = mAttachedViews.size();
466 for (int i = 0; i < count; i++) {
467 final long time = mAttachedViews.get(i).mUpdateTimeMillis;
468 if (time < result) {
469 result = time;
470 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800471 }
472 }
Adam Powell740da472014-11-07 14:51:16 -0800473 return result;
Joe Onoratoc83bb732010-01-19 16:32:22 -0800474 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800475
Dan Sandler7bd8e6a2015-07-30 11:46:10 -0400476 static final Context getApplicationContextIfAvailable(Context context) {
477 final Context ac = context.getApplicationContext();
Dan Sandler4b677132015-07-30 22:33:12 -0400478 return ac != null ? ac : ActivityThread.currentApplication().getApplicationContext();
Dan Sandler7bd8e6a2015-07-30 11:46:10 -0400479 }
480
Adam Powell740da472014-11-07 14:51:16 -0800481 void register(Context context) {
482 final IntentFilter filter = new IntentFilter();
483 filter.addAction(Intent.ACTION_TIME_TICK);
484 filter.addAction(Intent.ACTION_TIME_CHANGED);
485 filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
486 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
Jason Monkcd26af72017-01-11 14:32:58 -0500487 context.registerReceiver(mReceiver, filter, null, mHandler);
Joe Onoratoc83bb732010-01-19 16:32:22 -0800488 }
Adam Powell740da472014-11-07 14:51:16 -0800489
490 void unregister(Context context) {
491 context.unregisterReceiver(mReceiver);
Adam Powell740da472014-11-07 14:51:16 -0800492 }
Jason Monkcd26af72017-01-11 14:32:58 -0500493
494 public void setHandler(Handler handler) {
495 mHandler = handler;
496 synchronized (mAttachedViews) {
497 if (!mAttachedViews.isEmpty()) {
498 unregister(mAttachedViews.get(0).getContext());
499 register(mAttachedViews.get(0).getContext());
500 }
501 }
502 }
Adam Powell740da472014-11-07 14:51:16 -0800503 }
Joe Onoratoc83bb732010-01-19 16:32:22 -0800504}