Merge "Extend TzLookupGenerator to generate a new file" am: 0a4103ef37 am: d44d5dd375 am: a6af08b4b4 am: 16c2d8bfc7

Original change: https://android-review.googlesource.com/c/platform/system/timezone/+/1370416

Change-Id: I8b099714e8d9f9e449d98567dfd0f3dc6bec5655
diff --git a/input_tools/android/tzlookup_generator/Android.bp b/input_tools/android/tzlookup_generator/Android.bp
index b63324a..7278dc5 100644
--- a/input_tools/android/tzlookup_generator/Android.bp
+++ b/input_tools/android/tzlookup_generator/Android.bp
@@ -21,7 +21,19 @@
         include_dirs: ["external/protobuf/src"],
     },
 
-    srcs: ["src/main/proto/**/*.proto"],
+    srcs: ["src/main/proto/**/country_zones_file.proto"],
+}
+
+// Proto library
+java_library_host {
+    name: "tzaliasesprotos",
+
+    proto: {
+        type: "full",
+        include_dirs: ["external/protobuf/src"],
+    },
+
+    srcs: ["src/main/proto/**/tz_aliases_file.proto"],
 }
 
 java_library_host {
@@ -32,6 +44,7 @@
         "countryzonesprotos",
         "icu4j",
         "libprotobuf-java-full",
+        "tzaliasesprotos",
         "tztools_common",
     ],
 }
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java
index b010c01..95bbbf2 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java
@@ -30,7 +30,7 @@
 import java.util.stream.Collectors;
 
 /**
- * A class that knows about the structure of the backward file.
+ * A class that knows about the structure of the IANA tzdb backward file.
  */
 final class BackwardFile {
 
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/CountryZonesFileSupport.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/CountryZonesFileSupport.java
index 9f9ad77..2c82cb4 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/CountryZonesFileSupport.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/CountryZonesFileSupport.java
@@ -15,7 +15,7 @@
  */
 package com.android.libcore.timezone.tzlookup;
 
-import com.android.libcore.timezone.tzlookup.proto.CountryZonesFile;
+import com.android.libcore.timezone.countryzones.proto.CountryZonesFile;
 import com.google.protobuf.TextFormat;
 
 import java.io.BufferedReader;
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupFile.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupFile.java
index dd04d8e..6a24d0f 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupFile.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupFile.java
@@ -15,6 +15,7 @@
  */
 package com.android.libcore.timezone.tzlookup;
 
+import com.android.libcore.timezone.tzaliases.proto.TzAliasesFile;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
@@ -170,7 +171,7 @@
         private final String defaultTimeZoneId;
         private final boolean defaultTimeZoneBoost;
         private final boolean everUsesUtc;
-        private final List<TimeZoneMapping> timeZoneIds = new ArrayList<>();
+        private final List<TimeZoneMapping> timeZoneMappings = new ArrayList<>();
 
         Country(String isoCode, String defaultTimeZoneId, boolean defaultTimeZoneBoost,
                 boolean everUsesUtc) {
@@ -180,8 +181,41 @@
             this.everUsesUtc = everUsesUtc;
         }
 
-        void addTimeZoneIdentifier(TimeZoneMapping timeZoneId) {
-            timeZoneIds.add(timeZoneId);
+        void addTimeZoneMapping(TimeZoneMapping timeZoneMapping) {
+            timeZoneMappings.add(timeZoneMapping);
+        }
+
+        static TzAliasesFile.TimeZoneAliases createTimeZoneAliases(Country country) {
+            TzAliasesFile.TimeZoneAliases.Builder countryAliasesBuilder =
+                    TzAliasesFile.TimeZoneAliases.newBuilder();
+            for (TzLookupFile.TimeZoneMapping timeZoneMapping : country.timeZoneMappings) {
+                String mappingTimeZoneId = timeZoneMapping.olsonId;
+                String notUsedReplacementId = timeZoneMapping.notAfterReplacementId;
+                Instant notUsedAfterInstant = timeZoneMapping.notUsedAfterInclusive;
+                if (notUsedReplacementId != null && notUsedAfterInstant != null) {
+                    String replacedTimeZoneId = mappingTimeZoneId;
+                    TzAliasesFile.TimeZoneReplacement timeZoneReplacement =
+                            TzAliasesFile.TimeZoneReplacement.newBuilder()
+                                    .setReplacedId(replacedTimeZoneId)
+                                    .setReplacementId(notUsedReplacementId)
+                                    .setFromMillis(notUsedAfterInstant.toEpochMilli())
+                                    .build();
+                    countryAliasesBuilder.addTimeZoneReplacement(timeZoneReplacement);
+                }
+
+                for (String alternativeZoneId : timeZoneMapping.alternativeZoneIds) {
+                    // We could collapse links when notUsedReplacementId != null by using it instead
+                    // of mappingTimeZoneId below, but that would potentially lose information.
+                    // Leave it to the downstream components to collapse aliases if they want to.
+                    TzAliasesFile.TimeZoneLink timeZoneLink =
+                            TzAliasesFile.TimeZoneLink.newBuilder()
+                                    .setPreferredId(mappingTimeZoneId)
+                                    .setAlternativeId(alternativeZoneId)
+                                    .build();
+                    countryAliasesBuilder.addTimeZoneLink(timeZoneLink);
+                }
+            }
+            return countryAliasesBuilder.build();
         }
 
         static void writeXml(Country country, XMLStreamWriter writer)
@@ -195,8 +229,8 @@
             }
             writer.writeAttribute(EVER_USES_UTC_ATTRIBUTE, encodeBooleanAttribute(
                     country.everUsesUtc));
-            for (TimeZoneMapping timeZoneId : country.timeZoneIds) {
-                TimeZoneMapping.writeXml(timeZoneId, writer);
+            for (TimeZoneMapping timeZoneMapping : country.timeZoneMappings) {
+                TimeZoneMapping.writeXml(timeZoneMapping, writer);
             }
             writer.writeEndElement();
         }
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
index 75fef71..20ffe96 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
@@ -15,18 +15,23 @@
  */
 package com.android.libcore.timezone.tzlookup;
 
-import com.android.libcore.timezone.tzlookup.proto.CountryZonesFile;
+import com.android.libcore.timezone.tzaliases.proto.TzAliasesFile;
+import com.android.libcore.timezone.countryzones.proto.CountryZonesFile;
 import com.android.libcore.timezone.tzlookup.zonetree.CountryZoneTree;
 import com.android.libcore.timezone.tzlookup.zonetree.CountryZoneUsage;
 import com.android.libcore.timezone.util.Errors;
 import com.android.libcore.timezone.util.Errors.HaltExecutionException;
+import com.google.protobuf.TextFormat;
 import com.ibm.icu.util.BasicTimeZone;
 import com.ibm.icu.util.Calendar;
 import com.ibm.icu.util.GregorianCalendar;
 import com.ibm.icu.util.TimeZone;
 import com.ibm.icu.util.TimeZoneRule;
 
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
@@ -35,13 +40,14 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import javax.xml.stream.XMLStreamException;
 
 /**
- * Generates the tzlookup.xml file using the information from ICU4J, countryzones.txt, backwards
- * and zones.tab.
+ * Generates Android's tzlookup.xml and zone alias file using ICU4J, Android's countryzones.txt
+ * file, and TZDB's backwards and zones.tab files.
  */
 public final class TzLookupGenerator {
 
@@ -73,31 +79,34 @@
     private final String countryZonesFileIn;
     private final String zoneTabFileIn;
     private final String backwardFileIn;
-    private final String tzLookupXmlOut;
+    private final String tzLookupXmlFileOut;
+    private final String timeZoneAliasesFileOut;
 
     /**
      * Executes the generator.
      */
     public static void main(String[] args) throws Exception {
-        if (args.length != 4) {
+        if (args.length != 5) {
             System.err.println(
                     "usage: java com.android.libcore.timezone.tzlookup.TzLookupGenerator"
                             + " <[in] countryzones.txt file> <[in] zone.tab file>"
-                            + " <[in] backward file> <[out] tzlookup.xml file>");
+                            + " <[in] backward file>"
+                            + " <[out] tzlookup.xml file> <[out] zone alias file>");
             System.exit(0);
         }
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(args[0], args[1], args[2], args[3]);
+                new TzLookupGenerator(args[0], args[1], args[2], args[3], args[4]);
         boolean success = tzLookupGenerator.execute();
         System.exit(success ? 0 : 1);
     }
 
     TzLookupGenerator(String countryZonesFileIn, String zoneTabFileIn, String backwardFileIn,
-            String tzLookupXmlOut) {
+            String tzLookupXmlFileOut, String timeZoneAliasesFileOut) {
         this.countryZonesFileIn = countryZonesFileIn;
         this.zoneTabFileIn = zoneTabFileIn;
         this.backwardFileIn = backwardFileIn;
-        this.tzLookupXmlOut = tzLookupXmlOut;
+        this.tzLookupXmlFileOut = tzLookupXmlFileOut;
+        this.timeZoneAliasesFileOut = timeZoneAliasesFileOut;
     }
 
     boolean execute() {
@@ -152,12 +161,12 @@
             BackwardFile backwardIn = parseAndValidateBackwardFile(backwardFileIn, errors);
             errors.throwIfError("Errors accumulated");
 
-            TzLookupFile.TimeZones timeZonesOut = createOutputData(
+            OutputData outputData = createOutputData(
                     inputIanaVersion, zoneTabMapping, countriesIn, backwardIn, errors);
-            errors.throwIfError("Errors accumulated");
 
             // Write the output structure if there wasn't an error.
-            writeOutputData(timeZonesOut, tzLookupXmlOut, errors);
+            errors.throwIfError("Errors accumulated");
+            writeOutputData(outputData, tzLookupXmlFileOut, timeZoneAliasesFileOut, errors);
             return true;
         } catch (HaltExecutionException e) {
             logError("Stopping due to fatal condition", e);
@@ -227,29 +236,49 @@
         }
     }
 
-    private static void writeOutputData(TzLookupFile.TimeZones timeZonesOut,
-            String tzLookupXmlFileName, Errors errors) throws HaltExecutionException {
+    private static void writeOutputData(OutputData outputData,
+            String tzLookupXmlFileName, String timeZoneAliasesFileName, Errors errors)
+            throws HaltExecutionException {
         errors.pushScope("write " + tzLookupXmlFileName);
         try {
             // Write out the file used on device.
             logInfo("Writing " + tzLookupXmlFileName);
 
+            TzLookupFile.TimeZones timeZonesOut = outputData.getTzLookupTimeZones();
             TzLookupFile.write(timeZonesOut, tzLookupXmlFileName);
         } catch (IOException | XMLStreamException e) {
             errors.addFatalAndHalt("Unable to write " + tzLookupXmlFileName, e);
         } finally {
             errors.popScope();
         }
+
+        errors.pushScope("write " + timeZoneAliasesFileName);
+        try (OutputStreamWriter timeZoneAliasesWriter =
+                     new OutputStreamWriter(new FileOutputStream(timeZoneAliasesFileName),
+                             StandardCharsets.UTF_8)) {
+            // Write out the tz aliases file used during later stages of the pipeline.
+            logInfo("Writing " + timeZoneAliasesFileName);
+
+            TzAliasesFile.TimeZoneAliases timeZoneAliases = outputData.getTimeZoneAliases();
+            TextFormat.print(timeZoneAliases, timeZoneAliasesWriter);
+        } catch (IOException e) {
+            errors.addFatalAndHalt("Unable to write " + timeZoneAliasesFileName, e);
+        } finally {
+            errors.popScope();
+        }
     }
 
-    private static TzLookupFile.TimeZones createOutputData(String inputIanaVersion,
+    private static OutputData createOutputData(String inputIanaVersion,
             Map<String, List<String>> zoneTabMapping, List<CountryZonesFile.Country> countriesIn,
             BackwardFile backwardIn, Errors errors) throws HaltExecutionException {
 
         // Start constructing the output structure.
         TzLookupFile.TimeZones timeZonesOut = new TzLookupFile.TimeZones(inputIanaVersion);
-        TzLookupFile.CountryZones countryZonesOut = new TzLookupFile.CountryZones();
-        timeZonesOut.setCountryZones(countryZonesOut);
+        TzLookupFile.CountryZones tzLookupCountryZones = new TzLookupFile.CountryZones();
+        timeZonesOut.setCountryZones(tzLookupCountryZones);
+        TzAliasesFile.TimeZoneAliases.Builder timeZoneAliasesBuilder =
+                TzAliasesFile.TimeZoneAliases.newBuilder();
+        timeZoneAliasesBuilder.setIanaVersion(inputIanaVersion);
 
         // The time use when sampling the offsets for a zone.
         final long offsetSampleTimeMillis = getSampleOffsetTimeMillisForData(inputIanaVersion);
@@ -269,20 +298,22 @@
                 continue;
             }
 
-            TzLookupFile.Country countryOut = processCountry(
+            CountryOutputData countryOutputData = processCountry(
                     offsetSampleTimeMillis, everUseUtcStartTimeMillis, countryIn,
                     zoneTabCountryTimeZoneIds, backwardIn, errors);
-            if (countryOut == null) {
+            if (countryOutputData == null) {
                 // Continue processing countries if there are only errors.
                 continue;
             }
-            countryZonesOut.addCountry(countryOut);
+
+            tzLookupCountryZones.addCountry(countryOutputData.getTzLookupCountry());
+            timeZoneAliasesBuilder.mergeFrom(countryOutputData.getTimeZoneAliases());
         }
         errors.throwIfError("One or more countries failed");
-        return timeZonesOut;
+        return new OutputData(timeZonesOut, timeZoneAliasesBuilder.build());
     }
 
-    private static TzLookupFile.Country processCountry(long offsetSampleTimeMillis,
+    private static CountryOutputData processCountry(long offsetSampleTimeMillis,
             long everUseUtcStartTimeMillis, CountryZonesFile.Country countryIn,
             List<String> zoneTabCountryTimeZoneIds, BackwardFile backwardIn,
             Errors errors) {
@@ -412,12 +443,37 @@
                     TzLookupFile.TimeZoneMapping timeZoneIdOut = new TzLookupFile.TimeZoneMapping(
                             timeZoneInId, shownInPicker, notUsedAfterInstant, notUsedReplacementId,
                             alternativeZoneIds);
-                    countryOut.addTimeZoneIdentifier(timeZoneIdOut);
+                    countryOut.addTimeZoneMapping(timeZoneIdOut);
                 } finally {
                     errors.popScope();
                 }
             }
-            return countryOut;
+
+            // TimeZoneAliases contains only information that is available from Country so we can
+            // currently build one from the other.
+            // Note: Because TimeZoneAliases is driven from Android's country mapping data, the
+            // aliases will only include zone IDs that are linked to a country AND where the link
+            // stays within the scope of a ISO 3166 country code.
+            //
+            // For example, Europe/Guernsey is a link in IANA's data to Europe/London in the TZDB
+            // "europe" file. However, this is a cross-ISO code link: Europe/London is associated
+            // with ISO code "GB", Europe/Guernsey is associated with ISO code "GG". So
+            // Europe/Guernsey is not recognized as an alias for Europe/London and it will not
+            // appear in the TimeZoneAliases.
+            //
+            // This is the behavior we want: although the TZDB makes use of links as a shorthand
+            // when countries (often neighbours with complicated shared histories) have followed the
+            // same rules, Android's data almost exclusively uses region-specific time zone IDs on
+            // the assumption it is what users would prefer, e.g. if we didn't do this, it would be
+            // difficult for some users to understand why the exemplar location for their time zone
+            // is in a different region / country from the one they are in. We have had bugs to
+            // prove this for macro-areas like the EU that have harmonized time zone rules but where
+            // users still prefer (say) Europe/Paris over Europe/Berlin, where there has been no
+            // functional difference between their time zones for several decades.
+            TzAliasesFile.TimeZoneAliases timeZoneAliases =
+                    TzLookupFile.Country.createTimeZoneAliases(countryOut);
+
+            return new CountryOutputData(countryOut, timeZoneAliases);
         } finally{
             // End of country processing.
             errors.popScope();
@@ -621,4 +677,43 @@
     private static void logInfo(String msg) {
         System.err.println("I: " + msg);
     }
+
+    private static class CountryOutputData {
+        private final TzLookupFile.Country tzLookupCountry;
+        private final TzAliasesFile.TimeZoneAliases timeZoneAliases;
+
+        private CountryOutputData(TzLookupFile.Country tzLookupCountry,
+                TzAliasesFile.TimeZoneAliases timeZoneAliases) {
+            this.tzLookupCountry = Objects.requireNonNull(tzLookupCountry);
+            this.timeZoneAliases = Objects.requireNonNull(timeZoneAliases);
+        }
+
+        private TzLookupFile.Country getTzLookupCountry() {
+            return tzLookupCountry;
+        }
+
+        private TzAliasesFile.TimeZoneAliases getTimeZoneAliases() {
+            return timeZoneAliases;
+        }
+    }
+
+    private static class OutputData {
+
+        private final TzLookupFile.TimeZones tzLookupTimeZones;
+        private final TzAliasesFile.TimeZoneAliases timeZoneAliases;
+
+        private OutputData(TzLookupFile.TimeZones tzLookupTimeZones,
+                TzAliasesFile.TimeZoneAliases timeZoneAliases) {
+            this.tzLookupTimeZones = Objects.requireNonNull(tzLookupTimeZones);
+            this.timeZoneAliases = Objects.requireNonNull(timeZoneAliases);
+        }
+
+        private TzLookupFile.TimeZones getTzLookupTimeZones() {
+            return tzLookupTimeZones;
+        }
+
+        private TzAliasesFile.TimeZoneAliases getTimeZoneAliases() {
+            return timeZoneAliases;
+        }
+    }
 }
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/zonetree/CountryZoneTree.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/zonetree/CountryZoneTree.java
index c294cc8..88c9f21 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/zonetree/CountryZoneTree.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/zonetree/CountryZoneTree.java
@@ -15,8 +15,8 @@
  */
 package com.android.libcore.timezone.tzlookup.zonetree;
 
-import com.android.libcore.timezone.tzlookup.proto.CountryZonesFile;
-import com.android.libcore.timezone.tzlookup.proto.CountryZonesFile.Country;
+import com.android.libcore.timezone.countryzones.proto.CountryZonesFile;
+import com.android.libcore.timezone.countryzones.proto.CountryZonesFile.Country;
 import com.android.libcore.timezone.tzlookup.zonetree.ZoneOffsetPeriod.ZonePeriodsKey;
 import com.ibm.icu.text.TimeZoneNames;
 import com.ibm.icu.util.BasicTimeZone;
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/zonetree/UniqueZonesVisualizer.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/zonetree/UniqueZonesVisualizer.java
index 8d6f6c7..258872f 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/zonetree/UniqueZonesVisualizer.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/zonetree/UniqueZonesVisualizer.java
@@ -15,10 +15,10 @@
  */
 package com.android.libcore.timezone.tzlookup.zonetree;
 
+import com.android.libcore.timezone.countryzones.proto.CountryZonesFile.Country;
+import com.android.libcore.timezone.countryzones.proto.CountryZonesFile.CountryZones;
 import com.android.libcore.timezone.tzlookup.CountryZonesFileSupport;
 import com.android.libcore.timezone.tzlookup.TzLookupGenerator;
-import com.android.libcore.timezone.tzlookup.proto.CountryZonesFile.Country;
-import com.android.libcore.timezone.tzlookup.proto.CountryZonesFile.CountryZones;
 
 import java.io.IOException;
 import java.time.Instant;
diff --git a/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto b/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto
index 9ed5644..de3085b 100644
--- a/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto
+++ b/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto
@@ -16,10 +16,10 @@
 
 syntax = "proto2";
 
-option java_package = "com.android.libcore.timezone.tzlookup.proto";
+option java_package = "com.android.libcore.timezone.countryzones.proto";
 option java_multiple_files = false;
 
-package com.android.libcore.timezone.tzlookup.proto;
+package com.android.libcore.timezone.countryzones.proto;
 
 message CountryZones {
     required string ianaVersion = 1;
diff --git a/input_tools/android/tzlookup_generator/src/main/proto/tz_aliases_file.proto b/input_tools/android/tzlookup_generator/src/main/proto/tz_aliases_file.proto
new file mode 100644
index 0000000..c5ae52c
--- /dev/null
+++ b/input_tools/android/tzlookup_generator/src/main/proto/tz_aliases_file.proto
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+syntax = "proto2";
+
+option java_package = "com.android.libcore.timezone.tzaliases.proto";
+option java_multiple_files = false;
+
+package com.android.libcore.timezone.tzaliases.proto;
+
+// Information about Olson IDs used by Android.
+message TimeZoneAliases {
+    // The IANA TZDB version the data was generated from.
+    optional string ianaVersion = 1;
+    // Aliases that are always correct, i.e. obsoleted IDs.
+    repeated TimeZoneLink timeZoneLink = 2;
+    // Aliases that are time dependent. i.e. where time zones have become equivalent over time.
+    repeated TimeZoneReplacement timeZoneReplacement = 3;
+}
+
+// An alias / synonym when one time zone ID is just direct synonym for another.
+//
+// These will typically be as you'd expect from looking at the IANA tzdb backward file. However, the
+// IDs preferred by Android are determined by countryzones.txt, which could choose to continue using
+// an old ID, in which case the link may be reversed. This reversal is expected when an older
+// version of ICU doesn't have strings for a new ID because it was added after an Android release
+// launched.
+message TimeZoneLink {
+    // The alternative ID. This will typically be an obsoleted IANA ID.
+    required string alternativeId = 1;
+    // The Android preferred ID. This will typically be a newer / more correct Olson ID.
+    required string preferredId = 2;
+}
+
+// The functional replacement of one time zone by another after a point in time.
+// Computed by looking at offset behavior / zone metadata.
+message TimeZoneReplacement {
+    // The Olson ID that was replaced / ceased to be distinct.
+    required string replacedId = 1;
+    // The Olson ID that is the better / to use in place of replacedId.
+    required string replacementId = 2;
+    // When replacementId replaced replacedId. Milliseconds from the start of the Unix epoch.
+    required int64 fromMillis = 3;
+}
diff --git a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java
index e5ae559..373626a 100644
--- a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java
+++ b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java
@@ -16,8 +16,9 @@
 
 package com.android.libcore.timezone.tzlookup;
 
+import com.android.libcore.timezone.countryzones.proto.CountryZonesFile;
+import com.android.libcore.timezone.tzaliases.proto.TzAliasesFile;
 import com.android.libcore.timezone.testing.TestUtils;
-import com.android.libcore.timezone.tzlookup.proto.CountryZonesFile;
 import com.google.protobuf.TextFormat;
 import com.ibm.icu.util.TimeZone;
 
@@ -37,17 +38,18 @@
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import static com.android.libcore.timezone.countryzones.proto.CountryZonesFile.Country;
 import static com.android.libcore.timezone.testing.TestUtils.assertAbsent;
 import static com.android.libcore.timezone.testing.TestUtils.assertContains;
 import static com.android.libcore.timezone.testing.TestUtils.createFile;
-import static com.android.libcore.timezone.tzlookup.proto.CountryZonesFile.Country;
 import static junit.framework.TestCase.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 public class TzLookupGeneratorTest {
 
-    public static final String INVALID_TIME_ZONE_ID = "NOT_A_VALID_ID";
+    private static final String INVALID_TIME_ZONE_ID = "NOT_A_VALID_ID";
+    private static final String TZDB_VERSION = TimeZone.getTZDataVersion();
 
     private Path tempDir;
 
@@ -66,11 +68,12 @@
         String countryZonesFile = createFile(tempDir, "THIS IS NOT A VALID FILE");
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
     }
 
@@ -85,16 +88,17 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -104,18 +108,19 @@
                 createValidCountryGb().toBuilder().clearTimeZoneMappings().build();
         CountryZonesFile.CountryZones countryZones = createValidCountryZones(gbWithoutZones);
         String countryZonesFile = createCountryZonesFile(countryZones);
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
 
         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -130,18 +135,19 @@
         CountryZonesFile.CountryZones countryZones =
                 createValidCountryZones(gbWithDuplicateZones);
         String countryZonesFile = createCountryZonesFile(countryZones);
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
 
         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -156,16 +162,17 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -179,16 +186,17 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -202,11 +210,11 @@
                 .clearDefaultTimeZoneId().build();
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
 
-        String tzLookupXml = generateTzLookupXml(gbWithoutDefault, gbZoneTabEntries,
-                createValidBackwardLinks());
+        OutputData outputData =
+                generateOutputData(gbWithoutDefault, gbZoneTabEntries, createEmptyBackwardLinks());
 
         // Check gb's time zone was defaulted.
-        assertContains(tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
+        assertContains(outputData.tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
     }
 
     @Test
@@ -220,11 +228,11 @@
                         .build();
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
 
-        String tzLookupXml = generateTzLookupXml(gbWithExplicitDefaultTimeZone, gbZoneTabEntries,
-                createValidBackwardLinks());
+        OutputData outputData = generateOutputData(
+                gbWithExplicitDefaultTimeZone, gbZoneTabEntries, createEmptyBackwardLinks());
 
         // Check gb's time zone was defaulted.
-        assertContains(tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
+        assertContains(outputData.tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
     }
 
     @Test
@@ -239,16 +247,17 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -262,16 +271,17 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -283,15 +293,16 @@
 
         String zoneTabFile =
                 createZoneTabFile(createValidZoneTabEntriesFr(), createValidZoneTabEntriesUs());
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -305,15 +316,16 @@
         String countryZonesFile = createCountryZonesFile(countryZones);
 
         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -325,15 +337,16 @@
         String zoneTabFileWithDupes = createZoneTabFile(
                 createValidZoneTabEntriesGb(), createValidZoneTabEntriesGb());
 
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
-                countryZonesFile, zoneTabFileWithDupes, backwardFile, outputFile);
+                countryZonesFile, zoneTabFileWithDupes, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -347,16 +360,17 @@
         String countryZonesFile = createCountryZonesFile(countryZones);
 
         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -374,16 +388,17 @@
                 new ArrayList<>(createValidZoneTabEntriesGb());
         zoneTabEntriesWithBadId.add(new ZoneTabFile.CountryEntry("GB", INVALID_TIME_ZONE_ID));
         String zoneTabFile = createZoneTabFile(zoneTabEntriesWithBadId);
-        String backwardFile = createBackwardFile(createValidBackwardLinks());
+        String backwardFile = createBackwardFile(createEmptyBackwardLinks());
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
@@ -394,20 +409,51 @@
 
         String badBackwardFile = TestUtils.createFile(tempDir, "THIS IS NOT VALID");
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, badBackwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, badBackwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertEquals(0, Files.size(outputFilePath));
+        assertFileMissing(tzLookupFile);
+        assertFileMissing(tzAliasFile);
     }
 
     @Test
-    public void usingOldLinksValid() throws Exception {
+    public void checkNormalLinks() throws Exception {
+        String countryZonesText = "isoCode:\"gb\"\n"
+                + "timeZoneMappings:<\n"
+                + "  utcOffset:\"0:00\"\n"
+                + "  id:\"Europe/London\"\n"
+                + ">\n";
+
+        Country country = parseCountry(countryZonesText);
+        List<ZoneTabFile.CountryEntry> zoneTab = Arrays.asList(
+                new ZoneTabFile.CountryEntry("GB", "Europe/London"));
+        Map<String, String> backwardLinks = new HashMap<>();
+        // GB is an obsoleted ID for Europe/London.
+        backwardLinks.put("GB", "Europe/London");
+
+        OutputData outputData = generateOutputData(country, zoneTab, backwardLinks);
+
+        // GB will be listed as an alternative for Europe/London.
+        String expectedTzLookupXmlLine = "<id alts=\"GB\">Europe/London</id>\n";
+        assertContains(outputData.tzLookupXml, expectedTzLookupXmlLine);
+
+        TzAliasesFile.TimeZoneAliases.Builder b = TzAliasesFile.TimeZoneAliases
+                .newBuilder()
+                .setIanaVersion(TZDB_VERSION);
+        addLink(b, "GB" /* alternativeId */, "Europe/London" /* preferredId */);
+        assertEquals(b.build(), outputData.timeZoneAliases);
+    }
+
+    @Test
+    public void usingOldIdsInCountryTextIsValid() throws Exception {
         // This simulates a case where America/Godthab has been superseded by America/Nuuk in IANA
-        // data, but Android wants to continue using America/Godthab.
+        // data, but Android wants to continue using America/Godthab. This is signaled as deliberate
+        // through the use of the aliasId in countryzones.txt (otherwise the tooling will complain,
+        // see next test).
         String countryZonesWithOldIdText =
                 "isoCode:\"gl\"\n"
                 + "defaultTimeZoneId:\"America/Godthab\"\n"
@@ -441,16 +487,34 @@
         Map<String, String> backwardLinks = new HashMap<>();
         backwardLinks.put("America/Godthab", "America/Nuuk");
 
-        String tzLookupXml = generateTzLookupXml(country, zoneTabWithNewIds, backwardLinks);
+        OutputData outputData = generateOutputData(country, zoneTabWithNewIds, backwardLinks);
 
-        String expectedOutput = "<id>America/Danmarkshavn</id>\n"
+        String expectedTzLookupOutput = "<id>America/Danmarkshavn</id>\n"
                 + "<id>America/Scoresbysund</id>\n"
                 + "<id alts=\"America/Nuuk\">America/Godthab</id>\n"
                 + "<id>America/Thule</id>\n";
-        String[] expectedLines = expectedOutput.split("\\n");
-        for (String expectedLine : expectedLines) {
-            assertContains(tzLookupXml, expectedLine);
+        String[] expectedTzLookupXmlLines = expectedTzLookupOutput.split("\\n");
+        for (String expectedTzLookupXmlLine : expectedTzLookupXmlLines) {
+            assertContains(outputData.tzLookupXml, expectedTzLookupXmlLine);
         }
+
+        TzAliasesFile.TimeZoneAliases.Builder b = TzAliasesFile.TimeZoneAliases
+                .newBuilder()
+                .setIanaVersion(TZDB_VERSION);
+        // Because Android lists America/Nuuk as the aliasId in countryzones.txt, the link will
+        // be reversed from the usual.
+        addLink(b, "America/Nuuk" /* alternativeId */, "America/Godthab" /* preferredId */);
+        assertEquals(b.build(), outputData.timeZoneAliases);
+    }
+
+    private static void addLink(TzAliasesFile.TimeZoneAliases.Builder builder, String alternativeId,
+            String preferredId) {
+        TzAliasesFile.TimeZoneLink link =
+                TzAliasesFile.TimeZoneLink.newBuilder()
+                        .setAlternativeId(alternativeId)
+                        .setPreferredId(preferredId)
+                        .build();
+        builder.addTimeZoneLink(link);
     }
 
     @Test
@@ -499,21 +563,21 @@
     @Test
     public void everUtc_true() throws Exception {
         CountryZonesFile.Country validCountryGb = createValidCountryGb();
-        String tzLookupXml = generateTzLookupXml(validCountryGb, createValidZoneTabEntriesGb(),
-                createValidBackwardLinks());
+        OutputData outputData = generateOutputData(
+                validCountryGb, createValidZoneTabEntriesGb(), createEmptyBackwardLinks());
 
         // Check gb's entry contains everutc="y".
-        assertContains(tzLookupXml, "everutc=\"y\"");
+        assertContains(outputData.tzLookupXml, "everutc=\"y\"");
     }
 
     @Test
     public void everUtc_false() throws Exception {
         CountryZonesFile.Country validCountryFr = createValidCountryFr();
-        String tzLookupXml = generateTzLookupXml(validCountryFr, createValidZoneTabEntriesFr(),
-                createValidBackwardLinks());
+        OutputData outputData = generateOutputData(
+                validCountryFr, createValidZoneTabEntriesFr(), createEmptyBackwardLinks());
 
         // Check fr's entry contains everutc="n".
-        assertContains(tzLookupXml, "everutc=\"n\"");
+        assertContains(outputData.tzLookupXml, "everutc=\"n\"");
     }
 
     @Test
@@ -528,10 +592,10 @@
         countryBuilder.setTimeZoneMappings(0, timeZoneMappingBuilder);
         CountryZonesFile.Country country = countryBuilder.build();
 
-        String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr(),
-                createValidBackwardLinks());
+        OutputData outputData = generateOutputData(
+                country, createValidZoneTabEntriesFr(), createEmptyBackwardLinks());
 
-        assertContains(tzLookupXml, "picker=\"n\"");
+        assertContains(outputData.tzLookupXml, "picker=\"n\"");
     }
 
     @Test
@@ -546,20 +610,20 @@
         countryBuilder.setTimeZoneMappings(0, timeZoneMappingBuilder);
         CountryZonesFile.Country country = countryBuilder.build();
 
-        String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr(),
-                createValidBackwardLinks());
+        OutputData outputData = generateOutputData(
+                country, createValidZoneTabEntriesFr(), createEmptyBackwardLinks());
 
         // We should not see anything "picker="y" is the implicit default.
-        assertAbsent(tzLookupXml, "picker=");
+        assertAbsent(outputData.tzLookupXml, "picker=");
     }
 
     @Test
     public void notAfter() throws Exception {
         CountryZonesFile.Country country = createValidCountryUs();
         List<ZoneTabFile.CountryEntry> zoneTabEntries = createValidZoneTabEntriesUs();
-        String tzLookupXml = generateTzLookupXml(country, zoneTabEntries,
-                createValidBackwardLinks());
-        String expectedOutput =
+        OutputData outputData = generateOutputData(
+                country, zoneTabEntries, createEmptyBackwardLinks());
+        String expectedTzLookupOutput =
                 "<id>America/New_York</id>\n"
                 + "<id notafter=\"167814000000\" repl=\"America/New_York\">America/Detroit</id>\n"
                 + "<id notafter=\"152089200000\" repl=\"America/New_York\">America/Kentucky/Louisville</id>\n"
@@ -589,13 +653,60 @@
                 + "<id notafter=\"341402400000\" repl=\"America/Anchorage\">America/Sitka</id>\n"
                 + "<id>Pacific/Honolulu</id>\n"
                 + "<id>America/Adak</id>\n";
-        String[] expectedLines = expectedOutput.split("\\n");
-        for (String expectedLine : expectedLines) {
-            assertContains(tzLookupXml, expectedLine);
+        String[] expectedTzLookupXmlLines = expectedTzLookupOutput.split("\\n");
+        for (String expectedTzLookupXmlLine : expectedTzLookupXmlLines) {
+            assertContains(outputData.tzLookupXml, expectedTzLookupXmlLine);
+        }
+        
+        TzAliasesFile.TimeZoneAliases.Builder b = TzAliasesFile.TimeZoneAliases
+                .newBuilder()
+                .setIanaVersion(TZDB_VERSION);
+        addReplacement(b, 167814000000L, "America/New_York", "America/Detroit");
+        addReplacement(b, 152089200000L, "America/New_York", "America/Kentucky/Louisville");
+        addReplacement(b, 972802800000L, "America/New_York", "America/Kentucky/Monticello");
+        addReplacement(b, 1130652000000L, "America/New_York", "America/Indiana/Indianapolis");
+        addReplacement(b, 1194159600000L, "America/New_York", "America/Indiana/Vincennes");
+        addReplacement(b, 1173600000000L, "America/New_York", "America/Indiana/Winamac");
+        addReplacement(b, 183535200000L, "America/New_York", "America/Indiana/Marengo");
+        addReplacement(b, 247042800000L, "America/New_York", "America/Indiana/Petersburg");
+        addReplacement(b, 89186400000L, "America/New_York", "America/Indiana/Vevay");
+        addReplacement(b, 688546800000L, "America/Chicago", "America/Indiana/Knox");
+        addReplacement(b, 104918400000L, "America/Chicago", "America/Menominee");
+        addReplacement(b, 720000000000L, "America/Chicago", "America/North_Dakota/Center");
+        addReplacement(b, 1067155200000L, "America/Chicago", "America/North_Dakota/New_Salem");
+        addReplacement(b, 1143964800000L, "America/Chicago", "America/Indiana/Tell_City");
+        addReplacement(b, 1289116800000L, "America/Chicago", "America/North_Dakota/Beulah");
+        addReplacement(b, 129114000000L, "America/Phoenix", "America/Boise");
+        addReplacement(b, 436359600000L, "America/Anchorage", "America/Juneau");
+        addReplacement(b, 436356000000L, "America/Anchorage", "America/Yakutat");
+        addReplacement(b, 436363200000L, "America/Anchorage", "America/Nome");
+        addReplacement(b, 1547978400000L, "America/Anchorage", "America/Metlakatla");
+        addReplacement(b, 341402400000L, "America/Anchorage", "America/Sitka");;
+        assertEquals(b.build(), outputData.timeZoneAliases);
+    }
+
+    private static void addReplacement(TzAliasesFile.TimeZoneAliases.Builder builder,
+            long fromMillis, String replacementId, String replacedId) {
+        TzAliasesFile.TimeZoneReplacement replacement =
+                TzAliasesFile.TimeZoneReplacement.newBuilder()
+                        .setReplacedId(replacedId)
+                        .setReplacementId(replacementId)
+                        .setFromMillis(fromMillis)
+                        .build();
+        builder.addTimeZoneReplacement(replacement);
+    }
+
+    static class OutputData {
+        final String tzLookupXml;
+        final TzAliasesFile.TimeZoneAliases timeZoneAliases;
+
+        OutputData(String tzLookupXml, TzAliasesFile.TimeZoneAliases timeZoneAliases) {
+            this.tzLookupXml = tzLookupXml;
+            this.timeZoneAliases = timeZoneAliases;
         }
     }
 
-    private String generateTzLookupXml(CountryZonesFile.Country country,
+    private OutputData generateOutputData(CountryZonesFile.Country country,
             List<ZoneTabFile.CountryEntry> zoneTabEntries, Map<String, String> backwardLinks)
             throws Exception {
 
@@ -605,16 +716,23 @@
         String zoneTabFile = createZoneTabFile(zoneTabEntries);
         String backwardFile = createBackwardFile(backwardLinks);
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertTrue(tzLookupGenerator.execute());
 
-        Path outputFilePath = Paths.get(outputFile);
-        assertTrue(Files.exists(outputFilePath));
+        Path tzLookupFilePath = checkFileExists(tzLookupFile);
+        String tzLookupXml = readFileToString(tzLookupFilePath);
 
-        return readFileToString(outputFilePath);
+        Path tzAliasFilePath = checkFileExists(tzAliasFile);
+        String timeZoneAliasesText = readFileToString(tzAliasFilePath);
+        TzAliasesFile.TimeZoneAliases.Builder timeZoneAliasesBuilder =
+                TzAliasesFile.TimeZoneAliases.newBuilder();
+        TextFormat.merge(timeZoneAliasesText, timeZoneAliasesBuilder);
+
+        return new OutputData(tzLookupXml, timeZoneAliasesBuilder.build());
     }
 
     private void generateTzLookupXmlExpectFailure(CountryZonesFile.Country country,
@@ -627,10 +745,11 @@
         String zoneTabFile = createZoneTabFile(zoneTabEntries);
         String backwardFile = createBackwardFile(backwardLinks);
 
-        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+        String tzLookupFile = createTempFileName("tzlookup");
+        String tzAliasFile = createTempFileName("tzalias");
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFile, backwardFile, tzLookupFile, tzAliasFile);
         assertFalse(tzLookupGenerator.execute());
     }
 
@@ -656,7 +775,7 @@
             CountryZonesFile.Country... countries) {
         CountryZonesFile.CountryZones.Builder builder =
                 CountryZonesFile.CountryZones.newBuilder()
-                        .setIanaVersion(TimeZone.getTZDataVersion());
+                        .setIanaVersion(TZDB_VERSION);
         for (CountryZonesFile.Country country : countries) {
             builder.addCountries(country);
         }
@@ -854,8 +973,14 @@
     }
 
     private static List<ZoneTabFile.CountryEntry> createValidZoneTabEntriesFr() {
-        return Arrays.asList(
-                new ZoneTabFile.CountryEntry("FR", "Europe/Paris"));
+        return Arrays.asList(new ZoneTabFile.CountryEntry("FR", "Europe/Paris"));
+    }
+
+    /** Returns a file name for a file that does not exist. */
+    private String createTempFileName(String fileNamePrefix) throws IOException {
+        Path tempFile = Files.createTempFile(tempDir, fileNamePrefix, null /* suffix */);
+        Files.delete(tempFile);
+        return tempFile.toString();
     }
 
     private String createBackwardFile(Map<String, String> links) throws Exception {
@@ -865,10 +990,8 @@
         return TestUtils.createFile(tempDir, lines.toArray(new String[0]));
     }
 
-    private static Map<String, String> createValidBackwardLinks() {
-        Map<String, String> map = new HashMap<>();
-        map.put("America/Godthab", "America/Nuuk");
-        return map;
+    private static Map<String, String> createEmptyBackwardLinks() {
+        return new HashMap<>();
     }
 
     private static Country parseCountry(String text) throws Exception {
@@ -877,4 +1000,14 @@
         return builder.build();
     }
 
+    private static Path checkFileExists(String fileName) {
+        Path filePath = Paths.get(fileName);
+        assertTrue("File " + filePath + " unexpectedly missing", Files.exists(filePath));
+        return filePath;
+    }
+
+    private static void assertFileMissing(String fileName) throws IOException {
+        Path filePath = Paths.get(fileName);
+        assertFalse("File " + filePath + " unexpectedly exists", Files.exists(filePath));
+    }
 }
diff --git a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/zonetree/CountryZoneTreeTest.java b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/zonetree/CountryZoneTreeTest.java
index e93c380..3986215 100644
--- a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/zonetree/CountryZoneTreeTest.java
+++ b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/zonetree/CountryZoneTreeTest.java
@@ -23,7 +23,7 @@
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 
-import static com.android.libcore.timezone.tzlookup.proto.CountryZonesFile.Country;
+import static com.android.libcore.timezone.countryzones.proto.CountryZonesFile.Country;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
diff --git a/update-tzdata.py b/update-tzdata.py
index f914719..f35beca 100755
--- a/update-tzdata.py
+++ b/update-tzdata.py
@@ -144,7 +144,8 @@
   iana_zic_data_version = GetIanaVersion(iana_zic_data_tar_file)
 
   print('Found IANA zic release %s/%s in %s/%s ...' \
-      % (iana_zic_code_version, iana_zic_data_version, iana_zic_code_tar_file, iana_zic_data_tar_file))
+      % (iana_zic_code_version, iana_zic_data_version, iana_zic_code_tar_file,
+         iana_zic_data_tar_file))
 
   zic_build_dir = '%s/zic' % tmp_dir
   ExtractTarFile(iana_zic_code_tar_file, zic_build_dir)
@@ -190,18 +191,19 @@
                          header_string])
 
 
