Merge "Remove out-of-date comments about ICU4C."
diff --git a/JavaLibrary.mk b/JavaLibrary.mk
index 758b6ce..97e94f8 100644
--- a/JavaLibrary.mk
+++ b/JavaLibrary.mk
@@ -91,7 +91,7 @@
LOCAL_MODULE_TAGS := optional
LOCAL_JAVA_LANGUAGE_VERSION := 1.8
LOCAL_MODULE := core-all
-LOCAL_REQUIRED_MODULES := tzdata
+LOCAL_REQUIRED_MODULES := tzdata tzlookup.xml
LOCAL_CORE_LIBRARY := true
LOCAL_UNINSTALLABLE_MODULE := true
include $(BUILD_JAVA_LIBRARY)
@@ -110,7 +110,7 @@
LOCAL_MODULE := core-oj
LOCAL_JAVA_LIBRARIES := core-all
LOCAL_NOTICE_FILE := $(LOCAL_PATH)/ojluni/NOTICE
-LOCAL_REQUIRED_MODULES := tzdata
+LOCAL_REQUIRED_MODULES := tzdata tzlookup.xml
LOCAL_CORE_LIBRARY := true
include $(BUILD_JAVA_LIBRARY)
@@ -132,7 +132,7 @@
endif # EMMA_INSTRUMENT_STATIC
endif # EMMA_INSTRUMENT
LOCAL_CORE_LIBRARY := true
-LOCAL_REQUIRED_MODULES := tzdata
+LOCAL_REQUIRED_MODULES := tzdata tzlookup.xml
include $(BUILD_JAVA_LIBRARY)
# A library that exists to satisfy javac when
@@ -167,7 +167,7 @@
LOCAL_MODULE := core-oj-testdex
LOCAL_JAVA_LIBRARIES := core-all
LOCAL_NOTICE_FILE := $(LOCAL_PATH)/ojluni/NOTICE
-LOCAL_REQUIRED_MODULES := tzdata
+LOCAL_REQUIRED_MODULES := tzdata tzlookup.xml
LOCAL_CORE_LIBRARY := true
include $(BUILD_JAVA_LIBRARY)
@@ -201,7 +201,7 @@
LOCAL_MODULE := core-libart-testdex
LOCAL_JAVA_LIBRARIES := core-all
LOCAL_CORE_LIBRARY := true
-LOCAL_REQUIRED_MODULES := tzdata
+LOCAL_REQUIRED_MODULES := tzdata tzlookup.xml
include $(BUILD_JAVA_LIBRARY)
endif
@@ -330,7 +330,7 @@
LOCAL_MODULE_TAGS := optional
LOCAL_JAVA_LANGUAGE_VERSION := 1.8
LOCAL_MODULE := core-all-hostdex
-LOCAL_REQUIRED_MODULES := tzdata-host
+LOCAL_REQUIRED_MODULES := tzdata-host tzlookup.xml-host
LOCAL_CORE_LIBRARY := true
LOCAL_UNINSTALLABLE_MODULE := true
include $(BUILD_HOST_DALVIK_JAVA_LIBRARY)
@@ -346,7 +346,7 @@
LOCAL_MODULE := core-oj-hostdex
LOCAL_NOTICE_FILE := $(LOCAL_PATH)/ojluni/NOTICE
LOCAL_JAVA_LIBRARIES := core-all-hostdex
-LOCAL_REQUIRED_MODULES := tzdata-host
+LOCAL_REQUIRED_MODULES := tzdata-host tzlookup.xml-host
LOCAL_CORE_LIBRARY := true
include $(BUILD_HOST_DALVIK_JAVA_LIBRARY)
@@ -361,7 +361,7 @@
LOCAL_JAVA_LANGUAGE_VERSION := 1.8
LOCAL_MODULE := core-libart-hostdex
LOCAL_JAVA_LIBRARIES := core-oj-hostdex
-LOCAL_REQUIRED_MODULES := tzdata-host
+LOCAL_REQUIRED_MODULES := tzdata-host tzlookup.xml-host
include $(BUILD_HOST_DALVIK_JAVA_LIBRARY)
# A library that exists to satisfy javac when
diff --git a/luni/src/main/java/libcore/util/TimeZoneDataFiles.java b/luni/src/main/java/libcore/util/TimeZoneDataFiles.java
index 8361339..c792665 100644
--- a/luni/src/main/java/libcore/util/TimeZoneDataFiles.java
+++ b/luni/src/main/java/libcore/util/TimeZoneDataFiles.java
@@ -26,6 +26,14 @@
private TimeZoneDataFiles() {}
+ /**
+ * Returns two time zone file paths for the specified file name in an array in the order they
+ * should be tried. See {@link #generateIcuDataPath()} for ICU files instead.
+ * <ul>
+ * <li>[0] - the location of the file in the /data partition (may not exist).</li>
+ * <li>[1] - the location of the file in the /system partition (should exist).</li>
+ * </ul>
+ */
// VisibleForTesting
public static String[] getTimeZoneFilePaths(String fileName) {
return new String[] {
diff --git a/luni/src/main/java/libcore/util/TimeZoneFinder.java b/luni/src/main/java/libcore/util/TimeZoneFinder.java
new file mode 100644
index 0000000..4e47df4
--- /dev/null
+++ b/luni/src/main/java/libcore/util/TimeZoneFinder.java
@@ -0,0 +1,572 @@
+/*
+ * Copyright (C) 2017 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 libcore.util;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import android.icu.util.TimeZone;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A structure that can find matching time zones.
+ */
+public class TimeZoneFinder {
+
+ private static final String TZLOOKUP_FILE_NAME = "tzlookup.xml";
+ private static final String TIMEZONES_ELEMENT = "timezones";
+ private static final String COUNTRY_ZONES_ELEMENT = "countryzones";
+ private static final String COUNTRY_ELEMENT = "country";
+ private static final String COUNTRY_CODE_ATTRIBUTE = "code";
+ private static final String ID_ELEMENT = "id";
+
+ private static TimeZoneFinder instance;
+
+ private final ReaderSupplier xmlSource;
+
+ // Cached fields for the last country looked up.
+ private String lastCountryIso;
+ private List<TimeZone> lastCountryTimeZones;
+
+ private TimeZoneFinder(ReaderSupplier xmlSource) {
+ this.xmlSource = xmlSource;
+ }
+
+ /**
+ * Obtains an instance for use when resolving time zones. This method handles using the correct
+ * file when there are several to choose from. This method never returns {@code null}. No
+ * in-depth validation is performed on the file content, see {@link #validate()}.
+ */
+ public static TimeZoneFinder getInstance() {
+ synchronized(TimeZoneFinder.class) {
+ if (instance == null) {
+ String[] tzLookupFilePaths =
+ TimeZoneDataFiles.getTimeZoneFilePaths(TZLOOKUP_FILE_NAME);
+ instance = createInstanceWithFallback(tzLookupFilePaths[0], tzLookupFilePaths[1]);
+ }
+ }
+ return instance;
+ }
+
+ // VisibleForTesting
+ public static TimeZoneFinder createInstanceWithFallback(String... tzLookupFilePaths) {
+ for (String tzLookupFilePath : tzLookupFilePaths) {
+ try {
+ // We assume that any file in /data was validated before install, and the system
+ // file was validated before the device shipped. Therefore, we do not pay the
+ // validation cost here.
+ return createInstance(tzLookupFilePath);
+ } catch (IOException e) {
+ System.logE("Unable to process file: " + tzLookupFilePath + " Trying next one.", e);
+ }
+ }
+
+ System.logE("No valid file found in set: " + Arrays.toString(tzLookupFilePaths)
+ + " Falling back to empty map.");
+ return createInstanceForTests("<timezones><countryzones /></timezones>");
+ }
+
+ /**
+ * Obtains an instance using a specific data file, throwing an IOException if the file does not
+ * exist or is not readable. This method never returns {@code null}. No in-depth validation is
+ * performed on the file content, see {@link #validate()}.
+ */
+ public static TimeZoneFinder createInstance(String path) throws IOException {
+ ReaderSupplier xmlSupplier = ReaderSupplier.forFile(path, StandardCharsets.UTF_8);
+ return new TimeZoneFinder(xmlSupplier);
+ }
+
+ /** Used to create an instance using an in-memory XML String instead of a file. */
+ // VisibleForTesting
+ public static TimeZoneFinder createInstanceForTests(String xml) {
+ return new TimeZoneFinder(ReaderSupplier.forString(xml));
+ }
+
+ /**
+ * Parses the data file, throws an exception if it is invalid or cannot be read.
+ */
+ public void validate() throws IOException {
+ try {
+ processXml(new CountryZonesValidator());
+ } catch (XmlPullParserException e) {
+ throw new IOException("Parsing error", e);
+ }
+ }
+
+ /**
+ * Return a time zone that has / would have had the specified offset and DST value at the
+ * specified moment in the specified country.
+ *
+ * <p>In order to be considered a configured zone must match the supplied offset information.
+ *
+ * <p>Matches are considered in a well-defined order. If multiple zones match and one of them
+ * also matches the (optional) bias parameter then the bias time zone will be returned.
+ * Otherwise the first match found is returned.
+ */
+ public TimeZone lookupTimeZoneByCountryAndOffset(
+ String countryIso, int offsetSeconds, boolean isDst, long whenMillis, TimeZone bias) {
+
+ List<TimeZone> candidates = lookupTimeZonesByCountry(countryIso);
+ if (candidates == null || candidates.isEmpty()) {
+ return null;
+ }
+
+ TimeZone firstMatch = null;
+ for (int i = 0; i < candidates.size(); i++) {
+ TimeZone match = candidates.get(i);
+ if (!offsetMatchesAtTime(match, offsetSeconds, isDst, whenMillis)) {
+ continue;
+ }
+
+ if (firstMatch == null) {
+ if (bias == null) {
+ // No bias, so we can stop at the first match.
+ return match;
+ }
+ // We have to carry on checking in case the bias matches. We want to return the
+ // first if it doesn't, though.
+ firstMatch = match;
+ }
+
+ // Check if match is also the bias. There must be a bias otherwise we'd have terminated
+ // already.
+ if (match.getID().equals(bias.getID())) {
+ return match;
+ }
+ }
+ // Return firstMatch, which can be null if there was no match.
+ return firstMatch;
+ }
+
+ /**
+ * Returns {@code true} if the specified offset, DST state and time would be valid in the
+ * timeZone.
+ */
+ private static boolean offsetMatchesAtTime(TimeZone timeZone, int offsetMillis, boolean isDst,
+ long whenMillis) {
+ int[] offsets = new int[2];
+ timeZone.getOffset(whenMillis, false /* local */, offsets);
+
+ // offsets[1] == 0 when the zone is not in DST.
+ boolean zoneIsDst = offsets[1] != 0;
+ if (isDst != zoneIsDst) {
+ return false;
+ }
+ return offsetMillis == (offsets[0] + offsets[1]);
+ }
+
+ /**
+ * Returns a list of time zones known to be used in the specified country. If the country code
+ * is not recognized or there is an error during lookup this can return null. The TimeZones
+ * returned will never contain {@link TimeZone#UNKNOWN_ZONE}. This method can return an empty
+ * list in a case when the underlying configuration references only unknown zone IDs.
+ */
+ public List<TimeZone> lookupTimeZonesByCountry(String countryIso) {
+ synchronized(this) {
+ if (countryIso.equals(lastCountryIso)) {
+ return lastCountryTimeZones;
+ }
+ }
+
+ CountryZonesExtractor extractor = new CountryZonesExtractor(countryIso);
+ List<TimeZone> countryTimeZones = null;
+ try {
+ processXml(extractor);
+ countryTimeZones = extractor.getMatchedZones();
+ } catch (IOException e) {
+ System.logW("Error reading country zones ", e);
+
+ // Clear the cached code so we will try again next time.
+ countryIso = null;
+ } catch (XmlPullParserException e) {
+ System.logW("Error reading country zones ", e);
+ // We want to cache the null. This won't get better over time.
+ }
+
+ synchronized(this) {
+ lastCountryIso = countryIso;
+ lastCountryTimeZones = countryTimeZones;
+ }
+ return countryTimeZones;
+ }
+
+ /**
+ * Processes the XML, applying the {@link CountryZonesProcessor} to the <countryzones>
+ * element. Processing can terminate early if the
+ * {@link CountryZonesProcessor#process(String, List, String)} returns
+ * {@link CountryZonesProcessor#HALT} or it throws an exception.
+ */
+ private void processXml(CountryZonesProcessor processor)
+ throws XmlPullParserException, IOException {
+ try (Reader reader = xmlSource.get()) {
+ XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
+ xmlPullParserFactory.setNamespaceAware(false);
+
+ XmlPullParser parser = xmlPullParserFactory.newPullParser();
+ parser.setInput(reader);
+
+ /*
+ * The expected XML structure is:
+ * <timezones>
+ * <countryzones>
+ * <country code="us">
+ * <id>America/New_York"</id>
+ * ...
+ * <id>America/Los_Angeles</id>
+ * </country>
+ * <country code="gb">
+ * <id>Europe/London</id>
+ * </country>
+ * </countryzones>
+ * </timezones>
+ */
+
+ findRequiredStartTag(parser, TIMEZONES_ELEMENT);
+
+ // There is only one expected sub-element <countryzones> in the format currently, skip
+ // over anything before it.
+ findRequiredStartTag(parser, COUNTRY_ZONES_ELEMENT);
+
+ if (processCountryZones(parser, processor) == CountryZonesProcessor.HALT) {
+ return;
+ }
+
+ // Make sure we are on the </countryzones> tag.
+ checkOnEndTag(parser, COUNTRY_ZONES_ELEMENT);
+
+ // Advance to the next tag.
+ parser.next();
+
+ // Skip anything until </timezones>, and make sure the file is not truncated and we can
+ // find the end.
+ consumeUntilEndTag(parser, TIMEZONES_ELEMENT);
+
+ // Make sure we are on the </timezones> tag.
+ checkOnEndTag(parser, TIMEZONES_ELEMENT);
+ }
+ }
+
+ private static boolean processCountryZones(XmlPullParser parser,
+ CountryZonesProcessor processor) throws IOException, XmlPullParserException {
+
+ // Skip over any unexpected elements and process <country> elements.
+ while (findOptionalStartTag(parser, COUNTRY_ELEMENT)) {
+ if (processor == null) {
+ consumeUntilEndTag(parser, COUNTRY_ELEMENT);
+ } else {
+ String code = parser.getAttributeValue(
+ null /* namespace */, COUNTRY_CODE_ATTRIBUTE);
+ if (code == null || code.isEmpty()) {
+ throw new XmlPullParserException(
+ "Unable to find country code: " + parser.getPositionDescription());
+ }
+
+ String debugInfo = parser.getPositionDescription();
+ List<String> timeZoneIds = parseZoneIds(parser);
+ if (processor.process(code, timeZoneIds, debugInfo)
+ == CountryZonesProcessor.HALT) {
+ return CountryZonesProcessor.HALT;
+ }
+ }
+
+ // Make sure we are on the </country> element.
+ checkOnEndTag(parser, COUNTRY_ELEMENT);
+ }
+
+ return CountryZonesExtractor.CONTINUE;
+ }
+
+ private static List<String> parseZoneIds(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ List<String> timeZones = new ArrayList<>();
+
+ // Skip over any unexpected elements and process <id> elements.
+ while (findOptionalStartTag(parser, ID_ELEMENT)) {
+ String zoneIdString = consumeText(parser);
+
+ // Make sure we are on the </id> element.
+ checkOnEndTag(parser, ID_ELEMENT);
+
+ // Process the zone ID.
+ timeZones.add(zoneIdString);
+ }
+
+ // The list is made unmodifiable to avoid callers changing it.
+ return Collections.unmodifiableList(timeZones);
+ }
+
+ private static void findRequiredStartTag(XmlPullParser parser, String elementName)
+ throws IOException, XmlPullParserException {
+ findStartTag(parser, elementName, true /* elementRequired */);
+ }
+
+ /** Called when on a START_TAG. When returning false, it leaves the parser on the END_TAG. */
+ private static boolean findOptionalStartTag(XmlPullParser parser, String elementName)
+ throws IOException, XmlPullParserException {
+ return findStartTag(parser, elementName, false /* elementRequired */);
+ }
+
+ /**
+ * Find a START_TAG with the specified name without decreasing the depth, or increasing the
+ * depth by more than one. More deeply nested elements and text are skipped, even START_TAGs
+ * with matching names. Returns when the START_TAG is found or the next (non-nested) END_TAG is
+ * encountered. The return can take the form of an exception or a false if the START_TAG is not
+ * found. True is returned when it is.
+ */
+ private static boolean findStartTag(
+ XmlPullParser parser, String elementName, boolean elementRequired)
+ throws IOException, XmlPullParserException {
+
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ switch (type) {
+ case XmlPullParser.START_TAG:
+ String currentElementName = parser.getName();
+ if (elementName.equals(currentElementName)) {
+ return true;
+ }
+
+ // It was not the START_TAG we were looking for. Consume until the end.
+ parser.next();
+ consumeUntilEndTag(parser, currentElementName);
+ break;
+ case XmlPullParser.END_TAG:
+ if (elementRequired) {
+ throw new XmlPullParserException(
+ "No child element found with name " + elementName);
+ }
+ return false;
+ default:
+ // Ignore.
+ break;
+ }
+ }
+ throw new XmlPullParserException("Unexpected end of document while looking for "
+ + elementName);
+ }
+
+ /**
+ * Consume the remaining contents of an element and move to the END_TAG. Used when processing
+ * within an element can stop. The parser must be pointing at either the END_TAG we are looking
+ * for, a TEXT, or a START_TAG nested within the element to be consumed.
+ */
+ private static void consumeUntilEndTag(XmlPullParser parser, String elementName)
+ throws IOException, XmlPullParserException {
+
+ if (parser.getEventType() == XmlPullParser.END_TAG
+ && elementName.equals(parser.getName())) {
+ // Early return - we are already there.
+ return;
+ }
+
+ // Keep track of the required depth in case there are nested elements to be consumed.
+ // Both the name and the depth must match our expectation to complete.
+
+ int requiredDepth = parser.getDepth();
+ // A TEXT tag would be at the same depth as the END_TAG we are looking for.
+ if (parser.getEventType() == XmlPullParser.START_TAG) {
+ // A START_TAG would have incremented the depth, so we're looking for an END_TAG one
+ // higher than the current tag.
+ requiredDepth--;
+ }
+
+ while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+ int type = parser.next();
+
+ int currentDepth = parser.getDepth();
+ if (currentDepth < requiredDepth) {
+ throw new XmlPullParserException(
+ "Unexpected depth while looking for end tag: "
+ + parser.getPositionDescription());
+ } else if (currentDepth == requiredDepth) {
+ if (type == XmlPullParser.END_TAG) {
+ if (elementName.equals(parser.getName())) {
+ return;
+ }
+ throw new XmlPullParserException(
+ "Unexpected eng tag: " + parser.getPositionDescription());
+ }
+ }
+ // Everything else is either a type we are not interested in or is too deep and so is
+ // ignored.
+ }
+ throw new XmlPullParserException("Unexpected end of document");
+ }
+
+ /**
+ * Reads the text inside the current element. Should be called when the parser is currently
+ * on the START_TAG before the TEXT. The parser will be positioned on the END_TAG after this
+ * call when it completes successfully.
+ */
+ private static String consumeText(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+
+ int type = parser.next();
+ String text;
+ if (type == XmlPullParser.TEXT) {
+ text = parser.getText();
+ } else {
+ throw new XmlPullParserException("Text not found. Found type=" + type
+ + " at " + parser.getPositionDescription());
+ }
+
+ type = parser.next();
+ if (type != XmlPullParser.END_TAG) {
+ throw new XmlPullParserException(
+ "Unexpected nested tag or end of document when expecting text: type=" + type
+ + " at " + parser.getPositionDescription());
+ }
+ return text;
+ }
+
+ private static void checkOnEndTag(XmlPullParser parser, String elementName)
+ throws XmlPullParserException {
+ if (!(parser.getEventType() == XmlPullParser.END_TAG
+ && parser.getName().equals(elementName))) {
+ throw new XmlPullParserException(
+ "Unexpected tag encountered: " + parser.getPositionDescription());
+ }
+ }
+
+ /**
+ * Processes <countryzones> data.
+ */
+ private interface CountryZonesProcessor {
+
+ boolean CONTINUE = true;
+ boolean HALT = false;
+
+ /**
+ * Returns {@code #CONTINUE} if processing of the XML should continue, {@code HALT} if it
+ * should stop (but without considering this an error). Problems with parser are reported as
+ * an exception.
+ */
+ boolean process(String countryCode, List<String> timeZoneIds, String debugInfo)
+ throws XmlPullParserException;
+ }
+
+ /**
+ * Validates <countryzones> elements. To be valid the country ISO code must be unique
+ * and it must not be empty.
+ */
+ private static class CountryZonesValidator implements CountryZonesProcessor {
+
+ private final Set<String> knownCountryCodes = new HashSet<>();
+
+ @Override
+ public boolean process(String countryCode, List<String> timeZoneIds, String debugInfo)
+ throws XmlPullParserException {
+ if (knownCountryCodes.contains(countryCode)) {
+ throw new XmlPullParserException("Second entry for country code: " + countryCode
+ + " at " + debugInfo);
+ }
+ if (timeZoneIds.isEmpty()) {
+ throw new XmlPullParserException("No time zone IDs for country code: " + countryCode
+ + " at " + debugInfo);
+ }
+
+ // We don't validate the zone IDs - they may be new and we can't easily check them
+ // against other timezone data that may be associated with this file.
+
+ knownCountryCodes.add(countryCode);
+
+ return CONTINUE;
+ }
+ }
+
+ /**
+ * Extracts the zones associated with a country code, halting when the country code is matched
+ * and making them available via {@link #getMatchedZones()}.
+ */
+ private static class CountryZonesExtractor implements CountryZonesProcessor {
+
+ private final String countryCodeToMatch;
+ private List<TimeZone> matchedZones;
+
+ private CountryZonesExtractor(String countryCodeToMatch) {
+ this.countryCodeToMatch = countryCodeToMatch;
+ }
+
+ @Override
+ public boolean process(String countryCode, List<String> timeZoneIds, String debugInfo) {
+ if (!countryCodeToMatch.equals(countryCode)) {
+ return CONTINUE;
+ }
+
+ List<TimeZone> timeZones = new ArrayList<>();
+ for (String zoneIdString : timeZoneIds) {
+ TimeZone tz = TimeZone.getTimeZone(zoneIdString);
+ if (tz.getID().equals(TimeZone.UNKNOWN_ZONE_ID)) {
+ System.logW("Skipping invalid zone: " + zoneIdString + " at " + debugInfo);
+ } else {
+ // The zone is frozen to prevent mutation by callers.
+ timeZones.add(tz.freeze());
+ }
+ }
+ matchedZones = Collections.unmodifiableList(timeZones);
+ return HALT;
+ }
+
+ /**
+ * Returns the matched zones, or {@code null} if there were no matches. Unknown zone IDs are
+ * ignored so the list can be empty if there were no zones or the zone IDs were not
+ * recognized.
+ */
+ List<TimeZone> getMatchedZones() {
+ return matchedZones;
+ }
+ }
+
+ /**
+ * A source of Readers that can be used repeatedly.
+ */
+ private interface ReaderSupplier {
+ /** Returns a Reader. Throws an IOException if the Reader cannot be created. */
+ Reader get() throws IOException;
+
+ static ReaderSupplier forFile(String fileName, Charset charSet) throws IOException {
+ Path file = Paths.get(fileName);
+ if (!Files.exists(file)) {
+ throw new FileNotFoundException(fileName + " does not exist");
+ }
+ if (!Files.isRegularFile(file) && Files.isReadable(file)) {
+ throw new IOException(fileName + " must be a regular readable file.");
+ }
+ return () -> Files.newBufferedReader(file, charSet);
+ }
+
+ static ReaderSupplier forString(String xml) {
+ return () -> new StringReader(xml);
+ }
+ }
+}
diff --git a/luni/src/test/java/libcore/util/TimeZoneFinderTest.java b/luni/src/test/java/libcore/util/TimeZoneFinderTest.java
new file mode 100644
index 0000000..0b31c9a
--- /dev/null
+++ b/luni/src/test/java/libcore/util/TimeZoneFinderTest.java
@@ -0,0 +1,729 @@
+/*
+ * Copyright (C) 2017 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 libcore.util;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import android.icu.util.TimeZone;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+public class TimeZoneFinderTest {
+
+ private static final int HOUR_MILLIS = 60 * 60 * 1000;
+
+ // Zones used in the tests. NEW_YORK_TZ and LONDON_TZ chosen because they never overlap but both
+ // have DST.
+ private static final TimeZone NEW_YORK_TZ = TimeZone.getTimeZone("America/New_York");
+ private static final TimeZone LONDON_TZ = TimeZone.getTimeZone("Europe/London");
+ // A zone that matches LONDON_TZ for WHEN_NO_DST. It does not have DST so differs for WHEN_DST.
+ private static final TimeZone REYKJAVIK_TZ = TimeZone.getTimeZone("Atlantic/Reykjavik");
+ // Another zone that matches LONDON_TZ for WHEN_NO_DST. It does not have DST so differs for
+ // WHEN_DST.
+ private static final TimeZone UTC_TZ = TimeZone.getTimeZone("Etc/UTC");
+
+ // 22nd July 2017, 13:14:15 UTC (DST time in all the timezones used in these tests that observe
+ // DST).
+ private static final long WHEN_DST = 1500729255000L;
+ // 22nd January 2018, 13:14:15 UTC (non-DST time in all timezones used in these tests).
+ private static final long WHEN_NO_DST = 1516626855000L;
+
+ private static final int LONDON_DST_OFFSET_MILLIS = HOUR_MILLIS;
+ private static final int LONDON_NO_DST_OFFSET_MILLIS = 0;
+
+ private static final int NEW_YORK_DST_OFFSET_MILLIS = -4 * HOUR_MILLIS;
+ private static final int NEW_YORK_NO_DST_OFFSET_MILLIS = -5 * HOUR_MILLIS;
+
+ private Path testDir;
+
+ @Before
+ public void setUp() throws Exception {
+ testDir = Files.createTempDirectory("TimeZoneFinderTest");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // Delete the testDir and all contents.
+ Files.walkFileTree(testDir, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+ throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc)
+ throws IOException {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+
+ @Test
+ public void createInstanceWithFallback() throws Exception {
+ String validXml1 = "<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n";
+ String validXml2 = "<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/Paris</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n";
+
+ String invalidXml = "<foo></foo>\n";
+ checkValidateThrowsParserException(invalidXml);
+
+ String validFile1 = createFile(validXml1);
+ String validFile2 = createFile(validXml2);
+ String invalidFile = createFile(invalidXml);
+ String missingFile = createMissingFile();
+
+ TimeZoneFinder file1ThenFile2 =
+ TimeZoneFinder.createInstanceWithFallback(validFile1, validFile2);
+ assertZonesEqual(zones("Europe/London"), file1ThenFile2.lookupTimeZonesByCountry("gb"));
+
+ TimeZoneFinder missingFileThenFile1 =
+ TimeZoneFinder.createInstanceWithFallback(missingFile, validFile1);
+ assertZonesEqual(zones("Europe/London"),
+ missingFileThenFile1.lookupTimeZonesByCountry("gb"));
+
+ TimeZoneFinder file2ThenFile1 =
+ TimeZoneFinder.createInstanceWithFallback(validFile2, validFile1);
+ assertZonesEqual(zones("Europe/Paris"), file2ThenFile1.lookupTimeZonesByCountry("gb"));
+
+ // We assume the file has been validated so an invalid file is not checked ahead of time.
+ // We will find out when we look something up.
+ TimeZoneFinder invalidThenValid =
+ TimeZoneFinder.createInstanceWithFallback(invalidFile, validFile1);
+ assertNull(invalidThenValid.lookupTimeZonesByCountry("gb"));
+
+ // This is not a normal case: It would imply a define shipped without a file in /system!
+ TimeZoneFinder missingFiles =
+ TimeZoneFinder.createInstanceWithFallback(missingFile, missingFile);
+ assertNull(missingFiles.lookupTimeZonesByCountry("gb"));
+ }
+
+ @Test
+ public void xmlParsing_emptyFile() throws Exception {
+ checkValidateThrowsParserException("");
+ }
+
+ @Test
+ public void xmlParsing_unexpectedRootElement() throws Exception {
+ checkValidateThrowsParserException("<foo></foo>\n");
+ }
+
+ @Test
+ public void xmlParsing_missingCountryZones() throws Exception {
+ checkValidateThrowsParserException("<timezones></timezones>\n");
+ }
+
+ @Test
+ public void xmlParsing_noCountriesOk() throws Exception {
+ validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ }
+
+ @Test
+ public void xmlParsing_unexpectedComments() throws Exception {
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <!-- This is a comment -->"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+
+ // This is a crazy comment, but also helps prove that TEXT nodes are coalesced by the
+ // parser.
+ finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/<!-- Don't freak out! -->London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ }
+
+ @Test
+ public void xmlParsing_unexpectedElementsIgnored() throws Exception {
+ String unexpectedElement = "<unexpected-element>\n<a /></unexpected-element>\n";
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " " + unexpectedElement
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+
+ finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " " + unexpectedElement
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+
+ finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " " + unexpectedElement
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+
+ finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " " + unexpectedElement
+ + " <id>Europe/Paris</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London", "Europe/Paris"),
+ finder.lookupTimeZonesByCountry("gb"));
+
+ finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " " + unexpectedElement
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+
+ // This test is important because it ensures we can extend the format in future with
+ // more information.
+ finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + " " + unexpectedElement
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ }
+
+ @Test
+ public void xmlParsing_unexpectedTextIgnored() throws Exception {
+ String unexpectedText = "unexpected-text";
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " " + unexpectedText
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+
+ finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " " + unexpectedText
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+
+ finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " " + unexpectedText
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+
+ finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " " + unexpectedText
+ + " <id>Europe/Paris</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London", "Europe/Paris"),
+ finder.lookupTimeZonesByCountry("gb"));
+ }
+
+ @Test
+ public void xmlParsing_truncatedInput() throws Exception {
+ checkValidateThrowsParserException("<timezones>\n");
+
+ checkValidateThrowsParserException("<timezones>\n"
+ + " <countryzones>\n");
+
+ checkValidateThrowsParserException("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n");
+
+ checkValidateThrowsParserException("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n");
+
+ checkValidateThrowsParserException("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n");
+
+ checkValidateThrowsParserException("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n");
+ }
+
+ @Test
+ public void xmlParsing_unexpectedChildInTimeZoneIdThrows() throws Exception {
+ checkValidateThrowsParserException("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id><unexpected-element /></id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ }
+
+ @Test
+ public void xmlParsing_unknownTimeZoneIdIgnored() throws Exception {
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Unknown_Id</id>\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb"));
+ }
+
+ @Test
+ public void xmlParsing_missingCountryCode() throws Exception {
+ checkValidateThrowsParserException("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country>\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ }
+
+ @Test
+ public void xmlParsing_unknownCountryReturnsNull() throws Exception {
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+ assertNull(finder.lookupTimeZonesByCountry("gb"));
+ }
+
+ @Test
+ public void lookupTimeZonesByCountry_structuresAreImmutable() throws Exception {
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+
+ List<TimeZone> gbList = finder.lookupTimeZonesByCountry("gb");
+ assertEquals(1, gbList.size());
+ assertImmutableList(gbList);
+ assertImmutableTimeZone(gbList.get(0));
+
+ assertNull(finder.lookupTimeZonesByCountry("unknown"));
+ }
+
+ @Test
+ public void lookupTimeZoneByCountryAndOffset_unknownCountry() throws Exception {
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"xx\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+
+ // Demonstrate the arguments work for a known country.
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_DST, null /* bias */));
+
+ // Test with an unknown country.
+ String unknownCountryCode = "yy";
+ assertNull(finder.lookupTimeZoneByCountryAndOffset(unknownCountryCode,
+ LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */));
+
+ assertNull(finder.lookupTimeZoneByCountryAndOffset(unknownCountryCode,
+ LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, LONDON_TZ /* bias */));
+ }
+
+ @Test
+ public void lookupTimeZoneByCountryAndOffset_oneCandidate() throws Exception {
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"xx\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+
+ // The three parameters match the configured zone: offset, isDst and when.
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_DST, null /* bias */));
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS,
+ false /* isDst */, WHEN_NO_DST, null /* bias */));
+
+ // Some lookup failure cases where the offset, isDst and when do not match the configured
+ // zone.
+ TimeZone noDstMatch1 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */);
+ assertNull(noDstMatch1);
+
+ TimeZone noDstMatch2 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */);
+ assertNull(noDstMatch2);
+
+ TimeZone noDstMatch3 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */);
+ assertNull(noDstMatch3);
+
+ TimeZone noDstMatch4 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */);
+ assertNull(noDstMatch4);
+
+ TimeZone noDstMatch5 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */);
+ assertNull(noDstMatch5);
+
+ TimeZone noDstMatch6 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */);
+ assertNull(noDstMatch6);
+
+ // Some bias cases below.
+
+ // The bias is irrelevant here: it matches what would be returned anyway.
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_DST, LONDON_TZ /* bias */));
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS,
+ false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */));
+ // A sample of a non-matching case with bias.
+ assertNull(finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */));
+
+ // The bias should be ignored: it doesn't match any of the country's zones.
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */));
+
+ // The bias should still be ignored even though it matches the offset information given:
+ // it doesn't match any of the country's configured zones.
+ assertNull(finder.lookupTimeZoneByCountryAndOffset("xx", NEW_YORK_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */));
+ }
+
+ @Test
+ public void lookupTimeZoneByCountryAndOffset_multipleNonOverlappingCandidates()
+ throws Exception {
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"xx\">\n"
+ + " <id>America/New_York</id>\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+
+ // The three parameters match the configured zone: offset, isDst and when.
+ assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */));
+ assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */));
+ assertZoneEquals(NEW_YORK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx",
+ NEW_YORK_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */));
+ assertZoneEquals(NEW_YORK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx",
+ NEW_YORK_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */));
+
+ // Some lookup failure cases where the offset, isDst and when do not match the configured
+ // zone. This is a sample, not complete.
+ TimeZone noDstMatch1 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */);
+ assertNull(noDstMatch1);
+
+ TimeZone noDstMatch2 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */);
+ assertNull(noDstMatch2);
+
+ TimeZone noDstMatch3 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ NEW_YORK_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */);
+ assertNull(noDstMatch3);
+
+ TimeZone noDstMatch4 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ NEW_YORK_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */);
+ assertNull(noDstMatch4);
+
+ TimeZone noDstMatch5 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */);
+ assertNull(noDstMatch5);
+
+ TimeZone noDstMatch6 = finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */);
+ assertNull(noDstMatch6);
+
+ // Some bias cases below.
+
+ // The bias is irrelevant here: it matches what would be returned anyway.
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_DST, LONDON_TZ /* bias */));
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS,
+ false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */));
+ // A sample of a non-matching case with bias.
+ assertNull(finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */));
+
+ // The bias should be ignored: it matches a configured zone, but the offset is wrong so
+ // should not be considered a match.
+ assertZoneEquals(LONDON_TZ,
+ finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS,
+ true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */));
+ }
+
+ // This is an artificial case very similar to America/Denver and America/Phoenix in the US: both
+ // have the same offset for 6 months of the year but diverge. Australia/Lord_Howe too.
+ @Test
+ public void lookupTimeZoneByCountryAndOffset_multipleOverlappingCandidates() throws Exception {
+ // Three zones that have the same offset for some of the year. Europe/London changes
+ // offset WHEN_DST, the others do not.
+ TimeZoneFinder finder = validate("<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"xx\">\n"
+ + " <id>Atlantic/Reykjavik</id>\n"
+ + " <id>Europe/London</id>\n"
+ + " <id>Etc/UTC</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n");
+
+ // This is the no-DST offset for LONDON_TZ, REYKJAVIK_TZ. UTC_TZ.
+ final int noDstOffset = LONDON_NO_DST_OFFSET_MILLIS;
+ // This is the DST offset for LONDON_TZ.
+ final int dstOffset = LONDON_DST_OFFSET_MILLIS;
+
+ // The three parameters match the configured zone: offset, isDst and when.
+ assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset,
+ true /* isDst */, WHEN_DST, null /* bias */));
+ assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset,
+ false /* isDst */, WHEN_NO_DST, null /* bias */));
+ assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset,
+ true /* isDst */, WHEN_DST, null /* bias */));
+ assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset,
+ false /* isDst */, WHEN_NO_DST, null /* bias */));
+ assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset,
+ false /* isDst */, WHEN_DST, null /* bias */));
+
+ // Some lookup failure cases where the offset, isDst and when do not match the configured
+ // zones.
+ TimeZone noDstMatch1 = finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset,
+ true /* isDst */, WHEN_NO_DST, null /* bias */);
+ assertNull(noDstMatch1);
+
+ TimeZone noDstMatch2 = finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset,
+ true /* isDst */, WHEN_DST, null /* bias */);
+ assertNull(noDstMatch2);
+
+ TimeZone noDstMatch3 = finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset,
+ true /* isDst */, WHEN_NO_DST, null /* bias */);
+ assertNull(noDstMatch3);
+
+ TimeZone noDstMatch4 = finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset,
+ false /* isDst */, WHEN_DST, null /* bias */);
+ assertNull(noDstMatch4);
+
+
+ // Some bias cases below.
+
+ // The bias is relevant here: it overrides what would be returned naturally.
+ assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset,
+ false /* isDst */, WHEN_NO_DST, null /* bias */));
+ assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset,
+ false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */));
+ assertZoneEquals(UTC_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset,
+ false /* isDst */, WHEN_NO_DST, UTC_TZ /* bias */));
+
+ // The bias should be ignored: it matches a configured zone, but the offset is wrong so
+ // should not be considered a match.
+ assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx",
+ LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, REYKJAVIK_TZ /* bias */));
+ }
+
+ @Test
+ public void consistencyTest() throws Exception {
+ // Confirm that no new zones have been added to zones.tab without also adding them to the
+ // configuration used to drive TimeZoneFinder.
+
+ // zone.tab is a tab separated ASCII file provided by IANA and included in Android's tzdata
+ // file. Each line contains a mapping from country code -> zone ID. The ordering used by
+ // TimeZoneFinder is Android-specific, but we can use zone.tab to make sure we know about
+ // all country zones. Any update to tzdata that adds, renames, or removes zones should be
+ // reflected in the file used by TimeZoneFinder.
+ Map<String, Set<String>> zoneTabMappings = new HashMap<>();
+ for (String line : ZoneInfoDB.getInstance().getZoneTab().split("\n")) {
+ int countryCodeEnd = line.indexOf('\t', 1);
+ int olsonIdStart = line.indexOf('\t', 4) + 1;
+ int olsonIdEnd = line.indexOf('\t', olsonIdStart);
+ if (olsonIdEnd == -1) {
+ olsonIdEnd = line.length(); // Not all zone.tab lines have a comment.
+ }
+ String countryCode = line.substring(0, countryCodeEnd);
+ String olsonId = line.substring(olsonIdStart, olsonIdEnd);
+ Set<String> zoneIds = zoneTabMappings.get(countryCode);
+ if (zoneIds == null) {
+ zoneIds = new HashSet<>();
+ zoneTabMappings.put(countryCode, zoneIds);
+ }
+ zoneIds.add(olsonId);
+ }
+
+ TimeZoneFinder timeZoneFinder = TimeZoneFinder.getInstance();
+ for (Map.Entry<String, Set<String>> countryEntry : zoneTabMappings.entrySet()) {
+ String countryCode = countryEntry.getKey();
+ // Android uses lower case, IANA uses upper.
+ countryCode = countryCode.toLowerCase();
+
+ List<String> ianaZoneIds = countryEntry.getValue().stream().sorted()
+ .collect(Collectors.toList());
+ List<TimeZone> androidZones = timeZoneFinder.lookupTimeZonesByCountry(countryCode);
+ List<String> androidZoneIds =
+ androidZones.stream().map(TimeZone::getID).sorted()
+ .collect(Collectors.toList());
+
+ assertEquals("Android zones for " + countryCode + " do not match IANA data",
+ ianaZoneIds, androidZoneIds);
+ }
+ }
+
+ private void assertImmutableTimeZone(TimeZone timeZone) {
+ try {
+ timeZone.setRawOffset(1000);
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+ }
+
+ private static void assertImmutableList(List<TimeZone> timeZones) {
+ try {
+ timeZones.add(null);
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+ }
+
+ private static void assertZoneEquals(TimeZone expected, TimeZone actual) {
+ // TimeZone.equals() only checks the ID, but that's ok for these tests.
+ assertEquals(expected, actual);
+ }
+
+ private static void assertZonesEqual(List<TimeZone> expected, List<TimeZone> actual) {
+ // TimeZone.equals() only checks the ID, but that's ok for these tests.
+ assertEquals(expected, actual);
+ }
+
+ private static void checkValidateThrowsParserException(String xml) throws Exception {
+ try {
+ validate(xml);
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ private static TimeZoneFinder validate(String xml) throws IOException {
+ TimeZoneFinder timeZoneFinder = TimeZoneFinder.createInstanceForTests(xml);
+ timeZoneFinder.validate();
+ return timeZoneFinder;
+ }
+
+ private static List<TimeZone> zones(String... ids) {
+ return Arrays.stream(ids).map(TimeZone::getTimeZone).collect(Collectors.toList());
+ }
+
+ private String createFile(String fileContent) throws IOException {
+ Path filePath = Files.createTempFile(testDir, null, null);
+ Files.write(filePath, fileContent.getBytes(StandardCharsets.UTF_8));
+ return filePath.toString();
+ }
+
+ private String createMissingFile() throws IOException {
+ Path filePath = Files.createTempFile(testDir, null, null);
+ Files.delete(filePath);
+ return filePath.toString();
+ }
+}
diff --git a/non_openjdk_java_files.mk b/non_openjdk_java_files.mk
index 31ee139..9d33f46 100644
--- a/non_openjdk_java_files.mk
+++ b/non_openjdk_java_files.mk
@@ -308,6 +308,7 @@
luni/src/main/java/libcore/util/RecoverySystem.java \
luni/src/main/java/libcore/util/SneakyThrow.java \
luni/src/main/java/libcore/util/TimeZoneDataFiles.java \
+ luni/src/main/java/libcore/util/TimeZoneFinder.java \
luni/src/main/java/libcore/util/ZoneInfo.java \
luni/src/main/java/libcore/util/ZoneInfoDB.java \
luni/src/main/java/libcore/util/HexEncoding.java \
diff --git a/tzdata/shared2/src/main/libcore/tzdata/shared2/TimeZoneDistro.java b/tzdata/shared2/src/main/libcore/tzdata/shared2/TimeZoneDistro.java
index dd01fb0..9358c70 100644
--- a/tzdata/shared2/src/main/libcore/tzdata/shared2/TimeZoneDistro.java
+++ b/tzdata/shared2/src/main/libcore/tzdata/shared2/TimeZoneDistro.java
@@ -37,6 +37,9 @@
/** The name of the file inside the distro containing ICU TZ data. */
public static final String ICU_DATA_FILE_NAME = "icu/icu_tzdata.dat";
+ /** The name of the file inside the distro containing time zone lookup data. */
+ public static final String TZLOOKUP_FILE_NAME = "tzlookup.xml";
+
/**
* The name of the file inside the distro containing the distro version information.
* The content is ASCII bytes representing a set of version numbers. See {@link DistroVersion}.
diff --git a/tzdata/tools2/src/main/libcore/tzdata/update2/tools/CreateTimeZoneDistro.java b/tzdata/tools2/src/main/libcore/tzdata/update2/tools/CreateTimeZoneDistro.java
index 4b70152..7fabf7a 100644
--- a/tzdata/tools2/src/main/libcore/tzdata/update2/tools/CreateTimeZoneDistro.java
+++ b/tzdata/tools2/src/main/libcore/tzdata/update2/tools/CreateTimeZoneDistro.java
@@ -30,7 +30,7 @@
* A command-line tool for creating a timezone update distro.
*
* Args:
- * tzdata.properties file - the file describing the distro (see template file in tzdata/tools)
+ * tzdata.properties file - the file describing the distro (see template file in tzdata/tools2)
* output file - the name of the file to be generated
*/
public class CreateTimeZoneDistro {
@@ -56,8 +56,9 @@
Integer.parseInt(getMandatoryProperty(p, "revision")));
TimeZoneDistroBuilder builder = new TimeZoneDistroBuilder()
.setDistroVersion(distroVersion)
- .setTzData(getMandatoryPropertyFile(p, "bionic.file"))
- .setIcuData(getMandatoryPropertyFile(p, "icu.file"));
+ .setTzDataFile(getMandatoryPropertyFile(p, "bionic.file"))
+ .setIcuDataFile(getMandatoryPropertyFile(p, "icu.file"))
+ .setTzLookupFile(getMandatoryPropertyFile(p, "tzlookup.file"));
TimeZoneDistro distro = builder.build();
File outputFile = new File(args[1]);
diff --git a/tzdata/tools2/src/main/libcore/tzdata/update2/tools/TimeZoneDistroBuilder.java b/tzdata/tools2/src/main/libcore/tzdata/update2/tools/TimeZoneDistroBuilder.java
index 4c12e96..9860489 100644
--- a/tzdata/tools2/src/main/libcore/tzdata/update2/tools/TimeZoneDistroBuilder.java
+++ b/tzdata/tools2/src/main/libcore/tzdata/update2/tools/TimeZoneDistroBuilder.java
@@ -19,6 +19,7 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import libcore.tzdata.shared2.DistroException;
@@ -34,6 +35,7 @@
private DistroVersion distroVersion;
private byte[] tzData;
private byte[] icuData;
+ private String tzLookupXml;
public TimeZoneDistroBuilder setDistroVersion(DistroVersion distroVersion) {
this.distroVersion = distroVersion;
@@ -56,11 +58,11 @@
return this;
}
- public TimeZoneDistroBuilder setTzData(File tzDataFile) throws IOException {
- return setTzData(readFileAsByteArray(tzDataFile));
+ public TimeZoneDistroBuilder setTzDataFile(File tzDataFile) throws IOException {
+ return setTzDataFile(readFileAsByteArray(tzDataFile));
}
- public TimeZoneDistroBuilder setTzData(byte[] tzData) {
+ public TimeZoneDistroBuilder setTzDataFile(byte[] tzData) {
this.tzData = tzData;
return this;
}
@@ -71,15 +73,24 @@
return this;
}
- public TimeZoneDistroBuilder setIcuData(File icuDataFile) throws IOException {
- return setIcuData(readFileAsByteArray(icuDataFile));
+ public TimeZoneDistroBuilder setIcuDataFile(File icuDataFile) throws IOException {
+ return setIcuDataFile(readFileAsByteArray(icuDataFile));
}
- public TimeZoneDistroBuilder setIcuData(byte[] icuData) {
+ public TimeZoneDistroBuilder setIcuDataFile(byte[] icuData) {
this.icuData = icuData;
return this;
}
+ public TimeZoneDistroBuilder setTzLookupFile(File tzLookupFile) throws IOException {
+ return setTzLookupXml(readFileAsUtf8(tzLookupFile));
+ }
+
+ public TimeZoneDistroBuilder setTzLookupXml(String tzlookupXml) {
+ this.tzLookupXml = tzlookupXml;
+ return this;
+ }
+
// For use in tests.
public TimeZoneDistroBuilder clearIcuDataForTests() {
this.icuData = null;
@@ -102,6 +113,10 @@
if (icuData != null) {
addZipEntry(zos, TimeZoneDistro.ICU_DATA_FILE_NAME, icuData);
}
+ if (tzLookupXml != null) {
+ addZipEntry(zos, TimeZoneDistro.TZLOOKUP_FILE_NAME,
+ tzLookupXml.getBytes(StandardCharsets.UTF_8));
+ }
} catch (IOException e) {
throw new DistroException("Unable to create zip file", e);
}
@@ -140,7 +155,7 @@
/**
* Returns the contents of 'path' as a byte array.
*/
- public static byte[] readFileAsByteArray(File file) throws IOException {
+ private static byte[] readFileAsByteArray(File file) throws IOException {
byte[] buffer = new byte[8192];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (FileInputStream fis = new FileInputStream(file)) {
@@ -151,5 +166,12 @@
}
return baos.toByteArray();
}
+
+ /**
+ * Returns the contents of 'path' as a String, having interpreted the file as UTF-8.
+ */
+ private String readFileAsUtf8(File file) throws IOException {
+ return new String(readFileAsByteArray(file), StandardCharsets.UTF_8);
+ }
}
diff --git a/tzdata/tools2/testing/prepareTzDataUpdates.sh b/tzdata/tools2/testing/prepareTzDataUpdates.sh
index 5b9b2ab..9a41942 100755
--- a/tzdata/tools2/testing/prepareTzDataUpdates.sh
+++ b/tzdata/tools2/testing/prepareTzDataUpdates.sh
@@ -26,6 +26,7 @@
# Get the current tzdata version and find both the previous and new versions.
TZDATA=libc/zoneinfo/tzdata
+TZLOOKUP=libc/zoneinfo/tzlookup.xml
TZHEADER=$(head -n1 bionic/$TZDATA | cut -c1-11)
@@ -88,6 +89,7 @@
rules.version=${TZ_PREVIOUS}
bionic.file=${TMP_PREVIOUS}/tzdata
icu.file=${TMP_PREVIOUS}/icu_tzdata.dat
+tzlookup.file=${ANDROID_BUILD_TOP}/bionic/${TZLOOKUP}
EOF
TZ_PREVIOUS_UPDATE_ZIP=update_${TZ_PREVIOUS}_test.zip
@@ -117,6 +119,7 @@
rules.version=${TZ_CURRENT}
bionic.file=${TMP_CURRENT}/tzdata
icu.file=${TMP_CURRENT}/icu_tzdata.dat
+tzlookup.file=${ANDROID_BUILD_TOP}/bionic/${TZLOOKUP}
EOF
TZ_CURRENT_UPDATE_ZIP=update_${TZ_CURRENT}_test.zip
@@ -146,6 +149,7 @@
rules.version=${TZ_NEXT}
bionic.file=${TMP_NEXT}/tzdata
icu.file=${TMP_NEXT}/icu_tzdata.dat
+tzlookup.file=${ANDROID_BUILD_TOP}/bionic/${TZLOOKUP}
EOF
TZ_NEXT_UPDATE_ZIP=update_${TZ_NEXT}_test.zip
diff --git a/tzdata/tools2/tzupdate.properties b/tzdata/tools2/tzupdate.properties
index 82fa2c4..264960b 100644
--- a/tzdata/tools2/tzupdate.properties
+++ b/tzdata/tools2/tzupdate.properties
@@ -8,4 +8,4 @@
bionic.file=
icu.file=
-
+tzlookup.file=
diff --git a/tzdata/update2/src/main/libcore/tzdata/update2/TimeZoneDistroInstaller.java b/tzdata/update2/src/main/libcore/tzdata/update2/TimeZoneDistroInstaller.java
index e1ed794..baadc85 100644
--- a/tzdata/update2/src/main/libcore/tzdata/update2/TimeZoneDistroInstaller.java
+++ b/tzdata/update2/src/main/libcore/tzdata/update2/TimeZoneDistroInstaller.java
@@ -25,6 +25,7 @@
import libcore.tzdata.shared2.FileUtils;
import libcore.tzdata.shared2.StagedDistroOperation;
import libcore.tzdata.shared2.TimeZoneDistro;
+import libcore.util.TimeZoneFinder;
import libcore.util.ZoneInfoDB;
/**
@@ -150,6 +151,7 @@
return INSTALL_FAIL_RULES_TOO_OLD;
}
+ // Validate the tzdata file.
File zoneInfoFile = new File(workingDir, TimeZoneDistro.TZDATA_FILE_NAME);
ZoneInfoDB.TzData tzData = ZoneInfoDB.TzData.loadTzData(zoneInfoFile.getPath());
if (tzData == null) {
@@ -164,7 +166,23 @@
} finally {
tzData.close();
}
- // TODO(nfuller): Add deeper validity checks / canarying before applying.
+
+ // Validate the tzlookup.xml file.
+ File tzLookupFile = new File(workingDir, TimeZoneDistro.TZLOOKUP_FILE_NAME);
+ if (!tzLookupFile.exists()) {
+ Slog.i(logTag, "Update not applied: " + tzLookupFile + " does not exist");
+ return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
+ }
+ try {
+ TimeZoneFinder timeZoneFinder =
+ TimeZoneFinder.createInstance(tzLookupFile.getPath());
+ timeZoneFinder.validate();
+ } catch (IOException e) {
+ Slog.i(logTag, "Update not applied: " + tzLookupFile + " failed validation", e);
+ return INSTALL_FAIL_VALIDATION_ERROR;
+ }
+
+ // TODO(nfuller): Add validity checks for ICU data / canarying before applying.
// http://b/31008728
Slog.i(logTag, "Applying time zone update");
diff --git a/tzdata/update2/src/test/libcore/tzdata/update2/TimeZoneDistroInstallerTest.java b/tzdata/update2/src/test/libcore/tzdata/update2/TimeZoneDistroInstallerTest.java
index cf40bf6..3af9f15 100644
--- a/tzdata/update2/src/test/libcore/tzdata/update2/TimeZoneDistroInstallerTest.java
+++ b/tzdata/update2/src/test/libcore/tzdata/update2/TimeZoneDistroInstallerTest.java
@@ -24,7 +24,6 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.util.Arrays;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
@@ -190,7 +189,7 @@
assertNoInstalledDistro();
}
- /** Tests that a distro with a missing file will not update the content. */
+ /** Tests that a distro with a missing tzdata file will not update the content. */
public void testStageInstallWithErrorCode_missingTzDataFile() throws Exception {
TimeZoneDistro stagedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
assertEquals(
@@ -209,7 +208,7 @@
assertNoInstalledDistro();
}
- /** Tests that a distro with a missing file will not update the content. */
+ /** Tests that a distro with a missing ICU file will not update the content. */
public void testStageInstallWithErrorCode_missingIcuFile() throws Exception {
TimeZoneDistro stagedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
assertEquals(
@@ -228,6 +227,44 @@
assertNoInstalledDistro();
}
+ /** Tests that a distro with a missing tzlookup file will not update the content. */
+ public void testStageInstallWithErrorCode_missingTzLookupFile() throws Exception {
+ TimeZoneDistro stagedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(stagedDistro.getBytes()));
+ assertInstallDistroStaged(stagedDistro);
+
+ TimeZoneDistro incompleteDistro =
+ createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
+ .setTzLookupXml(null)
+ .buildUnvalidated();
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
+ installer.stageInstallWithErrorCode(incompleteDistro.getBytes()));
+ assertInstallDistroStaged(stagedDistro);
+ assertNoInstalledDistro();
+ }
+
+ /** Tests that a distro with a bad tzlookup file will not update the content. */
+ public void testStageInstallWithErrorCode_badTzLookupFile() throws Exception {
+ TimeZoneDistro stagedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(stagedDistro.getBytes()));
+ assertInstallDistroStaged(stagedDistro);
+
+ TimeZoneDistro incompleteDistro =
+ createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
+ .setTzLookupXml("<foo />")
+ .buildUnvalidated();
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR,
+ installer.stageInstallWithErrorCode(incompleteDistro.getBytes()));
+ assertInstallDistroStaged(stagedDistro);
+ assertNoInstalledDistro();
+ }
+
/**
* Tests that an update will be unpacked even if there is a partial update from a previous run.
*/
@@ -449,6 +486,17 @@
byte[] bionicTzData = createTzData(rulesVersion);
byte[] icuData = new byte[] { 'a' };
+ String tzlookupXml = "<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"us\">\n"
+ + " <id>America/New_York\"</id>\n"
+ + " <id>America/Los_Angeles</id>\n"
+ + " </country>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n";
DistroVersion distroVersion = new DistroVersion(
DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
DistroVersion.CURRENT_FORMAT_MINOR_VERSION,
@@ -456,8 +504,9 @@
revision);
return new TimeZoneDistroBuilder()
.setDistroVersion(distroVersion)
- .setTzData(bionicTzData)
- .setIcuData(icuData);
+ .setTzDataFile(bionicTzData)
+ .setIcuDataFile(icuData)
+ .setTzLookupXml(tzlookupXml);
}
private void assertInstallDistroStaged(TimeZoneDistro expectedDistro) throws Exception {
@@ -476,6 +525,9 @@
File icuFile = new File(stagedTzDataDir, TimeZoneDistro.ICU_DATA_FILE_NAME);
assertTrue(icuFile.exists());
+ File tzLookupFile = new File(stagedTzDataDir, TimeZoneDistro.TZLOOKUP_FILE_NAME);
+ assertTrue(tzLookupFile.exists());
+
// Assert getStagedDistroState() is reporting correctly.
StagedDistroOperation stagedDistroOperation = installer.getStagedDistroOperation();
assertNotNull(stagedDistroOperation);
@@ -494,6 +546,8 @@
actualFile = icuFile;
} else if (entryName.endsWith(TimeZoneDistro.TZDATA_FILE_NAME)) {
actualFile = bionicFile;
+ } else if (entryName.endsWith(TimeZoneDistro.TZLOOKUP_FILE_NAME)) {
+ actualFile = tzLookupFile;
} else {
throw new AssertionFailedError("Unknown file found");
}