Loading core/java/android/provider/CalendarContract.java +3 −5 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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> Loading Loading @@ -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); } Loading core/java/android/text/format/TimeFormatter.java +63 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -85,6 +88,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. */ Loading @@ -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; Loading @@ -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); Loading core/java/android/text/format/TimeMigrationUtils.java 0 → 100644 +40 −0 Original line number Diff line number Diff line /* * 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); } } core/tests/coretests/src/android/text/format/TimeMigrationUtilsTest.java 0 → 100644 +120 −0 Original line number Diff line number Diff line /* * 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); } } } } } services/core/java/com/android/server/DropBoxManagerService.java +2 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading Loading
core/java/android/provider/CalendarContract.java +3 −5 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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> Loading Loading @@ -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); } Loading
core/java/android/text/format/TimeFormatter.java +63 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -85,6 +88,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. */ Loading @@ -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; Loading @@ -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); Loading
core/java/android/text/format/TimeMigrationUtils.java 0 → 100644 +40 −0 Original line number Diff line number Diff line /* * 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); } }
core/tests/coretests/src/android/text/format/TimeMigrationUtilsTest.java 0 → 100644 +120 −0 Original line number Diff line number Diff line /* * 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); } } } } }
services/core/java/com/android/server/DropBoxManagerService.java +2 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading