Add more tzlookup.xml time zone metadata

This commit adds support for metadata to tell when a time
zone (effectively) becomes unused.

There are numerous cases where the number of distinct
time zones needed to represent all local times in a country/
region drops over time. For example, since 1970 the number
of zones required in the US has dropped to 8 from 29.

This commit just adds support for the information. Later
commits can make use of it and populate the data.

Bug: 72142943
Test: CTS: run cts -m CtsLibcoreTestCases
Change-Id: I74623859a7d7cb55daa1e767e88c11da48915b0b
diff --git a/luni/src/main/java/libcore/util/CountryTimeZones.java b/luni/src/main/java/libcore/util/CountryTimeZones.java
index 048e551..f05bf28 100644
--- a/luni/src/main/java/libcore/util/CountryTimeZones.java
+++ b/luni/src/main/java/libcore/util/CountryTimeZones.java
@@ -62,16 +62,18 @@
     public final static class TimeZoneMapping {
         public final String timeZoneId;
         public final boolean showInPicker;
+        public final Long notUsedAfter;
 
-        TimeZoneMapping(String timeZoneId, boolean showInPicker) {
+        TimeZoneMapping(String timeZoneId, boolean showInPicker, Long notUsedAfter) {
             this.timeZoneId = timeZoneId;
             this.showInPicker = showInPicker;
+            this.notUsedAfter = notUsedAfter;
         }
 
         // VisibleForTesting
-        public static TimeZoneMapping createForTests(String timeZoneId,
-                boolean showInPicker) {
-            return new TimeZoneMapping(timeZoneId, showInPicker);
+        public static TimeZoneMapping createForTests(
+                String timeZoneId, boolean showInPicker, Long notUsedAfter) {
+            return new TimeZoneMapping(timeZoneId, showInPicker, notUsedAfter);
         }
 
         @Override
@@ -84,12 +86,13 @@
             }
             TimeZoneMapping that = (TimeZoneMapping) o;
             return showInPicker == that.showInPicker &&
-                    Objects.equals(timeZoneId, that.timeZoneId);
+                    Objects.equals(timeZoneId, that.timeZoneId) &&
+                    Objects.equals(notUsedAfter, that.notUsedAfter);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(timeZoneId, showInPicker);
+            return Objects.hash(timeZoneId, showInPicker, notUsedAfter);
         }
 
         @Override
@@ -97,6 +100,7 @@
             return "TimeZoneMapping{"
                     + "timeZoneId='" + timeZoneId + '\''
                     + ", showInPicker=" + showInPicker
+                    + ", notUsedAfter=" + notUsedAfter
                     + '}';
         }
 
diff --git a/luni/src/main/java/libcore/util/TimeZoneFinder.java b/luni/src/main/java/libcore/util/TimeZoneFinder.java
index 55e25c7..7c682d0 100644
--- a/luni/src/main/java/libcore/util/TimeZoneFinder.java
+++ b/luni/src/main/java/libcore/util/TimeZoneFinder.java
@@ -60,10 +60,15 @@
     private static final String DEFAULT_TIME_ZONE_ID_ATTRIBUTE = "default";
     private static final String EVER_USES_UTC_ATTRIBUTE = "everutc";
 
-    // Country -> Time zone mapping. e.g. <id picker="n">
+    // Country -> Time zone mapping. e.g. <id>ZoneId</id>, <id picker="n">ZoneId</id>,
+    // <id notafter={timestamp}>ZoneId</id>
     // The default for the picker attribute when unspecified is "y".
+    // The notafter attribute is optional. It specifies a timestamp (time in milliseconds from Unix
+    // epoch start) after which the zone is not (effectively) in use. If unspecified the zone is in
+    // use forever.
     private static final String ZONE_ID_ELEMENT = "id";
     private static final String ZONE_SHOW_IN_PICKER_ATTRIBUTE = "picker";
+    private static final String ZONE_NOT_USED_AFTER_ATTRIBUTE = "notafter";
 
     private static final String TRUE_ATTRIBUTE_VALUE = "y";
     private static final String FALSE_ATTRIBUTE_VALUE = "n";
@@ -395,6 +400,8 @@
             // The picker attribute is optional and defaulted to true.
             boolean showInPicker = parseBooleanAttribute(
                     parser, ZONE_SHOW_IN_PICKER_ATTRIBUTE, true /* defaultValue */);
+            Long notUsedAfter = parseLongAttribute(
+                    parser, ZONE_NOT_USED_AFTER_ATTRIBUTE, null /* defaultValue */);
             String zoneIdString = consumeText(parser);
 
             // Make sure we are on the </id> element.
@@ -406,7 +413,8 @@
                         + parser.getPositionDescription());
             }
 
-            TimeZoneMapping timeZoneMapping = new TimeZoneMapping(zoneIdString, showInPicker);
+            TimeZoneMapping timeZoneMapping =
+                    new TimeZoneMapping(zoneIdString, showInPicker, notUsedAfter);
             timeZoneMappings.add(timeZoneMapping);
         }
 
@@ -415,6 +423,25 @@
     }
 
     /**
+     * Parses an attribute value, which must be either {@code null} or a valid signed long value.
+     * If the attribute value is {@code null} then {@code defaultValue} is returned. If the
+     * attribute is present but not a valid long value then an XmlPullParserException is thrown.
+     */
+    private static Long parseLongAttribute(XmlPullParser parser, String attributeName,
+            Long defaultValue) throws XmlPullParserException {
+        String attributeValueString = parser.getAttributeValue(null /* namespace */, attributeName);
+        if (attributeValueString == null) {
+            return defaultValue;
+        }
+        try {
+            return Long.parseLong(attributeValueString);
+        } catch (NumberFormatException e) {
+            throw new XmlPullParserException("Attribute \"" + attributeName
+                    + "\" is not a long value: " + parser.getPositionDescription());
+        }
+    }
+
+    /**
      * Parses an attribute value, which must be either {@code null}, {@code "y"} or {@code "n"}.
      * If the attribute value is {@code null} then {@code defaultValue} is returned. If the
      * attribute is present but not "y" or "n" then an XmlPullParserException is thrown.
diff --git a/luni/src/test/java/libcore/libcore/util/CountryTimeZonesTest.java b/luni/src/test/java/libcore/libcore/util/CountryTimeZonesTest.java
index aeb9c00..3466e7b 100644
--- a/luni/src/test/java/libcore/libcore/util/CountryTimeZonesTest.java
+++ b/luni/src/test/java/libcore/libcore/util/CountryTimeZonesTest.java
@@ -722,7 +722,8 @@
      */
     private static List<TimeZoneMapping> timeZoneMappings(String... timeZoneIds) {
         return Arrays.stream(timeZoneIds)
-                .map(x -> TimeZoneMapping.createForTests(x, true))
+                .map(x -> TimeZoneMapping.createForTests(
+                        x, true /* picker */, null /* notUsedAfter */))
                 .collect(Collectors.toList());
     }
 
diff --git a/luni/src/test/java/libcore/libcore/util/CountryZonesFinderTest.java b/luni/src/test/java/libcore/libcore/util/CountryZonesFinderTest.java
index d9e2287..eca59a0 100644
--- a/luni/src/test/java/libcore/libcore/util/CountryZonesFinderTest.java
+++ b/luni/src/test/java/libcore/libcore/util/CountryZonesFinderTest.java
@@ -107,7 +107,8 @@
      */
     private static List<TimeZoneMapping> timeZoneMappings(String... timeZoneIds) {
         return Arrays.stream(timeZoneIds)
-                .map(x -> TimeZoneMapping.createForTests(x, true))
+                .map(x -> TimeZoneMapping.createForTests(
+                        x, true /* picker */, null /* notUsedAfter */))
                 .collect(Collectors.toList());
     }
 }
diff --git a/luni/src/test/java/libcore/libcore/util/TimeZoneFinderTest.java b/luni/src/test/java/libcore/libcore/util/TimeZoneFinderTest.java
index d4663b5..d3a0e03 100644
--- a/luni/src/test/java/libcore/libcore/util/TimeZoneFinderTest.java
+++ b/luni/src/test/java/libcore/libcore/util/TimeZoneFinderTest.java
@@ -462,10 +462,48 @@
         CountryTimeZones usTimeZones = finder.lookupCountryTimeZones("us");
         List<TimeZoneMapping> actualTimeZoneMappings = usTimeZones.getTimeZoneMappings();
         List<TimeZoneMapping> expectedTimeZoneMappings = list(
-                TimeZoneMapping.createForTests("America/New_York", true /* shownInPicker */),
-                TimeZoneMapping.createForTests("America/Los_Angeles", true /* shownInPicker */),
                 TimeZoneMapping.createForTests(
-                        "America/Indiana/Vincennes", false /* shownInPicker */)
+                        "America/New_York", true /* shownInPicker */, null /* notUsedAfter */),
+                TimeZoneMapping.createForTests(
+                        "America/Los_Angeles", true /* shownInPicker */, null /* notUsedAfter */),
+                TimeZoneMapping.createForTests(
+                        "America/Indiana/Vincennes", false /* shownInPicker */,
+                        null /* notUsedAfter */)
+        );
+        assertEquals(expectedTimeZoneMappings, actualTimeZoneMappings);
+    }
+
+    @Test
+    public void xmlParsing_badTimeZoneMappingNotAfter() throws Exception {
+        checkValidateThrowsParserException("<timezones ianaversion=\"2017b\">\n"
+                + "  <countryzones>\n"
+                + "    <country code=\"gb\" default=\"Europe/London\" everutc=\"y\">\n"
+                + "      <id notafter=\"sometimes\">Europe/London</id>\n"
+                + "    </country>\n"
+                + "  </countryzones>\n"
+                + "</timezones>\n");
+    }
+
+    @Test
+    public void xmlParsing_timeZoneMappingNotAfter() throws Exception {
+        TimeZoneFinder finder = validate("<timezones ianaversion=\"2017b\">\n"
+                + "  <countryzones>\n"
+                + "    <country code=\"us\" default=\"America/New_York\" everutc=\"n\">\n"
+                + "      <!-- Explicit notafter -->\n"
+                + "      <id notafter=\"1234\">America/New_York</id>\n"
+                + "      <!-- Missing notafter -->\n"
+                + "      <id>America/Indiana/Vincennes</id>\n"
+                + "    </country>\n"
+                + "  </countryzones>\n"
+                + "</timezones>\n");
+        CountryTimeZones usTimeZones = finder.lookupCountryTimeZones("us");
+        List<TimeZoneMapping> actualTimeZoneMappings = usTimeZones.getTimeZoneMappings();
+        List<TimeZoneMapping> expectedTimeZoneMappings = list(
+                TimeZoneMapping.createForTests(
+                        "America/New_York", true /* shownInPicker */, 1234L /* notUsedAfter */),
+                TimeZoneMapping.createForTests(
+                        "America/Indiana/Vincennes", true /* shownInPicker */,
+                        null /* notUsedAfter */)
         );
         assertEquals(expectedTimeZoneMappings, actualTimeZoneMappings);
     }
@@ -908,7 +946,7 @@
         assertEquals(expected, actual);
     }
 
-    private static void checkValidateThrowsParserException(String xml) throws Exception {
+    private static void checkValidateThrowsParserException(String xml) {
         try {
             validate(xml);
             fail();
@@ -927,7 +965,8 @@
      */
     private static List<TimeZoneMapping> timeZoneMappings(String... timeZoneIds) {
         return Arrays.stream(timeZoneIds)
-                .map(x -> TimeZoneMapping.createForTests(x, true))
+                .map(x -> TimeZoneMapping.createForTests(
+                        x, true /* showInPicker */, null /* notUsedAfter */))
                 .collect(Collectors.toList());
     }