merge in oc-release history after reset to oc-dev
diff --git a/ojluni/src/main/java/java/util/Hashtable.java b/ojluni/src/main/java/java/util/Hashtable.java
index a07569c..6061290 100644
--- a/ojluni/src/main/java/java/util/Hashtable.java
+++ b/ojluni/src/main/java/java/util/Hashtable.java
@@ -861,6 +861,12 @@
         return h;
     }
 
+    @Override
+    public synchronized V getOrDefault(Object key, V defaultValue) {
+        V result = get(key);
+        return (null == result) ? defaultValue : result;
+    }
+
     @SuppressWarnings("unchecked")
     @Override
     public synchronized void forEach(BiConsumer<? super K, ? super V> action) {
@@ -902,57 +908,216 @@
         }
     }
 
-    // BEGIN Android-changed: Just wrap synchronization around Map.super implementations.
-    // Upstream uses different overridden implementations.
-    @Override
-    public synchronized V getOrDefault(Object key, V defaultValue) {
-        return Map.super.getOrDefault(key, defaultValue);
-    }
-
     @Override
     public synchronized V putIfAbsent(K key, V value) {
-        return Map.super.putIfAbsent(key, value);
+        Objects.requireNonNull(value);
+
+        // Makes sure the key is not already in the hashtable.
+        HashtableEntry<?,?> tab[] = table;
+        int hash = key.hashCode();
+        int index = (hash & 0x7FFFFFFF) % tab.length;
+        @SuppressWarnings("unchecked")
+        HashtableEntry<K,V> entry = (HashtableEntry<K,V>)tab[index];
+        for (; entry != null; entry = entry.next) {
+            if ((entry.hash == hash) && entry.key.equals(key)) {
+                V old = entry.value;
+                if (old == null) {
+                    entry.value = value;
+                }
+                return old;
+            }
+        }
+
+        addEntry(hash, key, value, index);
+        return null;
     }
 
     @Override
     public synchronized boolean remove(Object key, Object value) {
-       return Map.super.remove(key, value);
+        Objects.requireNonNull(value);
+
+        HashtableEntry<?,?> tab[] = table;
+        int hash = key.hashCode();
+        int index = (hash & 0x7FFFFFFF) % tab.length;
+        @SuppressWarnings("unchecked")
+        HashtableEntry<K,V> e = (HashtableEntry<K,V>)tab[index];
+        for (HashtableEntry<K,V> prev = null; e != null; prev = e, e = e.next) {
+            if ((e.hash == hash) && e.key.equals(key) && e.value.equals(value)) {
+                modCount++;
+                if (prev != null) {
+                    prev.next = e.next;
+                } else {
+                    tab[index] = e.next;
+                }
+                count--;
+                e.value = null;
+                return true;
+            }
+        }
+        return false;
     }
 
     @Override
     public synchronized boolean replace(K key, V oldValue, V newValue) {
-        return Map.super.replace(key, oldValue, newValue);
+        Objects.requireNonNull(oldValue);
+        Objects.requireNonNull(newValue);
+        HashtableEntry<?,?> tab[] = table;
+        int hash = key.hashCode();
+        int index = (hash & 0x7FFFFFFF) % tab.length;
+        @SuppressWarnings("unchecked")
+        HashtableEntry<K,V> e = (HashtableEntry<K,V>)tab[index];
+        for (; e != null; e = e.next) {
+            if ((e.hash == hash) && e.key.equals(key)) {
+                if (e.value.equals(oldValue)) {
+                    e.value = newValue;
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+        }
+        return false;
     }
 
     @Override
     public synchronized V replace(K key, V value) {
-        return Map.super.replace(key, value);
+        Objects.requireNonNull(value);
+        HashtableEntry<?,?> tab[] = table;
+        int hash = key.hashCode();
+        int index = (hash & 0x7FFFFFFF) % tab.length;
+        @SuppressWarnings("unchecked")
+        HashtableEntry<K,V> e = (HashtableEntry<K,V>)tab[index];
+        for (; e != null; e = e.next) {
+            if ((e.hash == hash) && e.key.equals(key)) {
+                V oldValue = e.value;
+                e.value = value;
+                return oldValue;
+            }
+        }
+        return null;
     }
 
     @Override
-    public synchronized V computeIfAbsent(K key, Function<? super K,
-            ? extends V> mappingFunction) {
-        return Map.super.computeIfAbsent(key, mappingFunction);
+    public synchronized V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
+        Objects.requireNonNull(mappingFunction);
+
+        HashtableEntry<?,?> tab[] = table;
+        int hash = key.hashCode();
+        int index = (hash & 0x7FFFFFFF) % tab.length;
+        @SuppressWarnings("unchecked")
+        HashtableEntry<K,V> e = (HashtableEntry<K,V>)tab[index];
+        for (; e != null; e = e.next) {
+            if (e.hash == hash && e.key.equals(key)) {
+                // Hashtable not accept null value
+                return e.value;
+            }
+        }
+
+        V newValue = mappingFunction.apply(key);
+        if (newValue != null) {
+            addEntry(hash, key, newValue, index);
+        }
+
+        return newValue;
     }
 
     @Override
-    public synchronized V computeIfPresent(K key, BiFunction<? super K,
-            ? super V, ? extends V> remappingFunction) {
-        return Map.super.computeIfPresent(key, remappingFunction);
+    public synchronized V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+        Objects.requireNonNull(remappingFunction);
+
+        HashtableEntry<?,?> tab[] = table;
+        int hash = key.hashCode();
+        int index = (hash & 0x7FFFFFFF) % tab.length;
+        @SuppressWarnings("unchecked")
+        HashtableEntry<K,V> e = (HashtableEntry<K,V>)tab[index];
+        for (HashtableEntry<K,V> prev = null; e != null; prev = e, e = e.next) {
+            if (e.hash == hash && e.key.equals(key)) {
+                V newValue = remappingFunction.apply(key, e.value);
+                if (newValue == null) {
+                    modCount++;
+                    if (prev != null) {
+                        prev.next = e.next;
+                    } else {
+                        tab[index] = e.next;
+                    }
+                    count--;
+                } else {
+                    e.value = newValue;
+                }
+                return newValue;
+            }
+        }
+        return null;
     }
 
     @Override
-    public synchronized V compute(K key, BiFunction<? super K, ? super V,
-            ? extends V> remappingFunction) {
-        return Map.super.compute(key, remappingFunction);
+    public synchronized V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+        Objects.requireNonNull(remappingFunction);
+
+        HashtableEntry<?,?> tab[] = table;
+        int hash = key.hashCode();
+        int index = (hash & 0x7FFFFFFF) % tab.length;
+        @SuppressWarnings("unchecked")
+        HashtableEntry<K,V> e = (HashtableEntry<K,V>)tab[index];
+        for (HashtableEntry<K,V> prev = null; e != null; prev = e, e = e.next) {
+            if (e.hash == hash && Objects.equals(e.key, key)) {
+                V newValue = remappingFunction.apply(key, e.value);
+                if (newValue == null) {
+                    modCount++;
+                    if (prev != null) {
+                        prev.next = e.next;
+                    } else {
+                        tab[index] = e.next;
+                    }
+                    count--;
+                } else {
+                    e.value = newValue;
+                }
+                return newValue;
+            }
+        }
+
+        V newValue = remappingFunction.apply(key, null);
+        if (newValue != null) {
+            addEntry(hash, key, newValue, index);
+        }
+
+        return newValue;
     }
 
     @Override
-    public synchronized V merge(K key, V value, BiFunction<? super V, ? super V,
-            ? extends V> remappingFunction) {
-        return Map.super.merge(key, value, remappingFunction);
+    public synchronized V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
+        Objects.requireNonNull(remappingFunction);
+
+        HashtableEntry<?,?> tab[] = table;
+        int hash = key.hashCode();
+        int index = (hash & 0x7FFFFFFF) % tab.length;
+        @SuppressWarnings("unchecked")
+        HashtableEntry<K,V> e = (HashtableEntry<K,V>)tab[index];
+        for (HashtableEntry<K,V> prev = null; e != null; prev = e, e = e.next) {
+            if (e.hash == hash && e.key.equals(key)) {
+                V newValue = remappingFunction.apply(e.value, value);
+                if (newValue == null) {
+                    modCount++;
+                    if (prev != null) {
+                        prev.next = e.next;
+                    } else {
+                        tab[index] = e.next;
+                    }
+                    count--;
+                } else {
+                    e.value = newValue;
+                }
+                return newValue;
+            }
+        }
+
+        if (value != null) {
+            addEntry(hash, key, value, index);
+        }
+
+        return value;
     }
-    // END Android-changed: Just add synchronization around Map's default implementations.
 
     /**
      * Save the state of the Hashtable to a stream (i.e., serialize it).
diff --git a/tzdata/shared2/src/main/libcore/tzdata/shared2/DistroVersion.java b/tzdata/shared2/src/main/libcore/tzdata/shared2/DistroVersion.java
index 3902d36..80f37bc 100644
--- a/tzdata/shared2/src/main/libcore/tzdata/shared2/DistroVersion.java
+++ b/tzdata/shared2/src/main/libcore/tzdata/shared2/DistroVersion.java
@@ -152,6 +152,15 @@
     }
 
     @Override
+    public int hashCode() {
+        int result = formatMajorVersion;
+        result = 31 * result + formatMinorVersion;
+        result = 31 * result + rulesVersion.hashCode();
+        result = 31 * result + revision;
+        return result;
+    }
+
+    @Override
     public String toString() {
         return "DistroVersion{" +
                 "formatMajorVersion=" + formatMajorVersion +
diff --git a/tzdata/shared2/src/main/libcore/tzdata/shared2/FileUtils.java b/tzdata/shared2/src/main/libcore/tzdata/shared2/FileUtils.java
index a170a9e..39626ac 100644
--- a/tzdata/shared2/src/main/libcore/tzdata/shared2/FileUtils.java
+++ b/tzdata/shared2/src/main/libcore/tzdata/shared2/FileUtils.java
@@ -17,6 +17,8 @@
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.LinkedList;
@@ -173,4 +175,14 @@
             return toReturn;
         }
     }
+
+    /**
+     * Creates an empty file.
+     *
+     * @param file the file to create
+     * @throws IOException if the file cannot be created
+     */
+    public static void createEmptyFile(File file) throws IOException {
+        new FileOutputStream(file, false /* append */).close();
+    }
 }
diff --git a/tzdata/shared2/src/main/libcore/tzdata/shared2/StagedDistroOperation.java b/tzdata/shared2/src/main/libcore/tzdata/shared2/StagedDistroOperation.java
new file mode 100644
index 0000000..0492e12
--- /dev/null
+++ b/tzdata/shared2/src/main/libcore/tzdata/shared2/StagedDistroOperation.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package libcore.tzdata.shared2;
+
+/**
+ * Information about a staged time zone distro operation.
+ */
+public class StagedDistroOperation {
+
+    private static final StagedDistroOperation UNINSTALL_STAGED =
+            new StagedDistroOperation(true /* isUninstall */, null /* stagedVersion */);
+
+    public final boolean isUninstall;
+    public final DistroVersion distroVersion;
+
+    private StagedDistroOperation(boolean isUninstall, DistroVersion distroVersion) {
+        this.isUninstall = isUninstall;
+        this.distroVersion = distroVersion;
+    }
+
+    public static StagedDistroOperation install(DistroVersion distroVersion) {
+        if (distroVersion == null) {
+            throw new NullPointerException("distroVersion==null");
+        }
+        return new StagedDistroOperation(false /* isUninstall */, distroVersion);
+    }
+
+    public static StagedDistroOperation uninstall() {
+        return UNINSTALL_STAGED;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        StagedDistroOperation that = (StagedDistroOperation) o;
+
+        if (isUninstall != that.isUninstall) {
+            return false;
+        }
+        return distroVersion != null ? distroVersion.equals(that.distroVersion)
+                : that.distroVersion == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (isUninstall ? 1 : 0);
+        result = 31 * result + (distroVersion != null ? distroVersion.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "StagedDistroOperation{" +
+                "isUninstall=" + isUninstall +
+                ", distroVersion=" + distroVersion +
+                '}';
+    }
+}
diff --git a/tzdata/shared2/src/test/libcore/tzdata/shared2/FileUtilsTest.java b/tzdata/shared2/src/test/libcore/tzdata/shared2/FileUtilsTest.java
index 3b9ec5b..87e6e6e 100644
--- a/tzdata/shared2/src/test/libcore/tzdata/shared2/FileUtilsTest.java
+++ b/tzdata/shared2/src/test/libcore/tzdata/shared2/FileUtilsTest.java
@@ -253,6 +253,43 @@
         assertTrue(Arrays.equals(contents, exhaustedFileRead));
     }
 
+    public void testCreateEmptyFile() throws Exception {
+        File dir = createTempDir();
+        File file = new File(dir, "one");
+        assertFalse(file.exists());
+        FileUtils.createEmptyFile(file);
+        assertTrue(file.exists());
+        assertEquals(0, file.length());
+    }
+
+    public void testCreateEmptyFile_isDir() throws Exception {
+        File dir = createTempDir();
+        assertTrue(dir.exists());
+        assertTrue(dir.isDirectory());
+
+        try {
+            FileUtils.createEmptyFile(dir);
+        } catch (FileNotFoundException expected) {
+        }
+        assertTrue(dir.exists());
+        assertTrue(dir.isDirectory());
+    }
+
+    public void testCreateEmptyFile_truncatesExisting() throws Exception {
+        File dir = createTempDir();
+        File file = new File(dir, "one");
+
+        try (FileOutputStream fos = new FileOutputStream(file)) {
+            fos.write(new byte[1000]);
+        }
+        assertTrue(file.exists());
+        assertEquals(1000, file.length());
+
+        FileUtils.createEmptyFile(file);
+        assertTrue(file.exists());
+        assertEquals(0, file.length());
+    }
+
     private File createFile(byte[] contents) throws IOException {
         File file = File.createTempFile(getClass().getSimpleName(), ".txt");
         try (FileOutputStream fos = new FileOutputStream(file)) {
diff --git a/tzdata/update2/src/main/libcore/tzdata/update2/TimeZoneDistroInstaller.java b/tzdata/update2/src/main/libcore/tzdata/update2/TimeZoneDistroInstaller.java
index bb49fe0..e1ed794 100644
--- a/tzdata/update2/src/main/libcore/tzdata/update2/TimeZoneDistroInstaller.java
+++ b/tzdata/update2/src/main/libcore/tzdata/update2/TimeZoneDistroInstaller.java
@@ -23,6 +23,7 @@
 import libcore.tzdata.shared2.DistroException;
 import libcore.tzdata.shared2.DistroVersion;
 import libcore.tzdata.shared2.FileUtils;
+import libcore.tzdata.shared2.StagedDistroOperation;
 import libcore.tzdata.shared2.TimeZoneDistro;
 import libcore.util.ZoneInfoDB;
 
@@ -31,38 +32,55 @@
  * testing. This class is not thread-safe: callers are expected to handle mutual exclusion.
  */
 public class TimeZoneDistroInstaller {
-    /** {@link #installWithErrorCode(byte[])} result code: Success. */
+    /** {@link #stageInstallWithErrorCode(byte[])} result code: Success. */
     public final static int INSTALL_SUCCESS = 0;
-    /** {@link #installWithErrorCode(byte[])} result code: Distro corrupt. */
+    /** {@link #stageInstallWithErrorCode(byte[])} result code: Distro corrupt. */
     public final static int INSTALL_FAIL_BAD_DISTRO_STRUCTURE = 1;
-    /** {@link #installWithErrorCode(byte[])} result code: Distro version incompatible. */
+    /** {@link #stageInstallWithErrorCode(byte[])} result code: Distro version incompatible. */
     public final static int INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION = 2;
-    /** {@link #installWithErrorCode(byte[])} result code: Distro rules too old for device. */
+    /** {@link #stageInstallWithErrorCode(byte[])} result code: Distro rules too old for device. */
     public final static int INSTALL_FAIL_RULES_TOO_OLD = 3;
-    /** {@link #installWithErrorCode(byte[])} result code: Distro content failed validation. */
+    /** {@link #stageInstallWithErrorCode(byte[])} result code: Distro content failed validation. */
     public final static int INSTALL_FAIL_VALIDATION_ERROR = 4;
 
+    // This constant must match one in system/core/tzdatacheck.cpp.
+    private static final String STAGED_TZ_DATA_DIR_NAME = "staged";
+    // This constant must match one in system/core/tzdatacheck.cpp.
     private static final String CURRENT_TZ_DATA_DIR_NAME = "current";
     private static final String WORKING_DIR_NAME = "working";
     private static final String OLD_TZ_DATA_DIR_NAME = "old";
 
+    /**
+     * The name of the file in the staged directory used to indicate a staged uninstallation.
+     */
+    // This constant must match one in system/core/tzdatacheck.cpp.
+    // VisibleForTesting.
+    public static final String UNINSTALL_TOMBSTONE_FILE_NAME = "STAGED_UNINSTALL_TOMBSTONE";
+
     private final String logTag;
     private final File systemTzDataFile;
-    private final File oldTzDataDir;
+    private final File oldStagedDataDir;
+    private final File stagedTzDataDir;
     private final File currentTzDataDir;
     private final File workingDir;
 
     public TimeZoneDistroInstaller(String logTag, File systemTzDataFile, File installDir) {
         this.logTag = logTag;
         this.systemTzDataFile = systemTzDataFile;
-        oldTzDataDir = new File(installDir, OLD_TZ_DATA_DIR_NAME);
+        oldStagedDataDir = new File(installDir, OLD_TZ_DATA_DIR_NAME);
+        stagedTzDataDir = new File(installDir, STAGED_TZ_DATA_DIR_NAME);
         currentTzDataDir = new File(installDir, CURRENT_TZ_DATA_DIR_NAME);
         workingDir = new File(installDir, WORKING_DIR_NAME);
     }
 
     // VisibleForTesting
-    File getOldTzDataDir() {
-        return oldTzDataDir;
+    File getOldStagedDataDir() {
+        return oldStagedDataDir;
+    }
+
+    // VisibleForTesting
+    File getStagedTzDataDir() {
+        return stagedTzDataDir;
     }
 
     // VisibleForTesting
@@ -76,34 +94,35 @@
     }
 
     /**
-     * Install the supplied content.
+     * Stage an install of the supplied content, to be installed the next time the device boots.
      *
-     * <p>Errors during unpacking or installation will throw an {@link IOException}.
+     * <p>Errors during unpacking or staging will throw an {@link IOException}.
      * If the distro content is invalid this method returns {@code false}.
      * If the installation completed successfully this method returns {@code true}.
      */
     public boolean install(byte[] content) throws IOException {
-        int result = installWithErrorCode(content);
+        int result = stageInstallWithErrorCode(content);
         return result == INSTALL_SUCCESS;
     }
 
     /**
-     * Install the supplied time zone distro.
+     * Stage an install of the supplied content, to be installed the next time the device boots.
      *
-     * <p>Errors during unpacking or installation will throw an {@link IOException}.
+     * <p>Errors during unpacking or staging will throw an {@link IOException}.
      * Returns {@link #INSTALL_SUCCESS} or an error code.
      */
-    public int installWithErrorCode(byte[] content) throws IOException {
-        if (oldTzDataDir.exists()) {
-            FileUtils.deleteRecursive(oldTzDataDir);
+    public int stageInstallWithErrorCode(byte[] content) throws IOException {
+        if (oldStagedDataDir.exists()) {
+            FileUtils.deleteRecursive(oldStagedDataDir);
         }
         if (workingDir.exists()) {
             FileUtils.deleteRecursive(workingDir);
         }
 
         Slog.i(logTag, "Unpacking / verifying time zone update");
-        unpackDistro(content, workingDir);
         try {
+            unpackDistro(content, workingDir);
+
             DistroVersion distroVersion;
             try {
                 distroVersion = readDistroVersion(workingDir);
@@ -151,52 +170,78 @@
             Slog.i(logTag, "Applying time zone update");
             FileUtils.makeDirectoryWorldAccessible(workingDir);
 
-            if (currentTzDataDir.exists()) {
-                Slog.i(logTag, "Moving " + currentTzDataDir + " to " + oldTzDataDir);
-                FileUtils.rename(currentTzDataDir, oldTzDataDir);
+            // Check if there is already a staged install or uninstall and remove it if there is.
+            if (!stagedTzDataDir.exists()) {
+                Slog.i(logTag, "Nothing to unstage at " + stagedTzDataDir);
+            } else {
+                Slog.i(logTag, "Moving " + stagedTzDataDir + " to " + oldStagedDataDir);
+                // Move stagedTzDataDir out of the way in one operation so we can't partially delete
+                // the contents.
+                FileUtils.rename(stagedTzDataDir, oldStagedDataDir);
             }
-            Slog.i(logTag, "Moving " + workingDir + " to " + currentTzDataDir);
-            FileUtils.rename(workingDir, currentTzDataDir);
-            Slog.i(logTag, "Update applied: " + currentTzDataDir + " successfully created");
+
+            // Move the workingDir to be the new staged directory.
+            Slog.i(logTag, "Moving " + workingDir + " to " + stagedTzDataDir);
+            FileUtils.rename(workingDir, stagedTzDataDir);
+            Slog.i(logTag, "Install staged: " + stagedTzDataDir + " successfully created");
             return INSTALL_SUCCESS;
         } finally {
-            deleteBestEffort(oldTzDataDir);
+            deleteBestEffort(oldStagedDataDir);
             deleteBestEffort(workingDir);
         }
     }
 
     /**
-     * Uninstall the current timezone update in /data, returning the device to using data from
-     * /system. Returns {@code true} if uninstallation was successful, {@code false} if there was
-     * nothing installed in /data to uninstall.
+     * Stage an uninstall of the current timezone update in /data which, on reboot, will return the
+     * device to using data from /system. Returns {@code true} if staging the uninstallation was
+     * successful, {@code false} if there was nothing installed in /data to uninstall. If there was
+     * something else staged it will be replaced by this call.
      *
      * <p>Errors encountered during uninstallation will throw an {@link IOException}.
      */
-    public boolean uninstall() throws IOException {
+    public boolean stageUninstall() throws IOException {
         Slog.i(logTag, "Uninstalling time zone update");
 
-        // Make sure we don't have a dir where we're going to move the currently installed data to.
-        if (oldTzDataDir.exists()) {
+        if (oldStagedDataDir.exists()) {
             // If we can't remove this, an exception is thrown and we don't continue.
-            FileUtils.deleteRecursive(oldTzDataDir);
+            FileUtils.deleteRecursive(oldStagedDataDir);
+        }
+        if (workingDir.exists()) {
+            FileUtils.deleteRecursive(workingDir);
         }
 
-        if (!currentTzDataDir.exists()) {
-            Slog.i(logTag, "Nothing to uninstall at " + currentTzDataDir);
-            return false;
+        try {
+            // Check if there is already an install or uninstall staged and remove it.
+            if (!stagedTzDataDir.exists()) {
+                Slog.i(logTag, "Nothing to unstage at " + stagedTzDataDir);
+            } else {
+                Slog.i(logTag, "Moving " + stagedTzDataDir + " to " + oldStagedDataDir);
+                // Move stagedTzDataDir out of the way in one operation so we can't partially delete
+                // the contents.
+                FileUtils.rename(stagedTzDataDir, oldStagedDataDir);
+            }
+
+            // If there's nothing actually installed, there's nothing to uninstall so no need to
+            // stage anything.
+            if (!currentTzDataDir.exists()) {
+                Slog.i(logTag, "Nothing to uninstall at " + currentTzDataDir);
+                return false;
+            }
+
+            // Stage an uninstall in workingDir.
+            FileUtils.ensureDirectoriesExist(workingDir, true /* makeWorldReadable */);
+            FileUtils.createEmptyFile(new File(workingDir, UNINSTALL_TOMBSTONE_FILE_NAME));
+
+            // Move the workingDir to be the new staged directory.
+            Slog.i(logTag, "Moving " + workingDir + " to " + stagedTzDataDir);
+            FileUtils.rename(workingDir, stagedTzDataDir);
+            Slog.i(logTag, "Uninstall staged: " + stagedTzDataDir + " successfully created");
+
+            return true;
+        } finally {
+            deleteBestEffort(oldStagedDataDir);
+            deleteBestEffort(workingDir);
         }
-
-        Slog.i(logTag, "Moving " + currentTzDataDir + " to " + oldTzDataDir);
-        // Move currentTzDataDir out of the way in one operation so we can't partially delete
-        // the contents, which would leave a partial install.
-        FileUtils.rename(currentTzDataDir, oldTzDataDir);
-
-        // Do our best to delete the now uninstalled timezone data.
-        deleteBestEffort(oldTzDataDir);
-
-        Slog.i(logTag, "Time zone update uninstalled.");
-
-        return true;
     }
 
     /**
@@ -214,6 +259,24 @@
     }
 
     /**
+     * Reads information about any currently staged distro operation. Returns {@code null} if there
+     * is no distro operation staged.
+     *
+     * @throws IOException if there was a problem reading data from /data
+     * @throws DistroException if there was a problem with the staged distro format/structure
+     */
+    public StagedDistroOperation getStagedDistroOperation() throws DistroException, IOException {
+        if (!stagedTzDataDir.exists()) {
+            return null;
+        }
+        if (new File(stagedTzDataDir, UNINSTALL_TOMBSTONE_FILE_NAME).exists()) {
+            return StagedDistroOperation.uninstall();
+        } else {
+            return StagedDistroOperation.install(readDistroVersion(stagedTzDataDir));
+        }
+    }
+
+    /**
      * Reads the timezone rules version present in /system. i.e. the version that would be present
      * after a factory reset.
      *
diff --git a/tzdata/update2/src/test/libcore/tzdata/update2/TimeZoneDistroInstallerTest.java b/tzdata/update2/src/test/libcore/tzdata/update2/TimeZoneDistroInstallerTest.java
index 325b605..cf40bf6 100644
--- a/tzdata/update2/src/test/libcore/tzdata/update2/TimeZoneDistroInstallerTest.java
+++ b/tzdata/update2/src/test/libcore/tzdata/update2/TimeZoneDistroInstallerTest.java
@@ -32,10 +32,13 @@
 import libcore.io.Streams;
 import libcore.tzdata.shared2.DistroVersion;
 import libcore.tzdata.shared2.FileUtils;
+import libcore.tzdata.shared2.StagedDistroOperation;
 import libcore.tzdata.shared2.TimeZoneDistro;
 import libcore.tzdata.testing.ZoneInfoTestHelper;
 import libcore.tzdata.update2.tools.TimeZoneDistroBuilder;
 
+import static org.junit.Assert.assertArrayEquals;
+
 /**
  * Tests for {@link TimeZoneDistroInstaller}.
  */
@@ -90,100 +93,110 @@
     }
 
     /** Tests the an update on a device will fail if the /system tzdata file cannot be found. */
-    public void testInstall_badSystemFile() throws Exception {
+    public void testStageInstallWithErrorCode_badSystemFile() throws Exception {
         File doesNotExist = new File(testSystemTzDataDir, "doesNotExist");
         TimeZoneDistroInstaller brokenSystemInstaller = new TimeZoneDistroInstaller(
                 "TimeZoneDistroInstallerTest", doesNotExist, testInstallDir);
         TimeZoneDistro tzData = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
 
         try {
-            brokenSystemInstaller.installWithErrorCode(tzData.getBytes());
+            brokenSystemInstaller.stageInstallWithErrorCode(tzData.getBytes());
             fail();
         } catch (IOException expected) {}
 
-        assertNoContentInstalled();
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
     }
 
     /** Tests the first successful update on a device */
-    public void testInstall_successfulFirstUpdate() throws Exception {
+    public void testStageInstallWithErrorCode_successfulFirstUpdate() throws Exception {
         TimeZoneDistro distro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
 
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_SUCCESS,
-                installer.installWithErrorCode(distro.getBytes()));
-        assertDistroInstalled(distro);
+                installer.stageInstallWithErrorCode(distro.getBytes()));
+        assertInstallDistroStaged(distro);
+        assertNoInstalledDistro();
     }
 
     /**
      * Tests we can install an update the same version as is in /system.
      */
-    public void testInstall_successfulFirstUpdate_sameVersionAsSystem() throws Exception {
+    public void testStageInstallWithErrorCode_successfulFirstUpdate_sameVersionAsSystem()
+            throws Exception {
         TimeZoneDistro distro = createValidTimeZoneDistro(SYSTEM_RULES_VERSION, 1);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_SUCCESS,
-                installer.installWithErrorCode(distro.getBytes()));
-        assertDistroInstalled(distro);
+                installer.stageInstallWithErrorCode(distro.getBytes()));
+        assertInstallDistroStaged(distro);
+        assertNoInstalledDistro();
     }
 
     /**
      * Tests we cannot install an update older than the version in /system.
      */
-    public void testInstall_unsuccessfulFirstUpdate_olderVersionThanSystem() throws Exception {
+    public void testStageInstallWithErrorCode_unsuccessfulFirstUpdate_olderVersionThanSystem()
+            throws Exception {
         TimeZoneDistro distro = createValidTimeZoneDistro(OLDER_RULES_VERSION, 1);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD,
-                installer.installWithErrorCode(distro.getBytes()));
-        assertNoContentInstalled();
+                installer.stageInstallWithErrorCode(distro.getBytes()));
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
     }
 
     /**
-     * Tests an update on a device when there is a prior update already applied.
+     * Tests an update on a device when there is a prior update already staged.
      */
-    public void testInstall_successfulFollowOnUpdate_newerVersion() throws Exception {
+    public void testStageInstallWithErrorCode_successfulFollowOnUpdate_newerVersion()
+            throws Exception {
         TimeZoneDistro distro1 = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_SUCCESS,
-                installer.installWithErrorCode(distro1.getBytes()));
-        assertDistroInstalled(distro1);
+                installer.stageInstallWithErrorCode(distro1.getBytes()));
+        assertInstallDistroStaged(distro1);
 
         TimeZoneDistro distro2 = createValidTimeZoneDistro(NEW_RULES_VERSION, 2);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_SUCCESS,
-                installer.installWithErrorCode(distro2.getBytes()));
-        assertDistroInstalled(distro2);
+                installer.stageInstallWithErrorCode(distro2.getBytes()));
+        assertInstallDistroStaged(distro2);
 
         TimeZoneDistro distro3 = createValidTimeZoneDistro(NEWER_RULES_VERSION, 1);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_SUCCESS,
-                installer.installWithErrorCode(distro3.getBytes()));
-        assertDistroInstalled(distro3);
+                installer.stageInstallWithErrorCode(distro3.getBytes()));
+        assertInstallDistroStaged(distro3);
+        assertNoInstalledDistro();
     }
 
     /**
      * Tests an update on a device when there is a prior update already applied, but the follow
      * on update is older than in /system.
      */
-    public void testInstall_unsuccessfulFollowOnUpdate_olderVersion() throws Exception {
+    public void testStageInstallWithErrorCode_unsuccessfulFollowOnUpdate_olderVersion()
+            throws Exception {
         TimeZoneDistro distro1 = createValidTimeZoneDistro(NEW_RULES_VERSION, 2);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_SUCCESS,
-                installer.installWithErrorCode(distro1.getBytes()));
-        assertDistroInstalled(distro1);
+                installer.stageInstallWithErrorCode(distro1.getBytes()));
+        assertInstallDistroStaged(distro1);
 
         TimeZoneDistro distro2 = createValidTimeZoneDistro(OLDER_RULES_VERSION, 1);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD,
-                installer.installWithErrorCode(distro2.getBytes()));
-        assertDistroInstalled(distro1);
+                installer.stageInstallWithErrorCode(distro2.getBytes()));
+        assertInstallDistroStaged(distro1);
+        assertNoInstalledDistro();
     }
 
     /** Tests that a distro with a missing file will not update the content. */
