From 0bab7fa5121aac02ecc5ff0a6ebed3b1f48c8543 Mon Sep 17 00:00:00 2001 From: Neil Fuller Date: Mon, 17 Jun 2019 16:38:55 +0100 Subject: [PATCH] Switch widgets away from android.text.format.Time android.text.format.Time is limited to 32-bit seconds from the beginning of the Unix epoch and so classes that use it are limited to 1901 - 2038. Switching to java.time avoids this issue. Manual Testing: AnalogClock is deprecated and not used anywhere so difficult to test. DateTimeView is used in the status bar. Behavior was verified with current date/time and also Europe/London around 2019-03-31 01:00 (skip forward) and around 2019-10-27 02:00 (fall back). The time picker in settings uses android.icu.util.Calendar which favored the later time if there are two local times with the same display time (e.g. the fall back case). The "repeat" case was tested with "date @1572137900" to set the clock to "the first" 01:58, then waiting 2 minutes to ensure that the 01:59 -> 01:00 transition occurs correctly. Bug: 16550209 Test: build / boot / treehugger Test: See above Change-Id: Ibadad3041a2e54fe12d347960bf1e0f3ebe10c01 --- core/java/android/widget/AnalogClock.java | 38 ++++--- core/java/android/widget/DateTimeView.java | 121 +++++++++++---------- 2 files changed, 90 insertions(+), 69 deletions(-) diff --git a/core/java/android/widget/AnalogClock.java b/core/java/android/widget/AnalogClock.java index 795b03493efb9..8716471561c16 100644 --- a/core/java/android/widget/AnalogClock.java +++ b/core/java/android/widget/AnalogClock.java @@ -26,12 +26,14 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.text.format.DateUtils; -import android.text.format.Time; import android.util.AttributeSet; import android.view.View; import android.widget.RemoteViews.RemoteView; -import java.util.TimeZone; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; /** * This widget display an analogic clock with two hands for hours and @@ -45,7 +47,7 @@ import java.util.TimeZone; @RemoteView @Deprecated public class AnalogClock extends View { - private Time mCalendar; + private Clock mClock; @UnsupportedAppUsage private Drawable mHourHand; @@ -97,7 +99,7 @@ public class AnalogClock extends View { mMinuteHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_minute); } - mCalendar = new Time(); + mClock = Clock.systemDefaultZone(); mDialWidth = mDial.getIntrinsicWidth(); mDialHeight = mDial.getIntrinsicHeight(); @@ -130,7 +132,7 @@ public class AnalogClock extends View { // in the main thread, therefore the receiver can't run before this method returns. // The time zone may have changed while the receiver wasn't registered, so update the Time - mCalendar = new Time(); + mClock = Clock.systemDefaultZone(); // Make sure we update to the current time onTimeChanged(); @@ -239,17 +241,18 @@ public class AnalogClock extends View { } private void onTimeChanged() { - mCalendar.setToNow(); + long nowMillis = mClock.millis(); + LocalDateTime localDateTime = toLocalDateTime(nowMillis, mClock.getZone()); - int hour = mCalendar.hour; - int minute = mCalendar.minute; - int second = mCalendar.second; + int hour = localDateTime.getHour(); + int minute = localDateTime.getMinute(); + int second = localDateTime.getSecond(); mMinutes = minute + second / 60.0f; mHour = hour + mMinutes / 60.0f; mChanged = true; - updateContentDescription(mCalendar); + updateContentDescription(nowMillis); } private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @@ -257,7 +260,7 @@ public class AnalogClock extends View { public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) { String tz = intent.getStringExtra("time-zone"); - mCalendar = new Time(TimeZone.getTimeZone(tz).getID()); + mClock = Clock.system(ZoneId.of(tz)); } onTimeChanged(); @@ -266,10 +269,17 @@ public class AnalogClock extends View { } }; - private void updateContentDescription(Time time) { + private void updateContentDescription(long timeMillis) { final int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_24HOUR; - String contentDescription = DateUtils.formatDateTime(mContext, - time.toMillis(false), flags); + String contentDescription = DateUtils.formatDateTime(mContext, timeMillis, flags); setContentDescription(contentDescription); } + + private static LocalDateTime toLocalDateTime(long timeMillis, ZoneId zoneId) { + // java.time types like LocalDateTime / Instant can support the full range of "long millis" + // with room to spare so we do not need to worry about overflow / underflow and the + // resulting exceptions while the input to this class is a long. + Instant instant = Instant.ofEpochMilli(timeMillis); + return LocalDateTime.ofInstant(instant, zoneId); + } } diff --git a/core/java/android/widget/DateTimeView.java b/core/java/android/widget/DateTimeView.java index bf2762a6d7278..fedd1d2b536d7 100644 --- a/core/java/android/widget/DateTimeView.java +++ b/core/java/android/widget/DateTimeView.java @@ -20,7 +20,6 @@ import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static android.text.format.DateUtils.YEAR_IN_MILLIS; -import static android.text.format.Time.getJulianDay; import android.annotation.UnsupportedAppUsage; import android.app.ActivityThread; @@ -32,7 +31,6 @@ import android.content.res.Configuration; import android.content.res.TypedArray; import android.database.ContentObserver; import android.os.Handler; -import android.text.format.Time; import android.util.AttributeSet; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; @@ -40,10 +38,14 @@ import android.widget.RemoteViews.RemoteView; import com.android.internal.R; import java.text.DateFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.JulianFields; import java.util.ArrayList; -import java.util.Calendar; import java.util.Date; -import java.util.TimeZone; // // TODO @@ -62,8 +64,9 @@ public class DateTimeView extends TextView { private static final int SHOW_TIME = 0; private static final int SHOW_MONTH_DAY_YEAR = 1; - Date mTime; - long mTimeMillis; + private long mTimeMillis; + // The LocalDateTime equivalent of mTimeMillis but truncated to minute, i.e. no seconds / nanos. + private LocalDateTime mLocalTime; int mLastDisplay = -1; DateFormat mLastFormat; @@ -127,11 +130,10 @@ public class DateTimeView extends TextView { @android.view.RemotableViewMethod @UnsupportedAppUsage - public void setTime(long time) { - Time t = new Time(); - t.set(time); - mTimeMillis = t.toMillis(false); - mTime = new Date(t.year-1900, t.month, t.monthDay, t.hour, t.minute, 0); + public void setTime(long timeMillis) { + mTimeMillis = timeMillis; + LocalDateTime dateTime = toLocalDateTime(timeMillis, ZoneId.systemDefault()); + mLocalTime = dateTime.withSecond(0); update(); } @@ -154,7 +156,7 @@ public class DateTimeView extends TextView { @UnsupportedAppUsage void update() { - if (mTime == null || getVisibility() == GONE) { + if (mLocalTime == null || getVisibility() == GONE) { return; } if (mShowRelativeTime) { @@ -163,31 +165,27 @@ public class DateTimeView extends TextView { } int display; - Date time = mTime; + ZoneId zoneId = ZoneId.systemDefault(); - Time t = new Time(); - t.set(mTimeMillis); - t.second = 0; + // localTime is the local time for mTimeMillis but at zero seconds past the minute. + LocalDateTime localTime = mLocalTime; + LocalDateTime localStartOfDay = + LocalDateTime.of(localTime.toLocalDate(), LocalTime.MIDNIGHT); + LocalDateTime localTomorrowStartOfDay = localStartOfDay.plusDays(1); + // now is current local time but at zero seconds past the minute. + LocalDateTime localNow = LocalDateTime.now(zoneId).withSecond(0); - t.hour -= 12; - long twelveHoursBefore = t.toMillis(false); - t.hour += 12; - long twelveHoursAfter = t.toMillis(false); - t.hour = 0; - t.minute = 0; - long midnightBefore = t.toMillis(false); - t.monthDay++; - long midnightAfter = t.toMillis(false); - - long nowMillis = System.currentTimeMillis(); - t.set(nowMillis); - t.second = 0; - nowMillis = t.normalize(false); + long twelveHoursBefore = toEpochMillis(localTime.minusHours(12), zoneId); + long twelveHoursAfter = toEpochMillis(localTime.plusHours(12), zoneId); + long midnightBefore = toEpochMillis(localStartOfDay, zoneId); + long midnightAfter = toEpochMillis(localTomorrowStartOfDay, zoneId); + long time = toEpochMillis(localTime, zoneId); + long now = toEpochMillis(localNow, zoneId); // Choose the display mode choose_display: { - if ((nowMillis >= midnightBefore && nowMillis < midnightAfter) - || (nowMillis >= twelveHoursBefore && nowMillis < twelveHoursAfter)) { + if ((now >= midnightBefore && now < midnightAfter) + || (now >= twelveHoursBefore && now < twelveHoursAfter)) { display = SHOW_TIME; break choose_display; } @@ -216,7 +214,7 @@ public class DateTimeView extends TextView { } // Set the text - String text = format.format(mTime); + String text = format.format(new Date(time)); setText(text); // Schedule the next update @@ -225,7 +223,7 @@ public class DateTimeView extends TextView { mUpdateTimeMillis = twelveHoursAfter > midnightAfter ? twelveHoursAfter : midnightAfter; } else { // Currently showing the date - if (mTimeMillis < nowMillis) { + if (mTimeMillis < now) { // If the time is in the past, don't schedule an update mUpdateTimeMillis = 0; } else { @@ -266,15 +264,18 @@ public class DateTimeView extends TextView { millisIncrease = HOUR_IN_MILLIS; } else if (duration < YEAR_IN_MILLIS) { // In weird cases it can become 0 because of daylight savings - TimeZone timeZone = TimeZone.getDefault(); - count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1); + LocalDateTime localDateTime = mLocalTime; + ZoneId zoneId = ZoneId.systemDefault(); + LocalDateTime localNow = toLocalDateTime(now, zoneId); + + count = Math.max(Math.abs(dayDistance(localDateTime, localNow)), 1); result = String.format(getContext().getResources().getQuantityString(past ? com.android.internal.R.plurals.duration_days_shortest : com.android.internal.R.plurals.duration_days_shortest_future, count), count); if (past || count != 1) { - mUpdateTimeMillis = computeNextMidnight(timeZone); + mUpdateTimeMillis = computeNextMidnight(localNow, zoneId); millisIncrease = -1; } else { millisIncrease = DAY_IN_MILLIS; @@ -300,18 +301,13 @@ public class DateTimeView extends TextView { } /** - * @param timeZone the timezone we are in - * @return the timepoint in millis at UTC at midnight in the current timezone + * Returns the epoch millis for the next midnight in the specified timezone. */ - private long computeNextMidnight(TimeZone timeZone) { - Calendar c = Calendar.getInstance(); - c.setTimeZone(timeZone); - c.add(Calendar.DAY_OF_MONTH, 1); - c.set(Calendar.HOUR_OF_DAY, 0); - c.set(Calendar.MINUTE, 0); - c.set(Calendar.SECOND, 0); - c.set(Calendar.MILLISECOND, 0); - return c.getTimeInMillis(); + private static long computeNextMidnight(LocalDateTime time, ZoneId zoneId) { + // This ignores the chance of overflow: it should never happen. + LocalDate tomorrow = time.toLocalDate().plusDays(1); + LocalDateTime nextMidnight = LocalDateTime.of(tomorrow, LocalTime.MIDNIGHT); + return toEpochMillis(nextMidnight, zoneId); } @Override @@ -329,11 +325,10 @@ public class DateTimeView extends TextView { com.android.internal.R.string.now_string_shortest); } - // Return the date difference for the two times in a given timezone. - private static int dayDistance(TimeZone timeZone, long startTime, - long endTime) { - return getJulianDay(endTime, timeZone.getOffset(endTime) / 1000) - - getJulianDay(startTime, timeZone.getOffset(startTime) / 1000); + // Return the number of days between the two dates. + private static int dayDistance(LocalDateTime start, LocalDateTime end) { + return (int) (end.getLong(JulianFields.JULIAN_DAY) + - start.getLong(JulianFields.JULIAN_DAY)); } private DateFormat getTimeFormat() { @@ -378,8 +373,11 @@ public class DateTimeView extends TextView { count); } else if (duration < YEAR_IN_MILLIS) { // In weird cases it can become 0 because of daylight savings - TimeZone timeZone = TimeZone.getDefault(); - count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1); + LocalDateTime localDateTime = mLocalTime; + ZoneId zoneId = ZoneId.systemDefault(); + LocalDateTime localNow = toLocalDateTime(now, zoneId); + + count = Math.max(Math.abs(dayDistance(localDateTime, localNow)), 1); result = String.format(getContext().getResources().getQuantityString(past ? com.android.internal. R.plurals.duration_days_relative @@ -515,4 +513,17 @@ public class DateTimeView extends TextView { } } } + + private static LocalDateTime toLocalDateTime(long timeMillis, ZoneId zoneId) { + // java.time types like LocalDateTime / Instant can support the full range of "long millis" + // with room to spare so we do not need to worry about overflow / underflow and the rsulting + // exceptions while the input to this class is a long. + Instant instant = Instant.ofEpochMilli(timeMillis); + return LocalDateTime.ofInstant(instant, zoneId); + } + + private static long toEpochMillis(LocalDateTime time, ZoneId zoneId) { + Instant instant = time.toInstant(zoneId.getRules().getOffset(time)); + return instant.toEpochMilli(); + } }