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 24 import android.annotation.IntDef; 25 import android.app.ActivityThread; 26 import android.compat.annotation.UnsupportedAppUsage; 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.Build; 35 import android.os.Handler; 36 import android.text.TextUtils; 37 import android.util.AttributeSet; 38 import android.util.PluralsMessageFormatter; 39 import android.view.accessibility.AccessibilityNodeInfo; 40 import android.view.inspector.InspectableProperty; 41 import android.widget.RemoteViews.RemoteView; 42 43 import com.android.internal.R; 44 45 import java.lang.annotation.Retention; 46 import java.lang.annotation.RetentionPolicy; 47 import java.text.DateFormat; 48 import java.time.Instant; 49 import java.time.LocalDate; 50 import java.time.LocalDateTime; 51 import java.time.LocalTime; 52 import java.time.ZoneId; 53 import java.time.temporal.JulianFields; 54 import java.util.ArrayList; 55 import java.util.Date; 56 import java.util.HashMap; 57 import java.util.Map; 58 59 // 60 // TODO 61 // - listen for the next threshold time to update the view. 62 // - listen for date format pref changed 63 // - put the AM/PM in a smaller font 64 // 65 66 /** 67 * Displays a given time in a convenient human-readable foramt. 68 * 69 * @hide 70 */ 71 @RemoteView 72 public class DateTimeView extends TextView { 73 private static final int SHOW_TIME = 0; 74 private static final int SHOW_MONTH_DAY_YEAR = 1; 75 76 /** @hide */ 77 @IntDef(value = {UNIT_DISPLAY_LENGTH_SHORTEST, UNIT_DISPLAY_LENGTH_MEDIUM}) 78 @Retention(RetentionPolicy.SOURCE) 79 public @interface UnitDisplayLength {} 80 public static final int UNIT_DISPLAY_LENGTH_SHORTEST = 0; 81 public static final int UNIT_DISPLAY_LENGTH_MEDIUM = 1; 82 83 /** @hide */ 84 @IntDef(flag = true, value = {DISAMBIGUATION_TEXT_PAST, DISAMBIGUATION_TEXT_FUTURE}) 85 @Retention(RetentionPolicy.SOURCE) 86 public @interface DisambiguationTextMask {} 87 public static final int DISAMBIGUATION_TEXT_PAST = 0x01; 88 public static final int DISAMBIGUATION_TEXT_FUTURE = 0x02; 89 90 private final boolean mCanUseRelativeTimeDisplayConfigs = 91 android.view.flags.Flags.dateTimeViewRelativeTimeDisplayConfigs(); 92 93 private long mTimeMillis; 94 // The LocalDateTime equivalent of mTimeMillis but truncated to minute, i.e. no seconds / nanos. 95 private LocalDateTime mLocalTime; 96 97 int mLastDisplay = -1; 98 DateFormat mLastFormat; 99 100 private long mUpdateTimeMillis; 101 private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>(); 102 private String mNowText; 103 private boolean mShowRelativeTime; 104 private int mRelativeTimeDisambiguationTextMask; 105 private int mRelativeTimeUnitDisplayLength = UNIT_DISPLAY_LENGTH_SHORTEST; 106 DateTimeView(Context context)107 public DateTimeView(Context context) { 108 this(context, null); 109 } 110 111 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) DateTimeView(Context context, AttributeSet attrs)112 public DateTimeView(Context context, AttributeSet attrs) { 113 super(context, attrs); 114 final TypedArray a = context.obtainStyledAttributes( 115 attrs, R.styleable.DateTimeView, 0, 0); 116 117 setShowRelativeTime(a.getBoolean(R.styleable.DateTimeView_showRelative, false)); 118 if (mCanUseRelativeTimeDisplayConfigs) { 119 setRelativeTimeDisambiguationTextMask( 120 a.getInt( 121 R.styleable.DateTimeView_relativeTimeDisambiguationText, 122 // The original implementation showed disambiguation text for future 123 // times only, so continue with that default. 124 DISAMBIGUATION_TEXT_FUTURE)); 125 setRelativeTimeUnitDisplayLength( 126 a.getInt( 127 R.styleable.DateTimeView_relativeTimeUnitDisplayLength, 128 UNIT_DISPLAY_LENGTH_SHORTEST)); 129 } 130 131 a.recycle(); 132 } 133 134 @Override onAttachedToWindow()135 protected void onAttachedToWindow() { 136 super.onAttachedToWindow(); 137 ReceiverInfo ri = sReceiverInfo.get(); 138 if (ri == null) { 139 ri = new ReceiverInfo(); 140 sReceiverInfo.set(ri); 141 } 142 ri.addView(this); 143 // The view may not be added to the view hierarchy immediately right after setTime() 144 // is called which means it won't get any update from intents before being added. 145 // In such case, the view might show the incorrect relative time after being added to the 146 // view hierarchy until the next update intent comes. 147 // So we update the time here if mShowRelativeTime is enabled to prevent this case. 148 if (mShowRelativeTime) { 149 update(); 150 } 151 } 152 153 @Override onDetachedFromWindow()154 protected void onDetachedFromWindow() { 155 super.onDetachedFromWindow(); 156 final ReceiverInfo ri = sReceiverInfo.get(); 157 if (ri != null) { 158 ri.removeView(this); 159 } 160 } 161 162 @android.view.RemotableViewMethod 163 @UnsupportedAppUsage setTime(long timeMillis)164 public void setTime(long timeMillis) { 165 mTimeMillis = timeMillis; 166 LocalDateTime dateTime = toLocalDateTime(timeMillis, ZoneId.systemDefault()); 167 mLocalTime = dateTime.withSecond(0); 168 update(); 169 } 170 171 @android.view.RemotableViewMethod setShowRelativeTime(boolean showRelativeTime)172 public void setShowRelativeTime(boolean showRelativeTime) { 173 mShowRelativeTime = showRelativeTime; 174 updateNowText(); 175 update(); 176 } 177 178 /** See {@link R.styleable.DateTimeView_relativeTimeDisambiguationText}. */ 179 @android.view.RemotableViewMethod setRelativeTimeDisambiguationTextMask( @isambiguationTextMask int disambiguationTextMask)180 public void setRelativeTimeDisambiguationTextMask( 181 @DisambiguationTextMask int disambiguationTextMask) { 182 if (!mCanUseRelativeTimeDisplayConfigs) { 183 return; 184 } 185 mRelativeTimeDisambiguationTextMask = disambiguationTextMask; 186 updateNowText(); 187 update(); 188 } 189 190 /** See {@link R.styleable.DateTimeView_relativeTimeUnitDisplayLength}. */ 191 @android.view.RemotableViewMethod setRelativeTimeUnitDisplayLength(@nitDisplayLength int unitDisplayLength)192 public void setRelativeTimeUnitDisplayLength(@UnitDisplayLength int unitDisplayLength) { 193 if (!mCanUseRelativeTimeDisplayConfigs) { 194 return; 195 } 196 mRelativeTimeUnitDisplayLength = unitDisplayLength; 197 updateNowText(); 198 update(); 199 } 200 201 /** 202 * Returns whether this view shows relative time 203 * 204 * @return True if it shows relative time, false otherwise 205 */ 206 @InspectableProperty(name = "showReleative", hasAttributeId = false) isShowRelativeTime()207 public boolean isShowRelativeTime() { 208 return mShowRelativeTime; 209 } 210 211 @Override 212 @android.view.RemotableViewMethod setVisibility(@isibility int visibility)213 public void setVisibility(@Visibility int visibility) { 214 boolean gotVisible = visibility != GONE && getVisibility() == GONE; 215 super.setVisibility(visibility); 216 if (gotVisible) { 217 update(); 218 } 219 } 220 221 @UnsupportedAppUsage update()222 void update() { 223 if (mLocalTime == null || getVisibility() == GONE) { 224 return; 225 } 226 if (mShowRelativeTime) { 227 updateRelativeTime(); 228 return; 229 } 230 231 int display; 232 ZoneId zoneId = ZoneId.systemDefault(); 233 234 // localTime is the local time for mTimeMillis but at zero seconds past the minute. 235 LocalDateTime localTime = mLocalTime; 236 LocalDateTime localStartOfDay = 237 LocalDateTime.of(localTime.toLocalDate(), LocalTime.MIDNIGHT); 238 LocalDateTime localTomorrowStartOfDay = localStartOfDay.plusDays(1); 239 // now is current local time but at zero seconds past the minute. 240 LocalDateTime localNow = LocalDateTime.now(zoneId).withSecond(0); 241 242 long twelveHoursBefore = toEpochMillis(localTime.minusHours(12), zoneId); 243 long twelveHoursAfter = toEpochMillis(localTime.plusHours(12), zoneId); 244 long midnightBefore = toEpochMillis(localStartOfDay, zoneId); 245 long midnightAfter = toEpochMillis(localTomorrowStartOfDay, zoneId); 246 long time = toEpochMillis(localTime, zoneId); 247 long now = toEpochMillis(localNow, zoneId); 248 249 // Choose the display mode 250 choose_display: { 251 if ((now >= midnightBefore && now < midnightAfter) 252 || (now >= twelveHoursBefore && now < twelveHoursAfter)) { 253 display = SHOW_TIME; 254 break choose_display; 255 } 256 // Else, show month day and year. 257 display = SHOW_MONTH_DAY_YEAR; 258 break choose_display; 259 } 260 261 // Choose the format 262 DateFormat format; 263 if (display == mLastDisplay && mLastFormat != null) { 264 // use cached format 265 format = mLastFormat; 266 } else { 267 switch (display) { 268 case SHOW_TIME: 269 format = getTimeFormat(); 270 break; 271 case SHOW_MONTH_DAY_YEAR: 272 format = DateFormat.getDateInstance(DateFormat.SHORT); 273 break; 274 default: 275 throw new RuntimeException("unknown display value: " + display); 276 } 277 mLastFormat = format; 278 } 279 280 // Set the text 281 String text = format.format(new Date(time)); 282 maybeSetText(text); 283 284 // Schedule the next update 285 if (display == SHOW_TIME) { 286 // Currently showing the time, update at the later of twelve hours after or midnight. 287 mUpdateTimeMillis = twelveHoursAfter > midnightAfter ? twelveHoursAfter : midnightAfter; 288 } else { 289 // Currently showing the date 290 if (mTimeMillis < now) { 291 // If the time is in the past, don't schedule an update 292 mUpdateTimeMillis = 0; 293 } else { 294 // If hte time is in the future, schedule one at the earlier of twelve hours 295 // before or midnight before. 296 mUpdateTimeMillis = twelveHoursBefore < midnightBefore 297 ? twelveHoursBefore : midnightBefore; 298 } 299 } 300 } 301 302 private void updateRelativeTime() { 303 long now = System.currentTimeMillis(); 304 long duration = Math.abs(now - mTimeMillis); 305 int count; 306 long millisIncrease; 307 boolean past = (now >= mTimeMillis); 308 String result; 309 if (duration < MINUTE_IN_MILLIS) { 310 maybeSetText(mNowText); 311 mUpdateTimeMillis = mTimeMillis + MINUTE_IN_MILLIS + 1; 312 return; 313 } else if (duration < HOUR_IN_MILLIS) { 314 count = (int)(duration / MINUTE_IN_MILLIS); 315 result = getContext().getResources().getString(getMinutesStringId(past), count); 316 millisIncrease = MINUTE_IN_MILLIS; 317 } else if (duration < DAY_IN_MILLIS) { 318 count = (int)(duration / HOUR_IN_MILLIS); 319 result = getContext().getResources().getString(getHoursStringId(past), count); 320 millisIncrease = HOUR_IN_MILLIS; 321 } else if (duration < YEAR_IN_MILLIS) { 322 // In weird cases it can become 0 because of daylight savings 323 LocalDateTime localDateTime = mLocalTime; 324 ZoneId zoneId = ZoneId.systemDefault(); 325 LocalDateTime localNow = toLocalDateTime(now, zoneId); 326 327 count = Math.max(Math.abs(dayDistance(localDateTime, localNow)), 1); 328 result = getContext().getResources().getString(getDaysStringId(past), count); 329 if (past || count != 1) { 330 mUpdateTimeMillis = computeNextMidnight(localNow, zoneId); 331 millisIncrease = -1; 332 } else { 333 millisIncrease = DAY_IN_MILLIS; 334 } 335 336 } else { 337 count = (int)(duration / YEAR_IN_MILLIS); 338 result = getContext().getResources().getString(getYearsStringId(past), count); 339 millisIncrease = YEAR_IN_MILLIS; 340 } 341 if (millisIncrease != -1) { 342 if (past) { 343 mUpdateTimeMillis = mTimeMillis + millisIncrease * (count + 1) + 1; 344 } else { 345 mUpdateTimeMillis = mTimeMillis - millisIncrease * count + 1; 346 } 347 } 348 maybeSetText(result); 349 } 350 getMinutesStringId(boolean past)351 private int getMinutesStringId(boolean past) { 352 if (!mCanUseRelativeTimeDisplayConfigs) { 353 return past 354 ? com.android.internal.R.string.duration_minutes_shortest 355 : com.android.internal.R.string.duration_minutes_shortest_future; 356 } 357 358 if (mRelativeTimeUnitDisplayLength == UNIT_DISPLAY_LENGTH_SHORTEST) { 359 if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) { 360 // "1m ago" 361 return com.android.internal.R.string.duration_minutes_shortest_past; 362 } else if (!past 363 && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) { 364 // "in 1m" 365 return com.android.internal.R.string.duration_minutes_shortest_future; 366 } else { 367 // "1m" 368 return com.android.internal.R.string.duration_minutes_shortest; 369 } 370 } else { // UNIT_DISPLAY_LENGTH_MEDIUM 371 if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) { 372 // "1min ago" 373 return com.android.internal.R.string.duration_minutes_medium_past; 374 } else if (!past 375 && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) { 376 // "in 1min" 377 return com.android.internal.R.string.duration_minutes_medium_future; 378 } else { 379 // "1min" 380 return com.android.internal.R.string.duration_minutes_medium; 381 } 382 } 383 } 384 getHoursStringId(boolean past)385 private int getHoursStringId(boolean past) { 386 if (!mCanUseRelativeTimeDisplayConfigs) { 387 return past 388 ? com.android.internal.R.string.duration_hours_shortest 389 : com.android.internal.R.string.duration_hours_shortest_future; 390 } 391 if (mRelativeTimeUnitDisplayLength == UNIT_DISPLAY_LENGTH_SHORTEST) { 392 if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) { 393 // "1h ago" 394 return com.android.internal.R.string.duration_hours_shortest_past; 395 } else if (!past 396 && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) { 397 // "in 1h" 398 return com.android.internal.R.string.duration_hours_shortest_future; 399 } else { 400 // "1h" 401 return com.android.internal.R.string.duration_hours_shortest; 402 } 403 } else { // UNIT_DISPLAY_LENGTH_MEDIUM 404 if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) { 405 // "1hr ago" 406 return com.android.internal.R.string.duration_hours_medium_past; 407 } else if (!past 408 && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) { 409 // "in 1hr" 410 return com.android.internal.R.string.duration_hours_medium_future; 411 } else { 412 // "1hr" 413 return com.android.internal.R.string.duration_hours_medium; 414 } 415 } 416 } 417 getDaysStringId(boolean past)418 private int getDaysStringId(boolean past) { 419 if (!mCanUseRelativeTimeDisplayConfigs) { 420 return past 421 ? com.android.internal.R.string.duration_days_shortest 422 : com.android.internal.R.string.duration_days_shortest_future; 423 } 424 if (mRelativeTimeUnitDisplayLength == UNIT_DISPLAY_LENGTH_SHORTEST) { 425 if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) { 426 // "1d ago" 427 return com.android.internal.R.string.duration_days_shortest_past; 428 } else if (!past 429 && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) { 430 // "in 1d" 431 return com.android.internal.R.string.duration_days_shortest_future; 432 } else { 433 // "1d" 434 return com.android.internal.R.string.duration_days_shortest; 435 } 436 } else { // UNIT_DISPLAY_LENGTH_MEDIUM 437 if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) { 438 // "1d ago" 439 return com.android.internal.R.string.duration_days_medium_past; 440 } else if (!past 441 && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) { 442 // "in 1d" 443 return com.android.internal.R.string.duration_days_medium_future; 444 } else { 445 // "1d" 446 return com.android.internal.R.string.duration_days_medium; 447 } 448 } 449 } 450 getYearsStringId(boolean past)451 private int getYearsStringId(boolean past) { 452 if (!mCanUseRelativeTimeDisplayConfigs) { 453 return past 454 ? com.android.internal.R.string.duration_years_shortest 455 : com.android.internal.R.string.duration_years_shortest_future; 456 } 457 if (mRelativeTimeUnitDisplayLength == UNIT_DISPLAY_LENGTH_SHORTEST) { 458 if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) { 459 // "1y ago" 460 return com.android.internal.R.string.duration_years_shortest_past; 461 } else if (!past 462 && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) { 463 // "in 1y" 464 return com.android.internal.R.string.duration_years_shortest_future; 465 } else { 466 // "1y" 467 return com.android.internal.R.string.duration_years_shortest; 468 } 469 } else { // UNIT_DISPLAY_LENGTH_MEDIUM 470 if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) { 471 // "1y ago" 472 return com.android.internal.R.string.duration_years_medium_past; 473 } else if (!past 474 && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) { 475 // "in 1y" 476 return com.android.internal.R.string.duration_years_medium_future; 477 } else { 478 // "1y" 479 return com.android.internal.R.string.duration_years_medium; 480 } 481 } 482 } 483 484 /** 485 * Sets text only if the text has actually changed. This prevents needles relayouts of this 486 * view when set to wrap_content. 487 */ maybeSetText(String text)488 private void maybeSetText(String text) { 489 if (TextUtils.equals(getText(), text)) { 490 return; 491 } 492 493 setText(text); 494 } 495 496 /** 497 * Returns the epoch millis for the next midnight in the specified timezone. 498 */ computeNextMidnight(LocalDateTime time, ZoneId zoneId)499 private static long computeNextMidnight(LocalDateTime time, ZoneId zoneId) { 500 // This ignores the chance of overflow: it should never happen. 501 LocalDate tomorrow = time.toLocalDate().plusDays(1); 502 LocalDateTime nextMidnight = LocalDateTime.of(tomorrow, LocalTime.MIDNIGHT); 503 return toEpochMillis(nextMidnight, zoneId); 504 } 505 506 @Override onConfigurationChanged(Configuration newConfig)507 protected void onConfigurationChanged(Configuration newConfig) { 508 super.onConfigurationChanged(newConfig); 509 updateNowText(); 510 update(); 511 } 512 updateNowText()513 private void updateNowText() { 514 if (!mShowRelativeTime) { 515 return; 516 } 517 mNowText = getContext().getResources().getString( 518 com.android.internal.R.string.now_string_shortest); 519 } 520 521 // Return the number of days between the two dates. dayDistance(LocalDateTime start, LocalDateTime end)522 private static int dayDistance(LocalDateTime start, LocalDateTime end) { 523 return (int) (end.getLong(JulianFields.JULIAN_DAY) 524 - start.getLong(JulianFields.JULIAN_DAY)); 525 } 526 getTimeFormat()527 private DateFormat getTimeFormat() { 528 return android.text.format.DateFormat.getTimeFormat(getContext()); 529 } 530 clearFormatAndUpdate()531 void clearFormatAndUpdate() { 532 mLastFormat = null; 533 update(); 534 } 535 536 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)537 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 538 super.onInitializeAccessibilityNodeInfoInternal(info); 539 if (mShowRelativeTime) { 540 // The short version of the time might not be completely understandable and for 541 // accessibility we rather have a longer version. 542 long now = System.currentTimeMillis(); 543 long duration = Math.abs(now - mTimeMillis); 544 int count; 545 boolean past = (now >= mTimeMillis); 546 String result; 547 Map<String, Object> arguments = new HashMap<>(); 548 if (duration < MINUTE_IN_MILLIS) { 549 result = mNowText; 550 } else if (duration < HOUR_IN_MILLIS) { 551 count = (int)(duration / MINUTE_IN_MILLIS); 552 arguments.put("count", count); 553 result = PluralsMessageFormatter.format( 554 getContext().getResources(), 555 arguments, 556 past ? R.string.duration_minutes_relative 557 : R.string.duration_minutes_relative_future); 558 } else if (duration < DAY_IN_MILLIS) { 559 count = (int)(duration / HOUR_IN_MILLIS); 560 arguments.put("count", count); 561 result = PluralsMessageFormatter.format( 562 getContext().getResources(), 563 arguments, 564 past ? R.string.duration_hours_relative 565 : R.string.duration_hours_relative_future); 566 } else if (duration < YEAR_IN_MILLIS) { 567 // In weird cases it can become 0 because of daylight savings 568 LocalDateTime localDateTime = mLocalTime; 569 ZoneId zoneId = ZoneId.systemDefault(); 570 LocalDateTime localNow = toLocalDateTime(now, zoneId); 571 572 count = Math.max(Math.abs(dayDistance(localDateTime, localNow)), 1); 573 arguments.put("count", count); 574 result = PluralsMessageFormatter.format( 575 getContext().getResources(), 576 arguments, 577 past ? R.string.duration_days_relative 578 : R.string.duration_days_relative_future); 579 } else { 580 count = (int)(duration / YEAR_IN_MILLIS); 581 arguments.put("count", count); 582 result = PluralsMessageFormatter.format( 583 getContext().getResources(), 584 arguments, 585 past ? R.string.duration_years_relative 586 : R.string.duration_years_relative_future); 587 } 588 info.setText(result); 589 } 590 } 591 592 /** 593 * @hide 594 */ setReceiverHandler(Handler handler)595 public static void setReceiverHandler(Handler handler) { 596 ReceiverInfo ri = sReceiverInfo.get(); 597 if (ri == null) { 598 ri = new ReceiverInfo(); 599 sReceiverInfo.set(ri); 600 } 601 ri.setHandler(handler); 602 } 603 604 private static class ReceiverInfo { 605 private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>(); 606 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 607 @Override 608 public void onReceive(Context context, Intent intent) { 609 String action = intent.getAction(); 610 if (Intent.ACTION_TIME_TICK.equals(action)) { 611 if (System.currentTimeMillis() < getSoonestUpdateTime()) { 612 // The update() function takes a few milliseconds to run because of 613 // all of the time conversions it needs to do, so we can't do that 614 // every minute. 615 return; 616 } 617 } 618 // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format. 619 updateAll(); 620 } 621 }; 622 623 private final ContentObserver mObserver = new ContentObserver(new Handler()) { 624 @Override 625 public void onChange(boolean selfChange) { 626 updateAll(); 627 } 628 }; 629 630 private Handler mHandler = new Handler(); 631 addView(DateTimeView v)632 public void addView(DateTimeView v) { 633 synchronized (mAttachedViews) { 634 final boolean register = mAttachedViews.isEmpty(); 635 mAttachedViews.add(v); 636 if (register) { 637 register(getApplicationContextIfAvailable(v.getContext())); 638 } 639 } 640 } 641 removeView(DateTimeView v)642 public void removeView(DateTimeView v) { 643 synchronized (mAttachedViews) { 644 final boolean removed = mAttachedViews.remove(v); 645 // Only unregister once when we remove the last view in the list otherwise we risk 646 // trying to unregister a receiver that is no longer registered. 647 if (removed && mAttachedViews.isEmpty()) { 648 unregister(getApplicationContextIfAvailable(v.getContext())); 649 } 650 } 651 } 652 updateAll()653 void updateAll() { 654 synchronized (mAttachedViews) { 655 final int count = mAttachedViews.size(); 656 for (int i = 0; i < count; i++) { 657 DateTimeView view = mAttachedViews.get(i); 658 view.post(() -> view.clearFormatAndUpdate()); 659 } 660 } 661 } 662 getSoonestUpdateTime()663 long getSoonestUpdateTime() { 664 long result = Long.MAX_VALUE; 665 synchronized (mAttachedViews) { 666 final int count = mAttachedViews.size(); 667 for (int i = 0; i < count; i++) { 668 final long time = mAttachedViews.get(i).mUpdateTimeMillis; 669 if (time < result) { 670 result = time; 671 } 672 } 673 } 674 return result; 675 } 676 getApplicationContextIfAvailable(Context context)677 static final Context getApplicationContextIfAvailable(Context context) { 678 final Context ac = context.getApplicationContext(); 679 return ac != null ? ac : ActivityThread.currentApplication().getApplicationContext(); 680 } 681 register(Context context)682 void register(Context context) { 683 final IntentFilter filter = new IntentFilter(); 684 filter.addAction(Intent.ACTION_TIME_TICK); 685 filter.addAction(Intent.ACTION_TIME_CHANGED); 686 filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); 687 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 688 context.registerReceiver(mReceiver, filter, null, mHandler); 689 } 690 unregister(Context context)691 void unregister(Context context) { 692 context.unregisterReceiver(mReceiver); 693 } 694 setHandler(Handler handler)695 public void setHandler(Handler handler) { 696 mHandler = handler; 697 synchronized (mAttachedViews) { 698 if (!mAttachedViews.isEmpty()) { 699 unregister(mAttachedViews.get(0).getContext()); 700 register(mAttachedViews.get(0).getContext()); 701 } 702 } 703 } 704 } 705 toLocalDateTime(long timeMillis, ZoneId zoneId)706 private static LocalDateTime toLocalDateTime(long timeMillis, ZoneId zoneId) { 707 // java.time types like LocalDateTime / Instant can support the full range of "long millis" 708 // with room to spare so we do not need to worry about overflow / underflow and the rsulting 709 // exceptions while the input to this class is a long. 710 Instant instant = Instant.ofEpochMilli(timeMillis); 711 return LocalDateTime.ofInstant(instant, zoneId); 712 } 713 toEpochMillis(LocalDateTime time, ZoneId zoneId)714 private static long toEpochMillis(LocalDateTime time, ZoneId zoneId) { 715 Instant instant = time.toInstant(zoneId.getRules().getOffset(time)); 716 return instant.toEpochMilli(); 717 } 718 } 719