-    public void testInstall_missingTzDataFile() throws Exception {
-        TimeZoneDistro installedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
+    public void testStageInstallWithErrorCode_missingTzDataFile() throws Exception {
+        TimeZoneDistro stagedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_SUCCESS,
-                installer.installWithErrorCode(installedDistro.getBytes()));
-        assertDistroInstalled(installedDistro);
+                installer.stageInstallWithErrorCode(stagedDistro.getBytes()));
+        assertInstallDistroStaged(stagedDistro);
 
         TimeZoneDistro incompleteDistro =
                 createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
@@ -191,17 +204,18 @@
                         .buildUnvalidated();
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
-                installer.installWithErrorCode(incompleteDistro.getBytes()));
-        assertDistroInstalled(installedDistro);
+                installer.stageInstallWithErrorCode(incompleteDistro.getBytes()));
+        assertInstallDistroStaged(stagedDistro);
+        assertNoInstalledDistro();
     }
 
     /** Tests that a distro with a missing file will not update the content. */
-    public void testInstall_missingIcuFile() throws Exception {
-        TimeZoneDistro installedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
+    public void testStageInstallWithErrorCode_missingIcuFile() throws Exception {
+        TimeZoneDistro stagedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_SUCCESS,
-                installer.installWithErrorCode(installedDistro.getBytes()));
-        assertDistroInstalled(installedDistro);
+                installer.stageInstallWithErrorCode(stagedDistro.getBytes()));
+        assertInstallDistroStaged(stagedDistro);
 
         TimeZoneDistro incompleteDistro =
                 createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
