Merge "Allow non default root CAs to be not subject to algorithm restrictions"
diff --git a/libart/src/main/java/dalvik/system/ClassExt.java b/libart/src/main/java/dalvik/system/ClassExt.java
index 772bd2e..3daa971 100644
--- a/libart/src/main/java/dalvik/system/ClassExt.java
+++ b/libart/src/main/java/dalvik/system/ClassExt.java
@@ -48,7 +48,7 @@
     private Object obsoleteMethods;
 
     /**
-     * If set, the bytes of the original dex-file associated with the related class.
+     * If set, the bytes or DexCache of the original dex-file associated with the related class.
      *
      * In this instance 'original' means either (1) the dex-file loaded for this class when it was
      * first loaded after all non-retransformation capable transformations had been performed but
@@ -59,7 +59,7 @@
      *
      * This field is a logical part of the 'Class' type.
      */
-    private byte[] originalDexFile;
+    private Object originalDexFile;
 
     /**
      * If class verify fails, we must return same error on subsequent tries. We may store either
diff --git a/luni/src/main/java/libcore/util/TimeZoneDataFiles.java b/luni/src/main/java/libcore/util/TimeZoneDataFiles.java
new file mode 100644
index 0000000..8361339
--- /dev/null
+++ b/luni/src/main/java/libcore/util/TimeZoneDataFiles.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package libcore.util;
+
+/**
+ * Utility methods associated with finding updateable time zone data files.
+ */
+public final class TimeZoneDataFiles {
+
+    private static final String ANDROID_ROOT_ENV = "ANDROID_ROOT";
+    private static final String ANDROID_DATA_ENV = "ANDROID_DATA";
+
+    private TimeZoneDataFiles() {}
+
+    // VisibleForTesting
+    public static String[] getTimeZoneFilePaths(String fileName) {
+        return new String[] {
+                getDataTimeZoneFile(fileName),
+                getSystemTimeZoneFile(fileName)
+        };
+    }
+
+    private static String getDataTimeZoneFile(String fileName) {
+        return System.getenv(ANDROID_DATA_ENV) + "/misc/zoneinfo/current/" + fileName;
+    }
+
+    // VisibleForTesting
+    public static String getSystemTimeZoneFile(String fileName) {
+        return System.getenv(ANDROID_ROOT_ENV) + "/usr/share/zoneinfo/" + fileName;
+    }
+
+    public static String generateIcuDataPath() {
+        StringBuilder icuDataPathBuilder = new StringBuilder();
+        // ICU should first look in ANDROID_DATA. This is used for (optional) timezone data.
+        String dataIcuDataPath = getEnvironmentPath(ANDROID_DATA_ENV, "/misc/zoneinfo/current/icu");
+        if (dataIcuDataPath != null) {
+            icuDataPathBuilder.append(dataIcuDataPath);
+        }
+
+        // ICU should always look in ANDROID_ROOT.
+        String systemIcuDataPath = getEnvironmentPath(ANDROID_ROOT_ENV, "/usr/icu");
+        if (systemIcuDataPath != null) {
+            if (icuDataPathBuilder.length() > 0) {
+                icuDataPathBuilder.append(":");
+            }
+            icuDataPathBuilder.append(systemIcuDataPath);
+        }
+        return icuDataPathBuilder.toString();
+    }
+
+    /**
+     * Creates a path by combining the value of an environment variable with a relative path.
+     * Returns {@code null} if the environment variable is not set.
+     */
+    private static String getEnvironmentPath(String environmentVariable, String path) {
+        String variable = System.getenv(environmentVariable);
+        if (variable == null) {
+            return null;
+        }
+        return variable + path;
+    }
+}
diff --git a/luni/src/main/java/libcore/util/ZoneInfoDB.java b/luni/src/main/java/libcore/util/ZoneInfoDB.java
index d9ff323..acb9c12 100644
--- a/luni/src/main/java/libcore/util/ZoneInfoDB.java
+++ b/luni/src/main/java/libcore/util/ZoneInfoDB.java
@@ -37,9 +37,12 @@
  * @hide - used to implement TimeZone
  */
 public final class ZoneInfoDB {
-  private static final TzData DATA = TzData.loadTzDataWithFallback(
-          System.getenv("ANDROID_DATA") + "/misc/zoneinfo/current/tzdata",
-          System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata");
+
+  // VisibleForTesting
+  public static final String TZDATA_FILE = "tzdata";
+
+  private static final TzData DATA =
+          TzData.loadTzDataWithFallback(TimeZoneDataFiles.getTimeZoneFilePaths(TZDATA_FILE));
 
   public static class TzData {
 
@@ -114,7 +117,7 @@
       // We didn't find any usable tzdata on disk, so let's just hard-code knowledge of "GMT".
       // This is actually implemented in TimeZone itself, so if this is the only time zone
       // we report, we won't be asked any more questions.
-      System.logE("Couldn't find any tzdata!");
+      System.logE("Couldn't find any " + TZDATA_FILE + " file!");
       return TzData.createFallback();
     }
 
@@ -183,7 +186,7 @@
 
         // Something's wrong with the file.
         // Log the problem and return false so we try the next choice.
-        System.logE("tzdata file \"" + path + "\" was present but invalid!", ex);
+        System.logE(TZDATA_FILE + " file \"" + path + "\" was present but invalid!", ex);
         return false;
       }
     }
diff --git a/luni/src/test/java/libcore/java/lang/SystemTest.java b/luni/src/test/java/libcore/java/lang/SystemTest.java
index 2de659d..48f4591 100644
--- a/luni/src/test/java/libcore/java/lang/SystemTest.java
+++ b/luni/src/test/java/libcore/java/lang/SystemTest.java
@@ -16,17 +16,16 @@
 
 package libcore.java.lang;
 
+import junit.framework.TestCase;
+
 import java.io.BufferedWriter;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.lang.SecurityException;
-import java.lang.SecurityManager;
 import java.util.Formatter;
 import java.util.Properties;
 import java.util.concurrent.atomic.AtomicBoolean;
-import junit.framework.TestCase;
 
 public class SystemTest extends TestCase {
 
@@ -250,14 +249,4 @@
         } catch (SecurityException expected) {
         }
     }
-
-    // http://b/34867424
-    public void testIcuPathIncludesTimeZoneOverride() {
-        String icuDataPath = System.getProperty("android.icu.impl.ICUBinary.dataPath");
-        String[] paths = icuDataPath.split(":");
-        assertEquals(2, paths.length);
-
-        assertTrue(paths[0].contains("/misc/zoneinfo/current/icu"));
-        assertTrue(paths[1].contains("/usr/icu"));
-    }
 }
