Add tzdata debug tooling

Add tzdata debug tooling to help when investigating tzdata
issues in future (and upgrades to zic, extension to use v2 data, etc.).

In this change I also move ZoneCompactor.java and associated files under
input_tools/android as things are getting a bit crowded under
system/timezone.

Bug: 73719425
Test: Ran update-tzdata.py / dump-tzdata.py
Change-Id: I2a700438c1fe5c7654008d1bc0c5f9be7cecba0d
diff --git a/debug_tools/host/Android.bp b/debug_tools/host/Android.bp
new file mode 100644
index 0000000..c07e062
--- /dev/null
+++ b/debug_tools/host/Android.bp
@@ -0,0 +1,20 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+    name: "timezone_host_debug_tools",
+    srcs: ["main/java/**/*.java"],
+    static_libs: ["guava"],
+}
+
diff --git a/debug_tools/host/README.android b/debug_tools/host/README.android
new file mode 100644
index 0000000..7ea52ef
--- /dev/null
+++ b/debug_tools/host/README.android
@@ -0,0 +1,11 @@
+This directory contains tooling to help when debugging time zone issues on
+Android.
+
+dump-tzdata.py
+ - A tool that takes a tzdata file and splits it into component tzfiles,
+   zone.tab, etc. Run it with --help for usage. The individual tzfiles can
+   be inspected with tools like zdump, for example with "zdump -V <tzfile>"
+
+   It also dumps human-readable CSV files of the (v1) content currently used by
+   Android's ZoneInfo class. These can be inspected and compared with dumps from
+   other tzdata files easily using your favourite text diffing tool.
diff --git a/debug_tools/host/dump-tzdata.py b/debug_tools/host/dump-tzdata.py
new file mode 100755
index 0000000..30cc9ac
--- /dev/null
+++ b/debug_tools/host/dump-tzdata.py
@@ -0,0 +1,97 @@
+#!/usr/bin/python -B
+
+# Copyright 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Dumps the contents of a tzdata file."""
+
+from __future__ import print_function
+
+import argparse
+import os
+import subprocess
+import sys
+
+sys.path.append('%s/external/icu/tools' % os.environ.get('ANDROID_BUILD_TOP'))
+import i18nutil
+
+
+# Calculate the paths that are referred to by multiple functions.
+android_build_top = i18nutil.GetAndroidRootOrDie()
+timezone_dir = os.path.realpath('%s/system/timezone' % android_build_top)
+i18nutil.CheckDirExists(timezone_dir, 'system/timezone')
+
+android_host_out = i18nutil.GetAndroidHostOutOrDie()
+
+debug_tools_dir = os.path.realpath('%s/system/timezone/debug_tools/host' % android_build_top)
+i18nutil.CheckDirExists(debug_tools_dir, 'system/timezone/debug_tools/host')
+
+
+def BuildDebugTools():
+  subprocess.check_call(['make', '-C', android_build_top, '-j30', 'timezone_host_debug_tools'])
+
+
+def SplitTzData(tzdata_file, output_dir):
+  jar_file = '%s/framework/timezone_host_debug_tools.jar' % android_host_out
+  subprocess.check_call(['java', '-cp', jar_file, 'ZoneSplitter', tzdata_file, output_dir])
+
+
+def CreateCsvFiles(zones_dir, csvs_dir):
+  jar_file = '%s/framework/timezone_host_debug_tools.jar' % android_host_out
+  subprocess.check_call(['java', '-cp', jar_file, 'TzFileDumper', zones_dir, csvs_dir])
+
+
+def CheckFileExists(file, filename):
+  if not os.path.isfile(file):
+    print("Couldn't find %s (%s)!" % (filename, file))
+    sys.exit(1)
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument('-tzdata', required=True,
+      help='The tzdata file to process')
+  parser.add_argument('-output', required=True,
+      help='The output directory for the dump')
+  args = parser.parse_args()
+
+  tzdata_file = args.tzdata
+  output_dir = args.output
+
+  CheckFileExists(tzdata_file, '-tzdata')
+  if not os.path.exists(output_dir):
+    print('Creating dir: %s'  % output_dir)
+    os.mkdir(output_dir)
+  i18nutil.CheckDirExists(output_dir, '-output')
+
+  BuildDebugTools()
+
+  SplitTzData(tzdata_file, output_dir)
+
+  zones_dir = '%s/zones' % output_dir
+  csvs_dir = '%s/csvs' % output_dir
+
+  i18nutil.CheckDirExists(zones_dir, 'zones output dir')
+  if not os.path.exists(csvs_dir):
+    os.mkdir(csvs_dir)
+
+  CreateCsvFiles(zones_dir, csvs_dir)
+
+  print('Look in %s for all extracted files' % output_dir)
+  print('Look in %s for dumped CSVs' % csvs_dir)
+  sys.exit(0)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/debug_tools/host/main/java/TzFileDumper.java b/debug_tools/host/main/java/TzFileDumper.java
new file mode 100644
index 0000000..e7d67e4
--- /dev/null
+++ b/debug_tools/host/main/java/TzFileDumper.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import com.google.common.base.Joiner;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.MappedByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Dumps out the contents of a tzfile (v1 format data only) in a CSV form.
+ *
+ * <p>This class contains a copy of logic found in Android's ZoneInfo.
+ */
+public class TzFileDumper {
+
+    public static void main(String[] args) throws Exception {
+        if (args.length != 2) {
+            System.err.println("usage: java TzFileDumper <tzfile|dir> <output file|output dir>");
+            System.exit(0);
+        }
+
+        File input = new File(args[0]);
+        File output = new File(args[1]);
+        if (input.isDirectory()) {
+            if (!output.isDirectory()) {
+                System.err.println("If first args is a directory, second arg must be a directory");
+                System.exit(1);
+            }
+
+            for (File inputFile : input.listFiles()) {
+                if (inputFile.isFile()) {
+                    File outputFile = new File(output, inputFile.getName() + ".csv");
+                    try {
+                        new TzFileDumper(inputFile, outputFile).execute();
+                    } catch (IOException e) {
+                        System.err.println("Error processing:" + inputFile);
+                    }
+                }
+            }
+        } else {
+            if (!output.isFile()) {
+                System.err.println("If first args is a file, second arg must be a file");
+                System.exit(1);
+            }
+            new TzFileDumper(input, output).execute();
+        }
+    }
+
+    private final File inputFile;
+    private final File outputFile;
+
+    private TzFileDumper(File inputFile, File outputFile) {
+        this.inputFile = inputFile;
+        this.outputFile = outputFile;
+    }
+
+    private void execute() throws IOException {
+        System.out.println("Dumping " + inputFile + " to " + outputFile);
+        MappedByteBuffer mappedTzFile = ZoneSplitter.createMappedByteBuffer(inputFile);
+
+        // Variable names beginning tzh_ correspond to those in "tzfile.h".
+        // Check tzh_magic.
+        int tzh_magic = mappedTzFile.getInt();
+        if (tzh_magic != 0x545a6966) { // "TZif"
+            throw new IOException("File=" + inputFile + " has an invalid header=" + tzh_magic);
+        }
+        // Skip the uninteresting part of the header.
+        mappedTzFile.position(mappedTzFile.position() + 28);
+
+        // Read the sizes of the arrays we're about to read.
+        int tzh_timecnt = mappedTzFile.getInt();
+        // Arbitrary ceiling to prevent allocating memory for corrupt data.
+        // 2 per year with 2^32 seconds would give ~272 transitions.
+        final int MAX_TRANSITIONS = 2000;
+        if (tzh_timecnt < 0 || tzh_timecnt > MAX_TRANSITIONS) {
+            throw new IOException(
+                    "File=" + inputFile + " has an invalid number of transitions=" + tzh_timecnt);
+        }
+
+        int tzh_typecnt = mappedTzFile.getInt();
+        final int MAX_TYPES = 256;
+        if (tzh_typecnt < 1) {
+            throw new IOException("ZoneInfo requires at least one type to be provided for each"
+                    + " timezone but could not find one for '" + inputFile + "'");
+        } else if (tzh_typecnt > MAX_TYPES) {
+            throw new IOException(
+                    "File=" + inputFile + " has too many types=" + tzh_typecnt);
+        }
+
+        mappedTzFile.getInt(); // Skip tzh_charcnt.
+
+        List<Transition> v1Transitions = readV1Transitions(mappedTzFile, tzh_timecnt, tzh_typecnt);
+        List<Type> v1Types = readTypes(mappedTzFile, tzh_typecnt);
+
+        try (Writer fileWriter = new OutputStreamWriter(
+                new FileOutputStream(outputFile), StandardCharsets.UTF_8)) {
+            writeCsvRow(fileWriter, "V1");
+            writeCsvRow(fileWriter);
+            writeTypes(v1Types, fileWriter);
+            writeCsvRow(fileWriter);
+            writeTransitions(v1Transitions, v1Types, fileWriter);
+        }
+    }
+
+    private List<Transition> readV1Transitions(MappedByteBuffer mappedTzFile, int transitionCount,
+            int typeCount) throws IOException {
+        int[] transitionTimes = new int[transitionCount];
+        byte[] typeIndexes = new byte[transitionCount];
+
+        // Read the data.
+        fillIntArray(mappedTzFile, transitionTimes);
+        mappedTzFile.get(typeIndexes);
+
+        // Validate and construct the CSV rows.
+        List<Transition> transitions = new ArrayList<>();
+        for (int i = 0; i < transitionCount; ++i) {
+            if (i > 0 && transitionTimes[i] <= transitionTimes[i - 1]) {
+                throw new IOException(
+                        inputFile + " transition at " + i + " is not sorted correctly, is "
+                                + transitionTimes[i] + ", previous is " + transitionTimes[i - 1]);
+            }
+
+            int typeIndex = typeIndexes[i] & 0xff;
+            if (typeIndex >= typeCount) {
+                throw new IOException(inputFile + " type at " + i + " is not < " + typeCount
+                        + ", is " + typeIndex);
+            }
+
+            Transition transition = new Transition(transitionTimes[i], typeIndex);
+            transitions.add(transition);
+        }
+
+        return transitions;
+    }
+
+    private void writeTransitions(List<Transition> transitions, List<Type> types, Writer fileWriter)
+            throws IOException {
+
+        List<Object[]> rows = new ArrayList<>();
+        for (Transition transition : transitions) {
+            Type type = types.get(transition.typeIndex);
+            Object[] row = new Object[] {
+                    transition.transitionTimeSeconds,
+                    transition.typeIndex,
+                    formatTimeSeconds(transition.transitionTimeSeconds),
+                    formatDurationSeconds(type.gmtOffsetSeconds),
+                    formatIsDst(type.isDst),
+            };
+            rows.add(row);
+        }
+
+        writeCsvRow(fileWriter, "Transitions");
+        writeTuplesCsv(fileWriter, rows, "transition", "type", "[UTC time]", "[Type offset]",
+                "[Type isDST]");
+    }
+
+    private List<Type> readTypes(MappedByteBuffer mappedTzFile, int typeCount)
+            throws IOException {
+
+        List<Type> types = new ArrayList<>();
+        for (int i = 0; i < typeCount; ++i) {
+            int gmtOffsetSeconds = mappedTzFile.getInt();
+            byte isDst = mappedTzFile.get();
+            if (isDst != 0 && isDst != 1) {
+                throw new IOException(inputFile + " dst at " + i + " is not 0 or 1, is " + isDst);
+            }
+
+            // We skip the abbreviation index.
+            mappedTzFile.get();
+
+            types.add(new Type(gmtOffsetSeconds, isDst));
+        }
+
+        return types;
+    }
+
+    private void writeTypes(List<Type> types, Writer fileWriter) throws IOException {
+
+        List<Object[]> rows = new ArrayList<>();
+        for (Type type : types) {
+            Object[] row = new Object[] {
+                    type.gmtOffsetSeconds,
+                    type.isDst,
+                    formatDurationSeconds(type.gmtOffsetSeconds),
+                    formatIsDst(type.isDst),
+            };
+            rows.add(row);
+        }
+
+        writeCsvRow(fileWriter, "Types");
+        writeTuplesCsv(
+                fileWriter, rows, "gmtOffset (seconds)", "isDst", "[gmtOffset ISO]", "[DST?]");
+    }
+
+    private static void fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill) {
+        for (int i = 0; i < toFill.length; i++) {
+            toFill[i] = mappedByteBuffer.getInt();
+        }
+    }
+
+    private static String formatTimeSeconds(long timeInSeconds) {
+        long timeInMillis = timeInSeconds * 1000L;
+        return Instant.ofEpochMilli(timeInMillis).toString();
+    }
+
+    private static String formatDurationSeconds(int duration) {
+        return Duration.ofSeconds(duration).toString();
+    }
+
+    private String formatIsDst(byte isDst) {
+        return isDst == 0 ? "STD" : "DST";
+    }
+
+    private static void writeCsvRow(Writer writer, Object... values) throws IOException {
+        writer.append(Joiner.on(',').join(values));
+        writer.append('\n');
+    }
+
+    private static void writeTuplesCsv(Writer writer, List<Object[]> lines, String... headings)
+            throws IOException {
+
+        writeCsvRow(writer, (Object[]) headings);
+        for (Object[] line : lines) {
+            writeCsvRow(writer, line);
+        }
+    }
+
+    private static class Type {
+
+        final int gmtOffsetSeconds;
+        final byte isDst;
+
+        Type(int gmtOffsetSeconds, byte isDst) {
+            this.gmtOffsetSeconds = gmtOffsetSeconds;
+            this.isDst = isDst;
+        }
+    }
+
+    private static class Transition {
+
+        final long transitionTimeSeconds;
+        final int typeIndex;
+
+        Transition(long transitionTimeSeconds, int typeIndex) {
+            this.transitionTimeSeconds = transitionTimeSeconds;
+            this.typeIndex = typeIndex;
+        }
+    }
+}
diff --git a/debug_tools/host/main/java/ZoneSplitter.java b/debug_tools/host/main/java/ZoneSplitter.java
new file mode 100644
index 0000000..3dd30ff
--- /dev/null
+++ b/debug_tools/host/main/java/ZoneSplitter.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
+
+/**
+ * Reverses the ZoneCompactor process to extract information and zic output files from Android's
+ * tzdata file. This enables easier debugging / inspection of Android's tzdata file with standard
+ * tools like zdump or Android tools like TzFileDumper.
+ *
+ * <p>This class contains a copy of logic found in Android's ZoneInfoDB.
+ */
+public class ZoneSplitter {
+
+    public static void main(String[] args) throws Exception {
+        if (args.length != 2) {
+            System.err.println("usage: java ZoneSplitter <tzdata file> <output directory>");
+            System.exit(0);
+        }
+        new ZoneSplitter(args[0], args[1]).execute();
+    }
+
+    private final File tzData;
+    private final File outputDir;
+
+    private ZoneSplitter(String tzData, String outputDir) {
+        this.tzData = new File(tzData);
+        this.outputDir = new File(outputDir);
+    }
+
+    private void execute() throws IOException {
+        if (!(tzData.exists() && tzData.isFile() && tzData.canRead())) {
+            throw new IOException(tzData + " not found or is not readable");
+        }
+        if (!(outputDir.exists() && outputDir.isDirectory())) {
+            throw new IOException(outputDir + " not found or is not a directory");
+        }
+
+        MappedByteBuffer mappedFile = createMappedByteBuffer(tzData);
+
+        // byte[12] tzdata_version  -- "tzdata2012f\0"
+        // int index_offset
+        // int data_offset
+        // int zonetab_offset
+        writeVersionFile(mappedFile, outputDir);
+
+        final int fileSize = (int) tzData.length();
+        int index_offset = mappedFile.getInt();
+        validateOffset(index_offset, fileSize);
+        int data_offset = mappedFile.getInt();
+        validateOffset(data_offset, fileSize);
+        int zonetab_offset = mappedFile.getInt();
+        validateOffset(zonetab_offset, fileSize);
+
+        if (index_offset >= data_offset || data_offset >= zonetab_offset) {
+            throw new IOException("Invalid offset: index_offset=" + index_offset
+                    + ", data_offset=" + data_offset + ", zonetab_offset=" + zonetab_offset
+                    + ", fileSize=" + fileSize);
+        }
+
+        File zicFilesDir = new File(outputDir, "zones");
+        zicFilesDir.mkdir();
+        extractZicFiles(mappedFile, index_offset, data_offset, zicFilesDir);
+
+        writeZoneTabFile(mappedFile, zonetab_offset, fileSize - zonetab_offset, outputDir);
+    }
+
+    static MappedByteBuffer createMappedByteBuffer(File tzData) throws IOException {
+        MappedByteBuffer mappedFile;
+        RandomAccessFile file = new RandomAccessFile(tzData, "r");
+        try (FileChannel fileChannel = file.getChannel()) {
+            mappedFile = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
+        }
+        mappedFile.load();
+        return mappedFile;
+    }
+
+    private static void validateOffset(int offset, int size) throws IOException {
+        if (offset < 0 || offset >= size) {
+            throw new IOException("Invalid offset=" + offset + ", size=" + size);
+        }
+    }
+
+    private static void writeVersionFile(MappedByteBuffer mappedFile, File targetDir)
+            throws IOException {
+
+        byte[] tzdata_version = new byte[12];
+        mappedFile.get(tzdata_version);
+
+        String magic = new String(tzdata_version, 0, 6, StandardCharsets.US_ASCII);
+        if (!magic.startsWith("tzdata") || tzdata_version[11] != 0) {
+            throw new IOException("bad tzdata magic: " + Arrays.toString(tzdata_version));
+        }
+        writeStringUtf8ToFile(new File(targetDir, "version"),
+                new String(tzdata_version, 6, 5, StandardCharsets.US_ASCII));
+    }
+
+    private static void extractZicFiles(MappedByteBuffer mappedFile, int indexOffset,
+            int dataOffset, File outputDir) throws IOException {
+
+        mappedFile.position(indexOffset);
+
+        // The index of the tzdata file is made up of entries for each time zone ID which describe
+        // the location of the associated zic data in the data section of the file. The index
+        // section has no padding so we can determine the number of entries from the size.
+        //
+        // Each index entry consists of:
+        // byte[MAXNAME] idBytes - the id string, \0 terminated. e.g. "America/New_York\0"
+        // int32 byteOffset      - the offset of the start of the zic data relative to the start of
+        //                         the tzdata data section
+        // int32 length          - the length of the of the zic data
+        // int32 unused          - no longer used
+        final int MAXNAME = 40;
+        final int SIZEOF_OFFSET = 4;
+        final int SIZEOF_INDEX_ENTRY = MAXNAME + 3 * SIZEOF_OFFSET;
+
+        int indexSize = (dataOffset - indexOffset);
+        if (indexSize % SIZEOF_INDEX_ENTRY != 0) {
+            throw new IOException("Index size is not divisible by " + SIZEOF_INDEX_ENTRY
+                    + ", indexSize=" + indexSize);
+        }
+
+        byte[] idBytes = new byte[MAXNAME];
+        int entryCount = indexSize / SIZEOF_INDEX_ENTRY;
+        int[] byteOffsets = new int[entryCount];
+        int[] lengths = new int[entryCount];
+        String[] ids = new String[entryCount];
+
+        for (int i = 0; i < entryCount; i++) {
+            // Read the fixed length timezone ID.
+            mappedFile.get(idBytes, 0, idBytes.length);
+
+            // Read the offset into the file where the data for ID can be found.
+            byteOffsets[i] = mappedFile.getInt();
+            byteOffsets[i] += dataOffset;
+
+            lengths[i] = mappedFile.getInt();
+            if (lengths[i] < 44) {
+                throw new IOException("length in index file < sizeof(tzhead)");
+            }
+            mappedFile.getInt(); // Skip the unused 4 bytes that used to be the raw offset.
+
+            // Calculate the true length of the ID.
+            int len = 0;
+            while (len < idBytes.length && idBytes[len] != 0) {
+                len++;
+            }
+            if (len == 0) {
+                throw new IOException("Invalid ID at index=" + i);
+            }
+            ids[i] = new String(idBytes, 0, len, StandardCharsets.US_ASCII);
+            if (i > 0) {
+                if (ids[i].compareTo(ids[i - 1]) <= 0) {
+                    throw new IOException(
+                            "Index not sorted or contains multiple entries with the same ID"
+                            + ", index=" + i + ", ids[i]=" + ids[i] + ", ids[i - 1]=" + ids[i - 1]);
+                }
+            }
+        }
+        for (int i = 0; i < entryCount; i++) {
+            String id = ids[i];
+            int byteOffset = byteOffsets[i];
+            int length = lengths[i];
+
+            File subFile = new File(outputDir, id.replace('/', '_'));
+            mappedFile.position(byteOffset);
+            byte[] bytes = new byte[length];
+            mappedFile.get(bytes, 0, length);
+
+            writeBytesToFile(subFile, bytes);
+        }
+    }
+
+    private static void writeZoneTabFile(MappedByteBuffer mappedFile,
+            int zoneTabOffset, int zoneTabSize, File outputDir) throws IOException {
+        byte[] bytes = new byte[zoneTabSize];
+        mappedFile.position(zoneTabOffset);
+        mappedFile.get(bytes, 0, bytes.length);
+        writeBytesToFile(new File(outputDir, "zone.tab"), bytes);
+    }
+
+    private static void writeStringUtf8ToFile(File file, String string) throws IOException {
+        writeBytesToFile(file, string.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static void writeBytesToFile(File file, byte[] bytes) throws IOException {
+        System.out.println("Writing: " + file);
+        Files.write(file.toPath(), bytes, StandardOpenOption.CREATE);
+    }
+}
diff --git a/zone_compactor/Android.bp b/input_tools/android/Android.bp
similarity index 100%
rename from zone_compactor/Android.bp
rename to input_tools/android/Android.bp
diff --git a/zone_compactor/main/java/ZoneCompactor.java b/input_tools/android/main/java/ZoneCompactor.java
similarity index 100%
rename from zone_compactor/main/java/ZoneCompactor.java
rename to input_tools/android/main/java/ZoneCompactor.java
diff --git a/zone_compactor/main/manifest/MANIFEST.mf b/input_tools/android/main/manifest/MANIFEST.mf
similarity index 100%
rename from zone_compactor/main/manifest/MANIFEST.mf
rename to input_tools/android/main/manifest/MANIFEST.mf
diff --git a/update-tzdata.py b/update-tzdata.py
index 0ad3af1..71618fd 100755
--- a/update-tzdata.py
+++ b/update-tzdata.py
@@ -39,8 +39,8 @@
 
 android_host_out = i18nutil.GetAndroidHostOutOrDie()
 
-zone_compactor_dir = os.path.realpath('%s/system/timezone/zone_compactor' % android_build_top)
-i18nutil.CheckDirExists(timezone_dir, 'system/timezone/zone_zompactor')
+zone_compactor_dir = os.path.realpath('%s/system/timezone/input_tools/android' % android_build_top)
+i18nutil.CheckDirExists(zone_compactor_dir, 'system/timezone/input_tools/android')
 
 timezone_input_tools_dir = os.path.realpath('%s/input_tools' % timezone_dir)
 timezone_input_data_dir = os.path.realpath('%s/input_data' % timezone_dir)