@@ -209,14 +223,15 @@
                         .buildUnvalidated();
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
-                installer.installWithErrorCode(incompleteDistro.getBytes()));
-        assertDistroInstalled(installedDistro);
+                installer.stageInstallWithErrorCode(incompleteDistro.getBytes()));
+        assertInstallDistroStaged(stagedDistro);
+        assertNoInstalledDistro();
     }
 
     /**
      * Tests that an update will be unpacked even if there is a partial update from a previous run.
      */
-    public void testInstall_withWorkingDir() throws Exception {
+    public void testStageInstallWithErrorCode_withWorkingDir() throws Exception {
         File workingDir = installer.getWorkingDir();
         assertTrue(workingDir.mkdir());
         createFile(new File(workingDir, "myFile"), new byte[] { 'a' });
@@ -224,42 +239,45 @@
         TimeZoneDistro distro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_SUCCESS,
-                installer.installWithErrorCode(distro.getBytes()));
-        assertDistroInstalled(distro);
+                installer.stageInstallWithErrorCode(distro.getBytes()));
+        assertInstallDistroStaged(distro);
+        assertNoInstalledDistro();
     }
 
     /**
      * Tests that a distro without a distro version file will be rejected.
      */
-    public void testInstall_withMissingDistroVersionFile() throws Exception {
+    public void testStageInstallWithErrorCode_withMissingDistroVersionFile() throws Exception {
         // Create a distro without a version file.
         TimeZoneDistro distro = createValidTimeZoneDistroBuilder(NEW_RULES_VERSION, 1)
                 .clearVersionForTests()
                 .buildUnvalidated();
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
-                installer.installWithErrorCode(distro.getBytes()));
-        assertNoContentInstalled();
+                installer.stageInstallWithErrorCode(distro.getBytes()));
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
     }
 
     /**
      * Tests that a distro with an newer distro version will be rejected.
      */