diff --git a/luni/src/test/java/libcore/java/util/TimeZoneTest.java b/luni/src/test/java/libcore/java/util/TimeZoneTest.java
index 6b692cf..375eb36 100644
--- a/luni/src/test/java/libcore/java/util/TimeZoneTest.java
+++ b/luni/src/test/java/libcore/java/util/TimeZoneTest.java
@@ -216,15 +216,26 @@
 
     // http://b/7955614 and http://b/8026776.
     public void testDisplayNames() throws Exception {
+        checkDisplayNames(Locale.US);
+    }
+
+    public void testDisplayNames_nonUS() throws Exception {
+        // run checkDisplayNames with an arbitrary set of Locales.
+        checkDisplayNames(Locale.CHINESE);
+        checkDisplayNames(Locale.FRENCH);
+        checkDisplayNames(Locale.forLanguageTag("bn-BD"));
+    }
+
+    public void checkDisplayNames(Locale locale) throws Exception {
         // Check that there are no time zones that use DST but have the same display name for
         // both standard and daylight time.
         StringBuilder failures = new StringBuilder();
         for (String id : TimeZone.getAvailableIDs()) {
             TimeZone tz = TimeZone.getTimeZone(id);
-            String longDst = tz.getDisplayName(true, TimeZone.LONG, Locale.US);
-            String longStd = tz.getDisplayName(false, TimeZone.LONG, Locale.US);
-            String shortDst = tz.getDisplayName(true, TimeZone.SHORT, Locale.US);
-            String shortStd = tz.getDisplayName(false, TimeZone.SHORT, Locale.US);
+            String longDst = tz.getDisplayName(true, TimeZone.LONG, locale);
+            String longStd = tz.getDisplayName(false, TimeZone.LONG, locale);
+            String shortDst = tz.getDisplayName(true, TimeZone.SHORT, locale);
+            String shortStd = tz.getDisplayName(false, TimeZone.SHORT, locale);
 
             if (tz.useDaylightTime()) {
                 // The long std and dst strings must differ!
@@ -329,15 +340,6 @@
         return String.format("GMT%c%02d:%02d", sign, offset / 60, offset % 60);
     }
 
-    public void testAllDisplayNames() throws Exception {
-      for (Locale locale : Locale.getAvailableLocales()) {
-        for (String id : TimeZone.getAvailableIDs()) {
-          TimeZone tz = TimeZone.getTimeZone(id);
-          assertNotNull(tz.getDisplayName(false, TimeZone.LONG, locale));
-        }
-      }
-    }
-
     // http://b/18839557
     public void testOverflowing32BitUnixDates() {
         final TimeZone tz = TimeZone.getTimeZone("America/New_York");
diff --git a/luni/src/test/java/libcore/javax/crypto/CipherOutputStreamTest.java b/luni/src/test/java/libcore/javax/crypto/CipherOutputStreamTest.java
new file mode 100644
index 0000000..dd9a9a0
--- /dev/null
+++ b/luni/src/test/java/libcore/javax/crypto/CipherOutputStreamTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.javax.crypto;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.Security;
+import java.security.spec.AlgorithmParameterSpec;
+import javax.crypto.AEADBadTagException;
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+public final class CipherOutputStreamTest extends TestCase {
+
+    // From b/36636576. CipherOutputStream had a bug where it would ignore exceptions
+    // thrown during close().
+    public void testDecryptCorruptGCM() throws Exception {
+        for (Provider provider : Security.getProviders()) {
+            Cipher cipher;
+            try {
+                cipher = Cipher.getInstance("AES/GCM/NoPadding", provider);
+            } catch (NoSuchAlgorithmException e) {
+                continue;
+            }
+            SecretKey key;
+            if (provider.getName().equals("AndroidKeyStoreBCWorkaround")) {
+                key = getAndroidKeyStoreSecretKey();
+            } else {
+                KeyGenerator keygen = KeyGenerator.getInstance("AES");
+                keygen.init(256);
+                key = keygen.generateKey();
+            }
+            GCMParameterSpec params = new GCMParameterSpec(128, new byte[12]);
+            byte[] unencrypted = new byte[200];
+
+            // Normal providers require specifying the IV, but KeyStore prohibits it, so
+            // we have to special-case it
+            if (provider.getName().equals("AndroidKeyStoreBCWorkaround")) {
+                cipher.init(Cipher.ENCRYPT_MODE, key);
+            } else {
+                cipher.init(Cipher.ENCRYPT_MODE, key, params);
+            }
+            byte[] encrypted = cipher.doFinal(unencrypted);
+
+            // Corrupt the final byte, which will corrupt the authentication tag
+            encrypted[encrypted.length - 1] ^= 1;
+
+            cipher.init(Cipher.DECRYPT_MODE, key, params);
+            CipherOutputStream cos = new CipherOutputStream(new ByteArrayOutputStream(), cipher);
+            try {
+                cos.write(encrypted);
+                cos.close();
+                fail("Writing a corrupted stream should throw an exception."
+                        + "  Provider: " + provider);
+            } catch (IOException expected) {
+                assertTrue(expected.getCause() instanceof AEADBadTagException);
+            }
+        }
+
+    }
+
+    // The AndroidKeyStoreBCWorkaround provider can't use keys created by anything
+    // but Android KeyStore, which requires using its own parameters class to create
+    // keys.  Since we're in javax, we can't link against the frameworks classes, so
+    // we have to use reflection to make a suitable key.  This will always be safe
+    // because if we're making a key for AndroidKeyStoreBCWorkaround, the KeyStore
+    // classes must be present.
+    private static SecretKey getAndroidKeyStoreSecretKey() throws Exception {
+        KeyGenerator keygen = KeyGenerator.getInstance("AES", "AndroidKeyStore");
+        Class<?> keyParamsBuilderClass = keygen.getClass().getClassLoader().loadClass(
+                "android.security.keystore.KeyGenParameterSpec$Builder");
+        Object keyParamsBuilder = keyParamsBuilderClass.getConstructor(String.class, Integer.TYPE)
+                // 3 is PURPOSE_ENCRYPT | PURPOSE_DECRYPT
+                .newInstance("testDecryptCorruptGCM", 3);
+        keyParamsBuilderClass.getMethod("setBlockModes", new Class[]{String[].class})
+                .invoke(keyParamsBuilder, new Object[]{new String[]{"GCM"}});
+        keyParamsBuilderClass.getMethod("setEncryptionPaddings", new Class[]{String[].class})
+                .invoke(keyParamsBuilder, new Object[]{new String[]{"NoPadding"}});
+        AlgorithmParameterSpec spec = (AlgorithmParameterSpec)
+                keyParamsBuilderClass.getMethod("build", new Class[]{}).invoke(keyParamsBuilder);
+        keygen.init(spec);
+        return keygen.generateKey();
+    }
+}
diff --git a/luni/src/test/java/libcore/util/TimeZoneDataFilesTest.java b/luni/src/test/java/libcore/util/TimeZoneDataFilesTest.java
new file mode 100644
index 0000000..efba900
--- /dev/null
+++ b/luni/src/test/java/libcore/util/TimeZoneDataFilesTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package libcore.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class TimeZoneDataFilesTest {
+
+    @Test
+    public void getTimeZoneFilePaths() {
+        String[] paths = TimeZoneDataFiles.getTimeZoneFilePaths("foo");
+        assertEquals(2, paths.length);
+
+        assertTrue(paths[0].contains("/misc/zoneinfo/current/"));
+        assertTrue(paths[0].endsWith("foo"));
+
+        assertTrue(paths[1].contains("/usr/share/zoneinfo/"));
+        assertTrue(paths[1].endsWith("foo"));
+    }
+
+    // http://b/34867424
+    @Test
+    public void generateIcuDataPath_includesTimeZoneOverride() {
+        String icuDataPath = System.getProperty("android.icu.impl.ICUBinary.dataPath");
+        assertEquals(icuDataPath, TimeZoneDataFiles.generateIcuDataPath());
+
+        String[] paths = icuDataPath.split(":");
+        assertEquals(2, paths.length);
+
+        assertTrue(paths[0].contains("/misc/zoneinfo/current/icu"));
+        assertTrue(paths[1].contains("/usr/icu"));
+    }
+}
diff --git a/luni/src/test/java/libcore/util/ZoneInfoDBTest.java b/luni/src/test/java/libcore/util/ZoneInfoDBTest.java
index 901519e..e6ec4df 100644
--- a/luni/src/test/java/libcore/util/ZoneInfoDBTest.java
+++ b/luni/src/test/java/libcore/util/ZoneInfoDBTest.java
@@ -28,27 +28,27 @@
 public class ZoneInfoDBTest extends junit.framework.TestCase {
 
   // The base tzdata file, always present on a device.
-  private static final String TZDATA_IN_ROOT =
-      System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata";
+  private static final String SYSTEM_TZDATA_FILE =
+      TimeZoneDataFiles.getSystemTimeZoneFile(ZoneInfoDB.TZDATA_FILE);
 
   // An empty override file should fall back to the default file.
   public void testLoadTzDataWithFallback_emptyOverrideFile() throws Exception {
-    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(TZDATA_IN_ROOT);
+    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(SYSTEM_TZDATA_FILE);
     String emptyFilePath = makeEmptyFile().getPath();
 
     ZoneInfoDB.TzData dataWithEmptyOverride =
-        ZoneInfoDB.TzData.loadTzDataWithFallback(emptyFilePath, TZDATA_IN_ROOT);
+        ZoneInfoDB.TzData.loadTzDataWithFallback(emptyFilePath, SYSTEM_TZDATA_FILE);
     assertEquals(data.getVersion(), dataWithEmptyOverride.getVersion());
     assertEquals(data.getAvailableIDs().length, dataWithEmptyOverride.getAvailableIDs().length);
   }
 
   // A corrupt override file should fall back to the default file.
   public void testLoadTzDataWithFallback_corruptOverrideFile() throws Exception {
-    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(TZDATA_IN_ROOT);
+    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(SYSTEM_TZDATA_FILE);
     String corruptFilePath = makeCorruptFile().getPath();
 
     ZoneInfoDB.TzData dataWithCorruptOverride =
-        ZoneInfoDB.TzData.loadTzDataWithFallback(corruptFilePath, TZDATA_IN_ROOT);
+        ZoneInfoDB.TzData.loadTzDataWithFallback(corruptFilePath, SYSTEM_TZDATA_FILE);
     assertEquals(data.getVersion(), dataWithCorruptOverride.getVersion());
     assertEquals(data.getAvailableIDs().length, dataWithCorruptOverride.getAvailableIDs().length);
   }
@@ -64,7 +64,7 @@
 
   // Given a valid override file, we should find ourselves using that.
   public void testLoadTzDataWithFallback_goodOverrideFile() throws Exception {
-    RandomAccessFile in = new RandomAccessFile(TZDATA_IN_ROOT, "r");
+    RandomAccessFile in = new RandomAccessFile(SYSTEM_TZDATA_FILE, "r");
     byte[] content = new byte[(int) in.length()];
     in.readFully(content);
     in.close();
@@ -79,9 +79,9 @@
     File goodFile = makeTemporaryFile(content);
     try {
       ZoneInfoDB.TzData dataWithOverride =
-              ZoneInfoDB.TzData.loadTzDataWithFallback(goodFile.getPath(), TZDATA_IN_ROOT);
+              ZoneInfoDB.TzData.loadTzDataWithFallback(goodFile.getPath(), SYSTEM_TZDATA_FILE);
       assertEquals("9999z", dataWithOverride.getVersion());
-      ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(TZDATA_IN_ROOT);
+      ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(SYSTEM_TZDATA_FILE);
       assertEquals(data.getAvailableIDs().length, dataWithOverride.getAvailableIDs().length);
     } finally {
       goodFile.delete();
@@ -89,7 +89,7 @@
   }
 
   public void testLoadTzData_badHeader() throws Exception {
-    RandomAccessFile in = new RandomAccessFile(TZDATA_IN_ROOT, "r");
+    RandomAccessFile in = new RandomAccessFile(SYSTEM_TZDATA_FILE, "r");
     byte[] content = new byte[(int) in.length()];
     in.readFully(content);
     in.close();
@@ -212,7 +212,7 @@
 
   // Confirms any caching that exists correctly handles TimeZone mutability.
   public void testMakeTimeZone_timeZoneMutability() throws Exception {
-    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(TZDATA_IN_ROOT);
+    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(SYSTEM_TZDATA_FILE);
     String tzId = "Europe/London";
     ZoneInfo first = data.makeTimeZone(tzId);
     ZoneInfo second = data.makeTimeZone(tzId);
@@ -229,21 +229,21 @@
   }
 
   public void testMakeTimeZone_notFound() throws Exception {
-    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(TZDATA_IN_ROOT);
+    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(SYSTEM_TZDATA_FILE);
     assertNull(data.makeTimeZone("THIS_TZ_DOES_NOT_EXIST"));
     assertFalse(data.hasTimeZone("THIS_TZ_DOES_NOT_EXIST"));
   }
 
   public void testMakeTimeZone_found() throws Exception {
-    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(TZDATA_IN_ROOT);
+    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(SYSTEM_TZDATA_FILE);
     assertNotNull(data.makeTimeZone("Europe/London"));
     assertTrue(data.hasTimeZone("Europe/London"));
   }
 
   public void testGetRulesVersion() throws Exception {
-    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(TZDATA_IN_ROOT);
+    ZoneInfoDB.TzData data = ZoneInfoDB.TzData.loadTzData(SYSTEM_TZDATA_FILE);
 
-    String rulesVersion = ZoneInfoDB.TzData.getRulesVersion(new File(TZDATA_IN_ROOT));
+    String rulesVersion = ZoneInfoDB.TzData.getRulesVersion(new File(SYSTEM_TZDATA_FILE));
     assertEquals(data.getVersion(), rulesVersion);
   }
 
diff --git a/non_openjdk_java_files.mk b/non_openjdk_java_files.mk
index fd1353f..31ee139 100644
--- a/non_openjdk_java_files.mk
+++ b/non_openjdk_java_files.mk
@@ -307,6 +307,7 @@
   luni/src/main/java/libcore/util/Objects.java \
   luni/src/main/java/libcore/util/RecoverySystem.java \
   luni/src/main/java/libcore/util/SneakyThrow.java \
+  luni/src/main/java/libcore/util/TimeZoneDataFiles.java \
   luni/src/main/java/libcore/util/ZoneInfo.java \
   luni/src/main/java/libcore/util/ZoneInfoDB.java \
   luni/src/main/java/libcore/util/HexEncoding.java \
diff --git a/ojluni/src/main/java/java/beans/ChangeListenerMap.java b/ojluni/src/main/java/java/beans/ChangeListenerMap.java
index fead0a4..fa8be47 100644
--- a/ojluni/src/main/java/java/beans/ChangeListenerMap.java
+++ b/ojluni/src/main/java/java/beans/ChangeListenerMap.java
@@ -76,7 +76,7 @@
      */
     public final synchronized void add(String name, L listener) {
         if (this.map == null) {
-            this.map = new HashMap<String, L[]>();
+            this.map = new HashMap<>();
         }
         L[] array = this.map.get(name);
         int size = (array != null)
@@ -146,7 +146,7 @@
     public final void set(String name, L[] listeners) {
         if (listeners != null) {
             if (this.map == null) {
-                this.map = new HashMap<String, L[]>();
+                this.map = new HashMap<>();
             }
             this.map.put(name, listeners);
         }
@@ -167,7 +167,7 @@
         if (this.map == null) {
             return newArray(0);
         }
-        List<L> list = new ArrayList<L>();
+        List<L> list = new ArrayList<>();
 
         L[] listeners = this.map.get(null);
         if (listeners != null) {
diff --git a/ojluni/src/main/java/java/lang/System.java b/ojluni/src/main/java/java/lang/System.java
index 4797c1b..6e8f74f 100644
--- a/ojluni/src/main/java/java/lang/System.java
+++ b/ojluni/src/main/java/java/lang/System.java
@@ -41,6 +41,8 @@
 import java.util.PropertyPermission;
 import libcore.icu.ICU;
 import libcore.io.Libcore;
+import libcore.util.TimeZoneDataFiles;
+
 import sun.reflect.CallerSensitive;
 import sun.security.util.SecurityConstants;
 /**
@@ -996,7 +998,7 @@
         // is prioritized over the properties in ICUConfig.properties. The issue with using
         // that is that it doesn't play well with jarjar and it needs complicated build rules
         // to change its default value.
-        String icuDataPath = generateIcuDataPath();
+        String icuDataPath = TimeZoneDataFiles.generateIcuDataPath();
         p.put("android.icu.impl.ICUBinary.dataPath", icuDataPath);
 
         parsePropertyAssignments(p, specialProperties());
@@ -1023,37 +1025,6 @@
         return p;
     }
 
-    private static String generateIcuDataPath() {
-        StringBuilder icuDataPathBuilder = new StringBuilder();
-        // ICU should first look in ANDROID_DATA. This is used for (optional) timezone data.
-        String dataIcuDataPath = getEnvironmentPath("ANDROID_DATA", "/misc/zoneinfo/current/icu");
-        if (dataIcuDataPath != null) {
-            icuDataPathBuilder.append(dataIcuDataPath);
-        }
-
-        // ICU should always look in ANDROID_ROOT.
-        String systemIcuDataPath = getEnvironmentPath("ANDROID_ROOT", "/usr/icu");
-        if (systemIcuDataPath != null) {
-            if (icuDataPathBuilder.length() > 0) {
-                icuDataPathBuilder.append(":");
-            }
-            icuDataPathBuilder.append(systemIcuDataPath);
-        }
-        return icuDataPathBuilder.toString();
-    }
-
-    /**
-     * Creates a path by combining the value of an environment variable with a relative path.
-     * Returns {@code null} if the environment variable is not set.
-     */
-    private static String getEnvironmentPath(String environmentVariable, String path) {
-        String variable = getenv(environmentVariable);
-        if (variable == null) {
-            return null;
-        }
-        return variable + path;
-    }
-
     private static Properties initProperties() {
         Properties p = new PropertiesWithNonOverrideableDefaults(unchangeableProps);
         setDefaultChangeableProperties(p);
diff --git a/ojluni/src/main/java/java/lang/reflect/Array.java b/ojluni/src/main/java/java/lang/reflect/Array.java
index 8285be7..790a930 100644
--- a/ojluni/src/main/java/java/lang/reflect/Array.java
+++ b/ojluni/src/main/java/java/lang/reflect/Array.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2014 The Android Open Source Project
- * Copyright (c) 1996, 2006, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -57,6 +57,8 @@
      * Array.newInstance(componentType, x);
      * </pre>
      * </blockquote>
+     * <p>The number of dimensions of the new array must not
+     * exceed 255.
      *
      * @param componentType the {@code Class} object representing the
      * component type of the new array
@@ -64,7 +66,9 @@
      * @return the new array
      * @exception NullPointerException if the specified
      * {@code componentType} parameter is null
-     * @exception IllegalArgumentException if componentType is {@link Void#TYPE}
+     * @exception IllegalArgumentException if componentType is {@link
+     * Void#TYPE} or if the number of dimensions of the requested array
+     * instance exceed 255.
      * @exception NegativeArraySizeException if the specified {@code length}
      * is negative
      */
@@ -88,8 +92,7 @@
      * {@code componentType}.
      *
      * <p>The number of dimensions of the new array must not
-     * exceed the number of array dimensions supported by the
-     * implementation (typically 255).
+     * exceed 255.
      *
      * @param componentType the {@code Class} object representing the component
      * type of the new array
@@ -99,10 +102,9 @@
      * @exception NullPointerException if the specified
      * {@code componentType} argument is null
      * @exception IllegalArgumentException if the specified {@code dimensions}
-     * argument is a zero-dimensional array, or if the number of
-     * requested dimensions exceeds the limit on the number of array dimensions
-     * supported by the implementation (typically 255), or if componentType
-     * is {@link Void#TYPE}.
+     * argument is a zero-dimensional array, if componentType is {@link
+     * Void#TYPE}, or if the number of dimensions of the requested array
+     * instance exceed 255.
      * @exception NegativeArraySizeException if any of the components in
      * the specified {@code dimensions} argument is negative.
      */
diff --git a/ojluni/src/main/java/java/lang/reflect/Constructor.java b/ojluni/src/main/java/java/lang/reflect/Constructor.java
index 3054e5f..de7176b 100644
--- a/ojluni/src/main/java/java/lang/reflect/Constructor.java
+++ b/ojluni/src/main/java/java/lang/reflect/Constructor.java
@@ -139,6 +139,7 @@
 
     /**
      * {@inheritDoc}
+     * @since 1.8
      */
     public int getParameterCount() {
         // Android-changed: This is handled by Executable.
diff --git a/ojluni/src/main/java/java/lang/reflect/Method.java b/ojluni/src/main/java/java/lang/reflect/Method.java
index 55d741c..ddda93d 100644
--- a/ojluni/src/main/java/java/lang/reflect/Method.java
+++ b/ojluni/src/main/java/java/lang/reflect/Method.java
@@ -186,6 +186,7 @@
 
     /**
      * {@inheritDoc}
+     * @since 1.8
      */
     public int getParameterCount() {
         // Android-changed: This is handled by Executable.
diff --git a/ojluni/src/main/java/java/lang/reflect/ReflectPermission.java b/ojluni/src/main/java/java/lang/reflect/ReflectPermission.java
index b8e8e63..96a2d21 100644
--- a/ojluni/src/main/java/java/lang/reflect/ReflectPermission.java
+++ b/ojluni/src/main/java/java/lang/reflect/ReflectPermission.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2004, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/AccessControlContext.java b/ojluni/src/main/java/java/security/AccessControlContext.java
index 86a88f0..e95cff2 100644
--- a/ojluni/src/main/java/java/security/AccessControlContext.java
+++ b/ojluni/src/main/java/java/security/AccessControlContext.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2014 The Android Open Source Project
- * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/AccessController.java b/ojluni/src/main/java/java/security/AccessController.java
index 1f7bdc5..b4d544c 100644
--- a/ojluni/src/main/java/java/security/AccessController.java
+++ b/ojluni/src/main/java/java/security/AccessController.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2014 The Android Open Source Project
- * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/AuthProvider.java b/ojluni/src/main/java/java/security/AuthProvider.java
index 0308291..23ddb0a 100644
--- a/ojluni/src/main/java/java/security/AuthProvider.java
+++ b/ojluni/src/main/java/java/security/AuthProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2004, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/CodeSource.java b/ojluni/src/main/java/java/security/CodeSource.java
index d678011..7396d91 100644
--- a/ojluni/src/main/java/java/security/CodeSource.java
+++ b/ojluni/src/main/java/java/security/CodeSource.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/DomainCombiner.java b/ojluni/src/main/java/java/security/DomainCombiner.java
index 7dc0849..5426bf6 100644
--- a/ojluni/src/main/java/java/security/DomainCombiner.java
+++ b/ojluni/src/main/java/java/security/DomainCombiner.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2006, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/NoSuchAlgorithmException.java b/ojluni/src/main/java/java/security/NoSuchAlgorithmException.java
index fb34981..951e44e 100644
--- a/ojluni/src/main/java/java/security/NoSuchAlgorithmException.java
+++ b/ojluni/src/main/java/java/security/NoSuchAlgorithmException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1996, 2003, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -58,13 +58,13 @@
     }
 
     /**
-     * Creates a <code>NoSuchAlgorithmException</code> with the specified
+     * Creates a {@code NoSuchAlgorithmException} with the specified
      * detail message and cause.
      *
      * @param message the detail message (which is saved for later retrieval
      *        by the {@link #getMessage()} method).
      * @param cause the cause (which is saved for later retrieval by the
-     *        {@link #getCause()} method).  (A <tt>null</tt> value is permitted,
+     *        {@link #getCause()} method).  (A {@code null} value is permitted,
      *        and indicates that the cause is nonexistent or unknown.)
      * @since 1.5
      */
@@ -73,13 +73,13 @@
     }
 
     /**
-     * Creates a <code>NoSuchAlgorithmException</code> with the specified cause
-     * and a detail message of <tt>(cause==null ? null : cause.toString())</tt>
+     * Creates a {@code NoSuchAlgorithmException} with the specified cause
+     * and a detail message of {@code (cause==null ? null : cause.toString())}
      * (which typically contains the class and detail message of
-     * <tt>cause</tt>).
+     * {@code cause}).
      *
      * @param cause the cause (which is saved for later retrieval by the
-     *        {@link #getCause()} method).  (A <tt>null</tt> value is permitted,
+     *        {@link #getCause()} method).  (A {@code null} value is permitted,
      *        and indicates that the cause is nonexistent or unknown.)
      * @since 1.5
      */
diff --git a/ojluni/src/main/java/java/security/Permission.java b/ojluni/src/main/java/java/security/Permission.java
index 9f04ac4..8d170c0 100644
--- a/ojluni/src/main/java/java/security/Permission.java
+++ b/ojluni/src/main/java/java/security/Permission.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2009, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/Permissions.java b/ojluni/src/main/java/java/security/Permissions.java
index de5d451..7411e06 100644
--- a/ojluni/src/main/java/java/security/Permissions.java
+++ b/ojluni/src/main/java/java/security/Permissions.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/PrivilegedAction.java b/ojluni/src/main/java/java/security/PrivilegedAction.java
index ab39f98..bef7e44 100644
--- a/ojluni/src/main/java/java/security/PrivilegedAction.java
+++ b/ojluni/src/main/java/java/security/PrivilegedAction.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1998, 2004, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/PrivilegedExceptionAction.java b/ojluni/src/main/java/java/security/PrivilegedExceptionAction.java
index 87e4209..bae883e 100644
--- a/ojluni/src/main/java/java/security/PrivilegedExceptionAction.java
+++ b/ojluni/src/main/java/java/security/PrivilegedExceptionAction.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1998, 2004, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/SecurityPermission.java b/ojluni/src/main/java/java/security/SecurityPermission.java
index 432ee4b..7d2ec47 100644
--- a/ojluni/src/main/java/java/security/SecurityPermission.java
+++ b/ojluni/src/main/java/java/security/SecurityPermission.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2006, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/java/security/UnresolvedPermission.java b/ojluni/src/main/java/java/security/UnresolvedPermission.java
index dec952b..6d97fbe 100644
--- a/ojluni/src/main/java/java/security/UnresolvedPermission.java
+++ b/ojluni/src/main/java/java/security/UnresolvedPermission.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
diff --git a/ojluni/src/main/java/javax/crypto/CipherOutputStream.java b/ojluni/src/main/java/javax/crypto/CipherOutputStream.java
index 6b8d273..370f7af 100644
--- a/ojluni/src/main/java/javax/crypto/CipherOutputStream.java
+++ b/ojluni/src/main/java/javax/crypto/CipherOutputStream.java
@@ -210,6 +210,8 @@
             obuffer = cipher.doFinal();
         } catch (IllegalBlockSizeException | BadPaddingException e) {
             obuffer = null;
+            // Android-added: Throw an exception when the underlying cipher does.  http://b/36636576
+            throw new IOException(e);
         }
         try {
             flush();
diff --git a/tools/upstream/oj_upstream_comparison.py b/tools/upstream/oj_upstream_comparison.py
index 312483c..deab5fb 100755
--- a/tools/upstream/oj_upstream_comparison.py
+++ b/tools/upstream/oj_upstream_comparison.py
@@ -46,10 +46,12 @@
 """
 
 import argparse
+import csv
 import filecmp
 import os
 import re
 import shutil
+import sys
 
 def rel_paths_from_makefile(build_top):
     """Returns the list of relative paths to .java files parsed from openjdk_java_files.mk"""
@@ -83,28 +85,75 @@
             return result
     return None
 
-def compare_to_upstreams(build_top, upstream_root, upstreams, rel_paths):
+
+# For files with N and M lines, respectively, this runs in time
+# O(N+M) if the files are identical or O(N*M) if not. This could
+# be improved to O(D*(N+M)) for files with at most D lines
+# difference by only considering array elements within D cells
+# from the diagonal.
+def edit_distance_lines(file_a, file_b):
     """
-    Returns a dict from rel_path to lists of length len(upstreams)
-    Each list entry specifies whether the file at a particular
-    rel_path is missing from, identical to, or different from
-    a particular upstream.
+    Computes the line-based edit distance between two text files, i.e.
+    the smallest number of line deletions, additions or replacements
+    that would transform the content of one file into that of the other.
     """
-    result = {}
+    if filecmp.cmp(file_a, file_b, shallow=False):
+        return 0 # files identical
+    with open(file_a) as f:
+        lines_a = f.readlines()
+    with open(file_b) as f:
+        lines_b = f.readlines()
+    prev_cost = range(0, len(lines_b) + 1)
+    for end_a in range(1, len(lines_a) + 1):
+        # For each valid index i, prev_cost[i] is the edit distance between
+        # lines_a[:end_a-1] and lines_b[:i].
+        # We now calculate cur_cost[end_b] as the edit distance between
+        # line_a[:end_a] and lines_b[:end_b]
+        cur_cost = [end_a]
+        for end_b in range(1, len(lines_b) + 1):
+            c = min(
+                cur_cost[-1] + 1, # append line from b
+                prev_cost[end_b] + 1, # append line from a
+                # match or replace line
+                prev_cost[end_b - 1] + (0 if lines_a[end_a - 1] == lines_b[end_b - 1] else 1)
+                )
+            cur_cost.append(c)
+        prev_cost = cur_cost
+    return prev_cost[-1]
+
+def compare_to_upstreams_and_save(out_file, build_top, upstream_root, upstreams, rel_paths, best_only=False):
+    """
+    Prints tab-separated values comparing ojluni files vs. each
+    upstream, for each of the rel_paths, suitable for human
+    analysis in a spreadsheet.
+    This includes whether the corresponding upstream file is
+    missing, identical, or by how many lines it differs, and
+    a guess as to the correct upstream based on minimal line
+    difference (ties broken in favor of upstreams that occur
+    earlier in the list).
+    """
+    writer = csv.writer(out_file, delimiter='\t')
+    writer.writerow(["rel_path", "guessed_upstream"] + upstreams)
     for rel_path in rel_paths:
         ojluni_file = ojluni_path(build_top, rel_path)
-        status = []
+        upstream_comparisons = []
+        best_distance = sys.maxint
+        guessed_upstream = ""
         for upstream in upstreams:
             upstream_file = upstream_path(upstream_root, upstream, rel_path)
             if upstream_file is None:
-                upstream_status = "missing"
-            elif filecmp.cmp(upstream_file, ojluni_file, shallow=False):
-                upstream_status = "identical"
+                upstream_comparison = "missing"
             else:
-                upstream_status = "different"
-            status.append(upstream_status)
-        result[rel_path] = status
-    return result
+                edit_distance = edit_distance_lines(upstream_file, ojluni_file)
+                if edit_distance == 0:
+                    upstream_comparison = "identical"
+                else:
+                    upstream_comparison = "different (%d lines)" % (edit_distance)
+                if edit_distance < best_distance:
+                    best_distance = edit_distance
+                    guessed_upstream = upstream
+            upstream_comparisons.append(upstream_comparison)
+        writer.writerow([rel_path, guessed_upstream ] + upstream_comparisons)
 
 def copy_files(rel_paths, upstream_root, upstream, output_dir):
     """Copies files at the given rel_paths from upstream to output_dir"""
@@ -148,13 +197,12 @@
             raise Exception("Upstream not found: " + upstream_path)
 
     rel_paths = rel_paths_from_makefile(args.build_top)
-    upstream_infos = compare_to_upstreams(args.build_top, args.upstream_root, upstreams, rel_paths)
+
+    compare_to_upstreams_and_save(
+        sys.stdout, args.build_top, args.upstream_root, upstreams, rel_paths)
 
     if args.output_dir is not None:
         copy_files(rel_paths, args.upstream_root, default_upstream, args.output_dir)
 
-    for rel_path in rel_paths:
-        print(rel_path + "\t" +  "\t".join(upstream_infos[rel_path]))
-
 if __name__ == '__main__':
     main()
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()