Merge "Convert some users of Time.format() to an alt."
diff --git a/core/java/android/provider/CalendarContract.java b/core/java/android/provider/CalendarContract.java
index f1d90be..7285166 100644
--- a/core/java/android/provider/CalendarContract.java
+++ b/core/java/android/provider/CalendarContract.java
@@ -40,7 +40,7 @@
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 @@
* <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>
@@ -2609,9 +2609,7 @@
@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 5a14092..f7fd89d 100644
--- a/core/java/android/text/format/TimeFormatter.java
+++ b/core/java/android/text/format/TimeFormatter.java
@@ -26,6 +26,9 @@
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 @@
}
/**
+ * 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 @@
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 @@
}
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 0000000..17bac8d
--- /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 0000000..b605520
--- /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 9dead16..aeb3e7f 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.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 @@
}
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 fa8c48b..a89daff 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.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;
@@ -1987,9 +1987,7 @@
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) -> {
@@ -2555,9 +2553,7 @@
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);
@@ -2622,9 +2618,7 @@
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 eb5d472..e655b35 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -29,7 +29,7 @@
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;
@@ -1643,17 +1643,13 @@
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 2d8a2ac..ebba128 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.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 @@
}
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) {