-def BuildTzlookup(iana_data_dir):
+def BuildTzlookupAndTzAliases(iana_data_dir, tzalias_dest_dir):
   countryzones_source_file = '%s/android/countryzones.txt' % timezone_input_data_dir
   tzlookup_dest_file = '%s/android/tzlookup.xml' % timezone_output_data_dir
+  tzaliases_dest_file = '%s/tzaliases.txt' % tzalias_dest_dir
 
-  print('Calling TzLookupGenerator to create tzlookup.xml...')
+  print('Calling TzLookupGenerator to create tzlookup.xml / tzaliases.txt...')
   tzdatautil.InvokeSoong(android_build_top, ['tzlookup_generator'])
 
   zone_tab_file = '%s/zone.tab' % iana_data_dir
   backward_file = '%s/backward' % iana_data_dir
   command = '%s/bin/tzlookup_generator' % android_host_out
   subprocess.check_call([command, countryzones_source_file, zone_tab_file, backward_file,
-                         tzlookup_dest_file])
+                         tzlookup_dest_file, tzaliases_dest_file])
 
 
 def BuildTelephonylookup():
@@ -250,6 +252,7 @@
 def main():
   print('Source data file structure: %s' % timezone_input_data_dir)
   print('Source tools file structure: %s' % timezone_input_tools_dir)
+  print('Intermediate / working dir: %s' % tmp_dir)
   print('Output data file structure: %s' % timezone_output_data_dir)
 
   iana_input_data_dir = '%s/iana' % timezone_input_data_dir
@@ -268,7 +271,11 @@
   iana_data_dir = '%s/iana_data' % tmp_dir
   ExtractTarFile(iana_data_tar_file, iana_data_dir)
   BuildTzdata(zic_binary_file, iana_data_dir, iana_data_version)
-  BuildTzlookup(iana_data_dir)
+
+  tzaliases_out_dir = '%s/android_intermediates' % tmp_dir
+  os.mkdir(tzaliases_out_dir)
+  BuildTzlookupAndTzAliases(iana_data_dir, tzaliases_out_dir)
+
   BuildTelephonylookup()
 
   # Create a distro file and version file from the output from prior stages.