diff options
author | Neil Fuller <nfuller@google.com> | 2019-07-23 07:49:41 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2019-07-23 07:49:41 +0000 |
commit | cac13d44ab9ced55b6bb13d05993634d9d322487 (patch) | |
tree | b174b2c17a693c03ccf673d5032fba0d2c7f7305 | |
parent | 60bb23b14f7f7b0d58dc50c3072b9d4418fd14ae (diff) | |
parent | 9aa7b74a7a64c37c1cc7bc42dc643c8916ac94fb (diff) |
Merge "Convert some users of Time.format() to an alt."
8 files changed, 235 insertions, 34 deletions
diff --git a/core/java/android/provider/CalendarContract.java b/core/java/android/provider/CalendarContract.java index 2f68eb475e7a..d862d6022154 100644 --- a/core/java/android/provider/CalendarContract.java +++ b/core/java/android/provider/CalendarContract.java @@ -40,7 +40,7 @@ import android.database.DatabaseUtils; import android.net.Uri; import android.os.RemoteException; import android.text.format.DateUtils; -import android.text.format.Time; +import android.text.format.TimeMigrationUtils; import android.util.Log; import com.android.internal.util.Preconditions; @@ -1680,7 +1680,7 @@ public final class CalendarContract { * <h3>Writing to Events</h3> There are further restrictions on all Updates * and Inserts in the Events table: * <ul> - * <li>If allDay is set to 1 eventTimezone must be {@link Time#TIMEZONE_UTC} + * <li>If allDay is set to 1 eventTimezone must be "UTC" * and the time must correspond to a midnight boundary.</li> * <li>Exceptions are not allowed to recur. If rrule or rdate is not empty, * original_id and original_sync_id must be empty.</li> @@ -2608,9 +2608,7 @@ public final class CalendarContract { @UnsupportedAppUsage public static void scheduleAlarm(Context context, AlarmManager manager, long alarmTime) { if (DEBUG) { - Time time = new Time(); - time.set(alarmTime); - String schedTime = time.format(" %a, %b %d, %Y %I:%M%P"); + String schedTime = TimeMigrationUtils.formatMillisWithFixedFormat(alarmTime); Log.d(TAG, "Schedule alarm at " + alarmTime + " " + schedTime); } diff --git a/core/java/android/text/format/TimeFormatter.java b/core/java/android/text/format/TimeFormatter.java index 5a14092c95a8..f7fd89d7d819 100644 --- a/core/java/android/text/format/TimeFormatter.java +++ b/core/java/android/text/format/TimeFormatter.java @@ -26,6 +26,9 @@ import libcore.icu.LocaleData; import libcore.util.ZoneInfo; import java.nio.CharBuffer; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Formatter; import java.util.Locale; import java.util.TimeZone; @@ -86,6 +89,59 @@ class TimeFormatter { } /** + * The implementation of {@link TimeMigrationUtils#formatMillisWithFixedFormat(long)} for + * 2038-safe formatting with the pattern "%Y-%m-%d %H:%M:%S" and including the historic + * incorrect digit localization behavior. + */ + String formatMillisWithFixedFormat(long timeMillis) { + // This method is deliberately not a general purpose replacement for + // format(String, ZoneInfo.WallTime, ZoneInfo): It hard-codes the pattern used; many of the + // pattern characters supported by Time.format() have unusual behavior which would make + // using java.time.format or similar packages difficult. It would be a lot of work to share + // behavior and many internal Android usecases can be covered by this common pattern + // behavior. + + // No need to worry about overflow / underflow: long millis is representable by Instant and + // LocalDateTime with room to spare. + Instant instant = Instant.ofEpochMilli(timeMillis); + + // Date/times are calculated in the current system default time zone. + LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + + // You'd think it would be as simple as: + // DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", locale); + // return formatter.format(localDateTime); + // but we retain Time's behavior around digits. + + StringBuilder stringBuilder = new StringBuilder(19); + + // This effectively uses the US locale because number localization is handled separately + // (see below). + stringBuilder.append(localDateTime.getYear()); + stringBuilder.append('-'); + append2DigitNumber(stringBuilder, localDateTime.getMonthValue()); + stringBuilder.append('-'); + append2DigitNumber(stringBuilder, localDateTime.getDayOfMonth()); + stringBuilder.append(' '); + append2DigitNumber(stringBuilder, localDateTime.getHour()); + stringBuilder.append(':'); + append2DigitNumber(stringBuilder, localDateTime.getMinute()); + stringBuilder.append(':'); + append2DigitNumber(stringBuilder, localDateTime.getSecond()); + + String result = stringBuilder.toString(); + return localizeDigits(result); + } + + /** Zero-pads value as needed to achieve a 2-digit number. */ + private static void append2DigitNumber(StringBuilder builder, int value) { + if (value < 10) { + builder.append('0'); + } + builder.append(value); + } + + /** * Format the specified {@code wallTime} using {@code pattern}. The output is returned. */ public String format(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) { @@ -99,12 +155,9 @@ class TimeFormatter { formatInternal(pattern, wallTime, zoneInfo); String result = stringBuilder.toString(); - // This behavior is the source of a bug since some formats are defined as being - // in ASCII and not localized. - if (localeData.zeroDigit != '0') { - result = localizeDigits(result); - } - return result; + // The localizeDigits() behavior is the source of a bug since some formats are defined + // as being in ASCII and not localized. + return localizeDigits(result); } finally { outputBuilder = null; numberFormatter = null; @@ -112,6 +165,10 @@ class TimeFormatter { } private String localizeDigits(String s) { + if (localeData.zeroDigit == '0') { + return s; + } + int length = s.length(); int offsetToLocalizedDigits = localeData.zeroDigit - '0'; StringBuilder result = new StringBuilder(length); diff --git a/core/java/android/text/format/TimeMigrationUtils.java b/core/java/android/text/format/TimeMigrationUtils.java new file mode 100644 index 000000000000..17bac8d67b26 --- /dev/null +++ b/core/java/android/text/format/TimeMigrationUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.text.format; + +/** + * Logic to ease migration away from {@link Time} in Android internal code. {@link Time} is + * afflicted by the Y2038 issue and deprecated. The methods here are intended to allow minimal + * changes to classes that use {@link Time} for common behavior. + * + * @hide + */ +public class TimeMigrationUtils { + + private TimeMigrationUtils() {} + + /** + * A Y2038-safe replacement for various users of the {@link Time#format(String)} with the + * pattern "%Y-%m-%d %H:%M:%S". Note, this method retains the unusual localization behavior + * originally implemented by Time, which can lead to non-latin numbers being produced if the + * default locale does not use latin numbers. + */ + public static String formatMillisWithFixedFormat(long timeMillis) { + // Delegate to TimeFormatter so that the unusual localization / threading behavior can be + // reused. + return new TimeFormatter().formatMillisWithFixedFormat(timeMillis); + } +} diff --git a/core/tests/coretests/src/android/text/format/TimeMigrationUtilsTest.java b/core/tests/coretests/src/android/text/format/TimeMigrationUtilsTest.java new file mode 100644 index 000000000000..b605520c659d --- /dev/null +++ b/core/tests/coretests/src/android/text/format/TimeMigrationUtilsTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text.format; + +import static org.junit.Assert.assertEquals; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; +import java.util.TimeZone; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TimeMigrationUtilsTest { + + private static final int ONE_DAY_IN_SECONDS = 24 * 60 * 60; + + private Locale mDefaultLocale; + private TimeZone mDefaultTimeZone; + + @Before + public void setUp() { + mDefaultLocale = Locale.getDefault(); + mDefaultTimeZone = TimeZone.getDefault(); + } + + @After + public void tearDown() { + Locale.setDefault(mDefaultLocale); + TimeZone.setDefault(mDefaultTimeZone); + } + + @Test + public void formatMillisWithFixedFormat_fixes2038Issue() { + Locale.setDefault(Locale.UK); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + + // The following cannot be represented properly using Time because they are outside of the + // supported range. + long y2038Issue1 = (((long) Integer.MIN_VALUE) - ONE_DAY_IN_SECONDS) * 1000L; + assertEquals( + "1901-12-12 20:45:52", TimeMigrationUtils.formatMillisWithFixedFormat(y2038Issue1)); + long y2038Issue2 = (((long) Integer.MAX_VALUE) + ONE_DAY_IN_SECONDS) * 1000L; + assertEquals( + "2038-01-20 03:14:07", TimeMigrationUtils.formatMillisWithFixedFormat(y2038Issue2)); + } + + /** + * Compares TimeMigrationUtils.formatSimpleDateTime() with the code it is replacing. + */ + @Test + public void formatMillisAsDateTime_matchesOldBehavior() { + // A selection of interesting locales. + Locale[] locales = new Locale[] { + Locale.US, + Locale.UK, + Locale.FRANCE, + Locale.JAPAN, + Locale.CHINA, + // Android supports RTL locales like arabic and arabic with latin numbers. + Locale.forLanguageTag("ar-AE"), + Locale.forLanguageTag("ar-AE-u-nu-latn"), + }; + // A selection of interesting time zones. + String[] timeZoneIds = new String[] { + "UTC", "Europe/London", "America/New_York", "America/Los_Angeles", "Asia/Shanghai", + }; + // Some arbitrary times when the two formatters should agree. + long[] timesMillis = new long[] { + System.currentTimeMillis(), + 0, + // The Time class only works in 32-bit range, the replacement works beyond that. To + // avoid messing around with offsets and complicating the test, below there are a + // day after / before the known limits. + (Integer.MIN_VALUE + ONE_DAY_IN_SECONDS) * 1000L, + (Integer.MAX_VALUE - ONE_DAY_IN_SECONDS) * 1000L, + }; + + for (Locale locale : locales) { + Locale.setDefault(locale); + for (String timeZoneId : timeZoneIds) { + TimeZone timeZone = TimeZone.getTimeZone(timeZoneId); + TimeZone.setDefault(timeZone); + for (long timeMillis : timesMillis) { + Time time = new Time(); + time.set(timeMillis); + String oldResult = time.format("%Y-%m-%d %H:%M:%S"); + String newResult = TimeMigrationUtils.formatMillisWithFixedFormat(timeMillis); + assertEquals( + "locale=" + locale + ", timeZoneId=" + timeZoneId + + ", timeMillis=" + timeMillis, + oldResult, newResult); + } + } + } + } +} diff --git a/services/core/java/com/android/server/DropBoxManagerService.java b/services/core/java/com/android/server/DropBoxManagerService.java index 9dead161390f..aeb3e7fd94de 100644 --- a/services/core/java/com/android/server/DropBoxManagerService.java +++ b/services/core/java/com/android/server/DropBoxManagerService.java @@ -41,7 +41,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; -import android.text.format.Time; +import android.text.format.TimeMigrationUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -582,11 +582,9 @@ public final class DropBoxManagerService extends SystemService { } int numFound = 0, numArgs = searchArgs.size(); - Time time = new Time(); out.append("\n"); for (EntryFile entry : mAllFiles.contents) { - time.set(entry.timestampMillis); - String date = time.format("%Y-%m-%d %H:%M:%S"); + String date = TimeMigrationUtils.formatMillisWithFixedFormat(entry.timestampMillis); boolean match = true; for (int i = 0; i < numArgs && match; i++) { String arg = searchArgs.get(i); diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java index 64a05c94e39e..f6c49ed4d29e 100644 --- a/services/core/java/com/android/server/content/SyncManager.java +++ b/services/core/java/com/android/server/content/SyncManager.java @@ -86,7 +86,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.os.WorkSource; import android.provider.Settings; -import android.text.format.Time; +import android.text.format.TimeMigrationUtils; import android.util.EventLog; import android.util.Log; import android.util.Pair; @@ -1993,9 +1993,7 @@ public class SyncManager { if (time == 0) { return "N/A"; } - Time tobj = new Time(); - tobj.set(time); - return tobj.format("%Y-%m-%d %H:%M:%S"); + return TimeMigrationUtils.formatMillisWithFixedFormat(time); } private final static Comparator<SyncOperation> sOpDumpComparator = (op1, op2) -> { @@ -2561,9 +2559,7 @@ public class SyncManager { accountKey = "Unknown"; } final long elapsedTime = item.elapsedTime; - final Time time = new Time(); final long eventTime = item.eventTime; - time.set(eventTime); final String key = authorityName + "/" + accountKey; final Long lastEventTime = lastTimeMap.get(key); @@ -2628,9 +2624,7 @@ public class SyncManager { authorityName = "Unknown"; accountKey = "Unknown"; } - final Time time = new Time(); final long eventTime = item.eventTime; - time.set(eventTime); pw.printf(" #%-3d: %s %8s ", i + 1, diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java index d73c25336808..6f2b33453891 100644 --- a/services/core/java/com/android/server/job/controllers/JobStatus.java +++ b/services/core/java/com/android/server/job/controllers/JobStatus.java @@ -28,7 +28,7 @@ import android.net.Network; import android.net.Uri; import android.os.RemoteException; import android.os.UserHandle; -import android.text.format.Time; +import android.text.format.TimeMigrationUtils; import android.util.ArraySet; import android.util.Pair; import android.util.Slog; @@ -1660,17 +1660,13 @@ public final class JobStatus { if (numFailures != 0) { pw.print(prefix); pw.print("Num failures: "); pw.println(numFailures); } - final Time t = new Time(); - final String format = "%Y-%m-%d %H:%M:%S"; if (mLastSuccessfulRunTime != 0) { pw.print(prefix); pw.print("Last successful run: "); - t.set(mLastSuccessfulRunTime); - pw.println(t.format(format)); + pw.println(TimeMigrationUtils.formatMillisWithFixedFormat(mLastSuccessfulRunTime)); } if (mLastFailedRunTime != 0) { pw.print(prefix); pw.print("Last failed run: "); - t.set(mLastFailedRunTime); - pw.println(t.format(format)); + pw.println(TimeMigrationUtils.formatMillisWithFixedFormat(mLastFailedRunTime)); } } diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 2d8a2acd575f..ebba128b5f28 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -79,7 +79,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManagerInternal; import android.text.TextUtils; -import android.text.format.Time; +import android.text.format.TimeMigrationUtils; import android.util.ArraySet; import android.util.AtomicFile; import android.util.KeyValueListParser; @@ -3981,9 +3981,7 @@ public class ShortcutService extends IShortcutService.Stub { } static String formatTime(long time) { - Time tobj = new Time(); - tobj.set(time); - return tobj.format("%Y-%m-%d %H:%M:%S"); + return TimeMigrationUtils.formatMillisWithFixedFormat(time); } private void dumpCurrentTime(PrintWriter pw) { |