-    public void testInstall_withNewerDistroVersion() throws Exception {
+    public void testStageInstallWithErrorCode_withNewerDistroVersion() throws Exception {
         // Create a distro that will appear to be newer than the one currently supported.
         TimeZoneDistro distro = createValidTimeZoneDistroBuilder(NEW_RULES_VERSION, 1)
                 .replaceFormatVersionForTests(2, 1)
                 .buildUnvalidated();
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION,
-                installer.installWithErrorCode(distro.getBytes()));
-        assertNoContentInstalled();
+                installer.stageInstallWithErrorCode(distro.getBytes()));
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
     }
 
     /**
      * Tests that a distro with a badly formed distro version will be rejected.
      */
-    public void testInstall_withBadlyFormedDistroVersion() throws Exception {
+    public void testStageInstallWithErrorCode_withBadlyFormedDistroVersion() throws Exception {
         // Create a distro that has an invalid major distro version. It should be 3 numeric
         // characters, "." and 3 more numeric characters.
         DistroVersion validDistroVersion = new DistroVersion(1, 1, NEW_RULES_VERSION, 1);
@@ -269,14 +287,15 @@
         TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidFormatVersionBytes);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
-                installer.installWithErrorCode(distro.getBytes()));
-        assertNoContentInstalled();
+                installer.stageInstallWithErrorCode(distro.getBytes()));
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
     }
 
     /**
      * Tests that a distro with a badly formed revision will be rejected.
      */
-    public void testInstall_withBadlyFormedRevision() throws Exception {
+    public void testStageInstallWithErrorCode_withBadlyFormedRevision() throws Exception {
         // Create a distro that has an invalid revision. It should be 3 numeric characters.
         DistroVersion validDistroVersion = new DistroVersion(1, 1, NEW_RULES_VERSION, 1);
         byte[] invalidRevisionBytes = validDistroVersion.toBytes();
@@ -285,14 +304,15 @@
         TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidRevisionBytes);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
-                installer.installWithErrorCode(distro.getBytes()));
-        assertNoContentInstalled();
+                installer.stageInstallWithErrorCode(distro.getBytes()));
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
     }
 
     /**
      * Tests that a distro with a badly formed rules version will be rejected.
      */
-    public void testInstall_withBadlyFormedRulesVersion() throws Exception {
+    public void testStageInstallWithErrorCode_withBadlyFormedRulesVersion() throws Exception {
         // Create a distro that has an invalid rules version. It should be in the form "2016c".
         DistroVersion validDistroVersion = new DistroVersion(1, 1, NEW_RULES_VERSION, 1);
         byte[] invalidRulesVersionBytes = validDistroVersion.toBytes();
@@ -301,38 +321,124 @@
         TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidRulesVersionBytes);
         assertEquals(
                 TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
-                installer.installWithErrorCode(distro.getBytes()));
-        assertNoContentInstalled();
+                installer.stageInstallWithErrorCode(distro.getBytes()));
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
     }
 
-    public void testUninstall_noExistingDataDistro() throws Exception {
-        assertFalse(installer.uninstall());
-        assertNoContentInstalled();
+    public void testStageUninstall_noExistingDistro() throws Exception {
+        // To stage an uninstall, there would need to be installed rules.
+        assertFalse(installer.stageUninstall());
+
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
     }
 
-    public void testUninstall_existingDataDistro() throws Exception {
-        File currentDataDir = installer.getCurrentTzDataDir();
-        assertTrue(currentDataDir.mkdir());
+    public void testStageUninstall_existingStagedDataDistro() throws Exception {
+        // To stage an uninstall, we need to have some installed rules.
+        TimeZoneDistro installedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
+        simulateInstalledDistro(installedDistro);
 
-        assertTrue(installer.uninstall());
-        assertNoContentInstalled();
+        File stagedDataDir = installer.getStagedTzDataDir();
+        assertTrue(stagedDataDir.mkdir());
+
+        assertTrue(installer.stageUninstall());
+        assertDistroUninstallStaged();
+        assertInstalledDistro(installedDistro);
     }
 
-    public void testUninstall_oldDirsAlreadyExists() throws Exception {
-        File oldTzDataDir = installer.getOldTzDataDir();
-        assertTrue(oldTzDataDir.mkdir());
+    public void testStageUninstall_oldDirsAlreadyExists() throws Exception {
+        // To stage an uninstall, we need to have some installed rules.
+        TimeZoneDistro installedDistro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
+        simulateInstalledDistro(installedDistro);
 
-        File currentDataDir = installer.getCurrentTzDataDir();
-        assertTrue(currentDataDir.mkdir());
+        File oldStagedDataDir = installer.getOldStagedDataDir();
+        assertTrue(oldStagedDataDir.mkdir());
 
-        assertTrue(installer.uninstall());
-        assertNoContentInstalled();
+        File workingDir = installer.getWorkingDir();
+        assertTrue(workingDir.mkdir());
+
+        assertTrue(installer.stageUninstall());
+
+        assertDistroUninstallStaged();
+        assertFalse(workingDir.exists());
+        assertFalse(oldStagedDataDir.exists());
+        assertInstalledDistro(installedDistro);
     }
 
     public void testGetSystemRulesVersion() throws Exception {
         assertEquals(SYSTEM_RULES_VERSION, installer.getSystemRulesVersion());
     }
 
+    public void testGetInstalledDistroVersion() throws Exception {
+        // Check result when nothing installed.
+        assertNull(installer.getInstalledDistroVersion());
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
+
+        // Now simulate there being an existing install active.
+        TimeZoneDistro distro = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
+        simulateInstalledDistro(distro);
+        assertInstalledDistro(distro);
+
+        // Check result when something installed.
+        assertEquals(distro.getDistroVersion(), installer.getInstalledDistroVersion());
+        assertNoDistroOperationStaged();
+        assertInstalledDistro(distro);
+    }
+
+    public void testGetStagedDistroOperation() throws Exception {
+        TimeZoneDistro distro1 = createValidTimeZoneDistro(NEW_RULES_VERSION, 1);
+        TimeZoneDistro distro2 = createValidTimeZoneDistro(NEWER_RULES_VERSION, 1);
+
+        // Check result when nothing staged.
+        assertNull(installer.getStagedDistroOperation());
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
+
+        // Check result after unsuccessfully staging an uninstall.
+        // Can't stage an uninstall without an installed distro.
+        assertFalse(installer.stageUninstall());
+        assertNull(installer.getStagedDistroOperation());
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
+
+        // Check result after staging an install.
+        assertTrue(installer.install(distro1.getBytes()));
+        StagedDistroOperation expectedStagedInstall =
+                StagedDistroOperation.install(distro1.getDistroVersion());
+        assertEquals(expectedStagedInstall, installer.getStagedDistroOperation());
+        assertInstallDistroStaged(distro1);
+        assertNoInstalledDistro();
+
+        // Check result after unsuccessfully staging an uninstall (but after removing a staged
+        // install). Can't stage an uninstall without an installed distro.
+        assertFalse(installer.stageUninstall());
+        assertNull(installer.getStagedDistroOperation());
+        assertNoDistroOperationStaged();
+        assertNoInstalledDistro();
+
+        // Now simulate there being an existing install active.
+        simulateInstalledDistro(distro1);
+        assertInstalledDistro(distro1);
+
+        // Check state after successfully staging an uninstall.
+        assertTrue(installer.stageUninstall());
+        StagedDistroOperation expectedStagedUninstall = StagedDistroOperation.uninstall();
+        assertEquals(expectedStagedUninstall, installer.getStagedDistroOperation());
+        assertDistroUninstallStaged();
+        assertInstalledDistro(distro1);
+
+        // Check state after successfully staging an install.
+        assertEquals(TimeZoneDistroInstaller.INSTALL_SUCCESS,
+                installer.stageInstallWithErrorCode(distro2.getBytes()));
+        StagedDistroOperation expectedStagedInstall2 =
+                StagedDistroOperation.install(distro2.getDistroVersion());
+        assertEquals(expectedStagedInstall2, installer.getStagedDistroOperation());
+        assertInstallDistroStaged(distro2);
+        assertInstalledDistro(distro1);
+    }
+
     private static TimeZoneDistro createValidTimeZoneDistro(
             String rulesVersion, int revision) throws Exception {
         return createValidTimeZoneDistroBuilder(rulesVersion, revision).build();
@@ -354,24 +460,27 @@
                 .setIcuData(icuData);
     }
 
-    private void assertDistroInstalled(TimeZoneDistro expectedDistro) throws Exception {
+    private void assertInstallDistroStaged(TimeZoneDistro expectedDistro) throws Exception {
         assertTrue(testInstallDir.exists());
 
-        File currentTzDataDir = installer.getCurrentTzDataDir();
-        assertTrue(currentTzDataDir.exists());
+        File stagedTzDataDir = installer.getStagedTzDataDir();
+        assertTrue(stagedTzDataDir.exists());
 
         File distroVersionFile =
-                new File(currentTzDataDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
+                new File(stagedTzDataDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
         assertTrue(distroVersionFile.exists());
 
-        File bionicFile = new File(currentTzDataDir, TimeZoneDistro.TZDATA_FILE_NAME);
+        File bionicFile = new File(stagedTzDataDir, TimeZoneDistro.TZDATA_FILE_NAME);
         assertTrue(bionicFile.exists());
 
-        File icuFile = new File(currentTzDataDir, TimeZoneDistro.ICU_DATA_FILE_NAME);
+        File icuFile = new File(stagedTzDataDir, TimeZoneDistro.ICU_DATA_FILE_NAME);
         assertTrue(icuFile.exists());
 
-        // Assert getInstalledDistroVersion() is reporting correctly.
-        assertEquals(expectedDistro.getDistroVersion(), installer.getInstalledDistroVersion());
+        // Assert getStagedDistroState() is reporting correctly.
+        StagedDistroOperation stagedDistroOperation = installer.getStagedDistroOperation();
+        assertNotNull(stagedDistroOperation);
+        assertFalse(stagedDistroOperation.isUninstall);
+        assertEquals(expectedDistro.getDistroVersion(), stagedDistroOperation.distroVersion);
 
         try (ZipInputStream zis = new ZipInputStream(
                 new ByteArrayInputStream(expectedDistro.getBytes()))) {
@@ -401,23 +510,64 @@
             throws Exception {
         byte[] actualBytes = IoUtils.readFileAsByteArray(actual.getPath());
         byte[] expectedBytes = Streams.readFullyNoClose(expected);
-        assertTrue(Arrays.equals(expectedBytes, actualBytes));
+        assertArrayEquals(expectedBytes, actualBytes);
     }
 
-    private void assertNoContentInstalled() throws Exception {
-        assertNull(installer.getInstalledDistroVersion());
+    private void assertNoDistroOperationStaged() throws Exception {
+        assertNull(installer.getStagedDistroOperation());
 
-        File currentTzDataDir = installer.getCurrentTzDataDir();
-        assertFalse(currentTzDataDir.exists());
+        File stagedTzDataDir = installer.getStagedTzDataDir();
+        assertFalse(stagedTzDataDir.exists());
 
         // Also check no working directories are left lying around.
         File workingDir = installer.getWorkingDir();
         assertFalse(workingDir.exists());
 
-        File oldDataDir = installer.getOldTzDataDir();
+        File oldDataDir = installer.getOldStagedDataDir();
         assertFalse(oldDataDir.exists());
     }
 
+    private void assertDistroUninstallStaged() throws Exception {
+        assertEquals(StagedDistroOperation.uninstall(), installer.getStagedDistroOperation());
+
+        File stagedTzDataDir = installer.getStagedTzDataDir();
+        assertTrue(stagedTzDataDir.exists());
+        assertTrue(stagedTzDataDir.isDirectory());
+
+        File uninstallTombstone =
+                new File(stagedTzDataDir, TimeZoneDistroInstaller.UNINSTALL_TOMBSTONE_FILE_NAME);
+        assertTrue(uninstallTombstone.exists());
+        assertTrue(uninstallTombstone.isFile());
+
+        // Also check no working directories are left lying around.
+        File workingDir = installer.getWorkingDir();
+        assertFalse(workingDir.exists());
+
+        File oldDataDir = installer.getOldStagedDataDir();
+        assertFalse(oldDataDir.exists());
+    }
+
+    private void simulateInstalledDistro(TimeZoneDistro timeZoneDistro) throws Exception {
+        File currentTzDataDir = installer.getCurrentTzDataDir();
+        assertFalse(currentTzDataDir.exists());
+        assertTrue(currentTzDataDir.mkdir());
+        timeZoneDistro.extractTo(currentTzDataDir);
+    }
+
+    private void assertNoInstalledDistro() {
+        assertFalse(installer.getCurrentTzDataDir().exists());
+    }
+
+    private void assertInstalledDistro(TimeZoneDistro timeZoneDistro) throws Exception {
+        File currentTzDataDir = installer.getCurrentTzDataDir();
+        assertTrue(currentTzDataDir.exists());
+        File versionFile = new File(currentTzDataDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
+        assertTrue(versionFile.exists());
+        byte[] expectedVersionBytes = timeZoneDistro.getDistroVersion().toBytes();
+        byte[] actualVersionBytes = FileUtils.readBytes(versionFile, expectedVersionBytes.length);
+        assertArrayEquals(expectedVersionBytes, actualVersionBytes);
+    }
+
     private static byte[] createTzData(String rulesVersion) {
         return new ZoneInfoTestHelper.TzDataBuilder()
                 .initializeToValid()