Merge changes If3a6eeb3,I2a29dc60

* changes:
  Factor out code for loading and saving rollback data.
  Store meta data for a rollback in a single file.
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index d12f7ed..48ddf8c 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -49,18 +49,9 @@
 import com.android.server.LocalServices;
 import com.android.server.pm.PackageManagerServiceUtils;
 
-import libcore.io.IoUtils;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
 import java.io.File;
 import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.file.Files;
 import java.time.Instant;
-import java.time.format.DateTimeParseException;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -108,34 +99,7 @@
     @GuardedBy("mLock")
     private List<RollbackInfo> mRecentlyExecutedRollbacks;
 
-    // Data for available rollbacks and recently executed rollbacks is
-    // persisted in storage. Assuming the rollback data directory is
-    // /data/rollback, we use the following directory structure
-    // to store this data:
-    //   /data/rollback/
-    //      available/
-    //          XXX/
-    //              com.package.A/
-    //                  base.apk
-    //                  info.json
-    //              enabled.txt
-    //          YYY/
-    //              com.package.B/
-    //                  base.apk
-    //                  info.json
-    //              enabled.txt
-    //      recently_executed.json
-    //
-    // * XXX, YYY are random strings from Files.createTempDirectory
-    // * info.json contains the package version to roll back from/to.
-    // * enabled.txt contains a timestamp for when the rollback was first
-    //   made available. This file is not written until the rollback is made
-    //   available.
-    //
-    // TODO: Use AtomicFile for all the .json files?
-    private final File mRollbackDataDir;
-    private final File mAvailableRollbacksDir;
-    private final File mRecentlyExecutedRollbacksFile;
+    private final RollbackStore mRollbackStore;
 
     private final Context mContext;
     private final HandlerThread mHandlerThread;
@@ -145,9 +109,7 @@
         mHandlerThread = new HandlerThread("RollbackManagerServiceHandler");
         mHandlerThread.start();
 
-        mRollbackDataDir = new File(Environment.getDataDirectory(), "rollback");
-        mAvailableRollbacksDir = new File(mRollbackDataDir, "available");
-        mRecentlyExecutedRollbacksFile = new File(mRollbackDataDir, "recently_executed.json");
+        mRollbackStore = new RollbackStore(new File(Environment.getDataDirectory(), "rollback"));
 
         // Kick off loading of the rollback data from strorage in a background
         // thread.
@@ -447,7 +409,7 @@
                 for (PackageRollbackInfo info : data.packages) {
                     if (info.packageName.equals(packageName)) {
                         iter.remove();
-                        removeFile(data.backupDir);
+                        mRollbackStore.deleteAvailableRollback(data);
                         break;
                     }
                 }
@@ -474,87 +436,20 @@
     @GuardedBy("mLock")
     private void ensureRollbackDataLoadedLocked() {
         if (mAvailableRollbacks == null) {
-            loadRollbackDataLocked();
+            loadAllRollbackDataLocked();
         }
     }
 
     /**
-     * Load rollback data from storage.
+     * Load all rollback data from storage.
      * Note: We do potentially heavy IO here while holding mLock, because we
      * have to have the rollback data loaded before we can do anything else
      * meaningful.
      */
     @GuardedBy("mLock")
-    private void loadRollbackDataLocked() {
-        mAvailableRollbacksDir.mkdirs();
-        mAvailableRollbacks = new ArrayList<>();
-        for (File rollbackDir : mAvailableRollbacksDir.listFiles()) {
-            File enabledFile = new File(rollbackDir, "enabled.txt");
-            // TODO: Delete any directories without an enabled.txt? That could
-            // potentially delete pending rollback data if reloadPersistedData
-            // is called, though there's no reason besides testing for that to
-            // be called.
-            if (rollbackDir.isDirectory() && enabledFile.isFile()) {
-                RollbackData data = new RollbackData(rollbackDir);
-                try {
-                    PackageRollbackInfo info = null;
-                    for (File packageDir : rollbackDir.listFiles()) {
-                        if (packageDir.isDirectory()) {
-                            File jsonFile = new File(packageDir, "info.json");
-                            String jsonString = IoUtils.readFileAsString(
-                                    jsonFile.getAbsolutePath());
-                            JSONObject jsonObject = new JSONObject(jsonString);
-                            String packageName = jsonObject.getString("packageName");
-                            long higherVersionCode = jsonObject.getLong("higherVersionCode");
-                            long lowerVersionCode = jsonObject.getLong("lowerVersionCode");
-
-                            data.packages.add(new PackageRollbackInfo(packageName,
-                                        new PackageRollbackInfo.PackageVersion(higherVersionCode),
-                                        new PackageRollbackInfo.PackageVersion(lowerVersionCode)));
-                        }
-                    }
-
-                    if (data.packages.isEmpty()) {
-                        throw new IOException("No package rollback info found");
-                    }
-
-                    String enabledString = IoUtils.readFileAsString(enabledFile.getAbsolutePath());
-                    data.timestamp = Instant.parse(enabledString.trim());
-                    mAvailableRollbacks.add(data);
-                } catch (IOException | JSONException | DateTimeParseException e) {
-                    Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
-                    removeFile(rollbackDir);
-                }
-            }
-        }
-
-        mRecentlyExecutedRollbacks = new ArrayList<>();
-        if (mRecentlyExecutedRollbacksFile.exists()) {
-            try {
-                // TODO: How to cope with changes to the format of this file from
-                // when RollbackStore is updated in the future?
-                String jsonString = IoUtils.readFileAsString(
-                        mRecentlyExecutedRollbacksFile.getAbsolutePath());
-                JSONObject object = new JSONObject(jsonString);
-                JSONArray array = object.getJSONArray("recentlyExecuted");
-                for (int i = 0; i < array.length(); ++i) {
-                    JSONObject element = array.getJSONObject(i);
-                    String packageName = element.getString("packageName");
-                    long higherVersionCode = element.getLong("higherVersionCode");
-                    long lowerVersionCode = element.getLong("lowerVersionCode");
-                    PackageRollbackInfo target = new PackageRollbackInfo(packageName,
-                            new PackageRollbackInfo.PackageVersion(higherVersionCode),
-                            new PackageRollbackInfo.PackageVersion(lowerVersionCode));
-                    RollbackInfo rollback = new RollbackInfo(target);
-                    mRecentlyExecutedRollbacks.add(rollback);
-                }
-            } catch (IOException | JSONException e) {
-                // TODO: What to do here? Surely we shouldn't just forget about
-                // everything after the point of exception?
-                Log.e(TAG, "Failed to read recently executed rollbacks", e);
-            }
-        }
-
+    private void loadAllRollbackDataLocked() {
+        mAvailableRollbacks = mRollbackStore.loadAvailableRollbacks();
+        mRecentlyExecutedRollbacks = mRollbackStore.loadRecentlyExecutedRollbacks();
         scheduleExpiration(0);
     }
 
@@ -578,7 +473,7 @@
                     if (info.packageName.equals(packageName)
                             && !info.higherVersion.equals(installedVersion)) {
                         iter.remove();
-                        removeFile(data.backupDir);
+                        mRollbackStore.deleteAvailableRollback(data);
                         break;
                     }
                 }
@@ -606,42 +501,12 @@
             }
 
             if (changed) {
-                saveRecentlyExecutedRollbacksLocked();
+                mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks);
             }
         }
     }
 
     /**
-     * Write the list of recently executed rollbacks to storage.
-     * Note: This happens while mLock is held, which should be okay because we
-     * expect executed rollbacks to be modified only in exceptional cases.
-     */
-    @GuardedBy("mLock")
-    private void saveRecentlyExecutedRollbacksLocked() {
-        try {
-            JSONObject json = new JSONObject();
-            JSONArray array = new JSONArray();
-            json.put("recentlyExecuted", array);
-
-            for (int i = 0; i < mRecentlyExecutedRollbacks.size(); ++i) {
-                RollbackInfo rollback = mRecentlyExecutedRollbacks.get(i);
-                JSONObject element = new JSONObject();
-                element.put("packageName", rollback.targetPackage.packageName);
-                element.put("higherVersionCode", rollback.targetPackage.higherVersion.versionCode);
-                element.put("lowerVersionCode", rollback.targetPackage.lowerVersion.versionCode);
-                array.put(element);
-            }
-
-            PrintWriter pw = new PrintWriter(mRecentlyExecutedRollbacksFile);
-            pw.println(json.toString());
-            pw.close();
-        } catch (IOException | JSONException e) {
-            // TODO: What to do here?
-            Log.e(TAG, "Failed to save recently executed rollbacks", e);
-        }
-    }
-
-    /**
      * Records that the given package has been recently rolled back.
      */
     private void addRecentlyExecutedRollback(RollbackInfo rollback) {
@@ -650,7 +515,7 @@
         synchronized (mLock) {
             ensureRollbackDataLoadedLocked();
             mRecentlyExecutedRollbacks.add(rollback);
-            saveRecentlyExecutedRollbacksLocked();
+            mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks);
         }
     }
 
@@ -701,7 +566,7 @@
                 RollbackData data = iter.next();
                 if (!now.isBefore(data.timestamp.plusMillis(ROLLBACK_LIFETIME_DURATION_MILLIS))) {
                     iter.remove();
-                    removeFile(data.backupDir);
+                    mRollbackStore.deleteAvailableRollback(data);
                 } else if (oldest == null || oldest.isAfter(data.timestamp)) {
                     oldest = data.timestamp;
                 }
@@ -827,9 +692,7 @@
                 mChildSessions.put(childSessionId, parentSessionId);
                 data = mPendingRollbacks.get(parentSessionId);
                 if (data == null) {
-                    File backupDir = Files.createTempDirectory(
-                            mAvailableRollbacksDir.toPath(), null).toFile();
-                    data = new RollbackData(backupDir);
+                    data = mRollbackStore.createAvailableRollback();
                     mPendingRollbacks.put(parentSessionId, data);
                 }
                 data.packages.add(info);
@@ -839,29 +702,13 @@
             return false;
         }
 
-        File packageDir = new File(data.backupDir, packageName);
+        File packageDir = mRollbackStore.packageCodePathForAvailableRollback(data, packageName);
         packageDir.mkdirs();
-        try {
-            JSONObject json = new JSONObject();
-            json.put("packageName", packageName);
-            json.put("higherVersionCode", newVersion.versionCode);
-            json.put("lowerVersionCode", installedVersion.versionCode);
-
-            File jsonFile = new File(packageDir, "info.json");
-            PrintWriter pw = new PrintWriter(jsonFile);
-            pw.println(json.toString());
-            pw.close();
-        } catch (IOException | JSONException e) {
-            Log.e(TAG, "Unable to create rollback for " + packageName, e);
-            removeFile(packageDir);
-            return false;
-        }
 
         // TODO: Copy by hard link instead to save on cpu and storage space?
         int status = PackageManagerServiceUtils.copyPackage(installedPackage.codePath, packageDir);
         if (status != PackageManager.INSTALL_SUCCEEDED) {
             Log.e(TAG, "Unable to copy package for rollback for " + packageName);
-            removeFile(packageDir);
             return false;
         }
 
@@ -898,22 +745,6 @@
     }
 
     /**
-     * Deletes a file completely.
-     * If the file is a directory, its contents are deleted as well.
-     * Has no effect if the directory does not exist.
-     */
-    private void removeFile(File file) {
-        if (file.isDirectory()) {
-            for (File child : file.listFiles()) {
-                removeFile(child);
-            }
-        }
-        if (file.exists()) {
-            file.delete();
-        }
-    }
-
-    /**
      * Gets the version of the package currently installed.
      * Returns null if the package is not currently installed.
      */
@@ -958,11 +789,8 @@
                 if (success) {
                     try {
                         data.timestamp = Instant.now();
-                        File enabledFile = new File(data.backupDir, "enabled.txt");
-                        PrintWriter pw = new PrintWriter(enabledFile);
-                        pw.println(data.timestamp.toString());
-                        pw.close();
 
+                        mRollbackStore.saveAvailableRollback(data);
                         synchronized (mLock) {
                             // Note: There is a small window of time between when
                             // the session has been committed by the package
@@ -981,12 +809,12 @@
                         scheduleExpiration(ROLLBACK_LIFETIME_DURATION_MILLIS);
                     } catch (IOException e) {
                         Log.e(TAG, "Unable to enable rollback", e);
-                        removeFile(data.backupDir);
+                        mRollbackStore.deleteAvailableRollback(data);
                     }
                 } else {
                     // The install session was aborted, clean up the pending
                     // install.
-                    removeFile(data.backupDir);
+                    mRollbackStore.deleteAvailableRollback(data);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java
new file mode 100644
index 0000000..f9a838f
--- /dev/null
+++ b/services/core/java/com/android/server/rollback/RollbackStore.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2018 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 com.android.server.rollback;
+
+import android.content.rollback.PackageRollbackInfo;
+import android.content.rollback.RollbackInfo;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class for loading and saving rollback data to persistent storage.
+ */
+class RollbackStore {
+    private static final String TAG = "RollbackManager";
+
+    // Assuming the rollback data directory is /data/rollback, we use the
+    // following directory structure to store persisted data for available and
+    // recently executed rollbacks:
+    //   /data/rollback/
+    //      available/
+    //          XXX/
+    //              rollback.json
+    //              com.package.A/
+    //                  base.apk
+    //              com.package.B/
+    //                  base.apk
+    //          YYY/
+    //              rollback.json
+    //              com.package.C/
+    //                  base.apk
+    //      recently_executed.json
+    //
+    // * XXX, YYY are random strings from Files.createTempDirectory
+    // * rollback.json contains all relevant metadata for the rollback. This
+    //   file is not written until the rollback is made available.
+    //
+    // TODO: Use AtomicFile for all the .json files?
+    private final File mRollbackDataDir;
+    private final File mAvailableRollbacksDir;
+    private final File mRecentlyExecutedRollbacksFile;
+
+    RollbackStore(File rollbackDataDir) {
+        mRollbackDataDir = rollbackDataDir;
+        mAvailableRollbacksDir = new File(mRollbackDataDir, "available");
+        mRecentlyExecutedRollbacksFile = new File(mRollbackDataDir, "recently_executed.json");
+    }
+
+    /**
+     * Reads the list of available rollbacks from persistent storage.
+     */
+    List<RollbackData> loadAvailableRollbacks() {
+        List<RollbackData> availableRollbacks = new ArrayList<>();
+        mAvailableRollbacksDir.mkdirs();
+        for (File rollbackDir : mAvailableRollbacksDir.listFiles()) {
+            if (rollbackDir.isDirectory()) {
+                try {
+                    RollbackData data = loadRollbackData(rollbackDir);
+                    availableRollbacks.add(data);
+                } catch (IOException e) {
+                    // Note: Deleting the rollbackDir here will cause pending
+                    // rollbacks to be deleted. This should only ever happen
+                    // if reloadPersistedData is called while there are
+                    // pending rollbacks. The reloadPersistedData method is
+                    // currently only for testing, so that should be okay.
+                    Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
+                    removeFile(rollbackDir);
+                }
+            }
+        }
+        return availableRollbacks;
+    }
+
+    /**
+     * Reads the list of recently executed rollbacks from persistent storage.
+     */
+    List<RollbackInfo> loadRecentlyExecutedRollbacks() {
+        List<RollbackInfo> recentlyExecutedRollbacks = new ArrayList<>();
+        if (mRecentlyExecutedRollbacksFile.exists()) {
+            try {
+                // TODO: How to cope with changes to the format of this file from
+                // when RollbackStore is updated in the future?
+                String jsonString = IoUtils.readFileAsString(
+                        mRecentlyExecutedRollbacksFile.getAbsolutePath());
+                JSONObject object = new JSONObject(jsonString);
+                JSONArray array = object.getJSONArray("recentlyExecuted");
+                for (int i = 0; i < array.length(); ++i) {
+                    JSONObject element = array.getJSONObject(i);
+                    String packageName = element.getString("packageName");
+                    long higherVersionCode = element.getLong("higherVersionCode");
+                    long lowerVersionCode = element.getLong("lowerVersionCode");
+                    PackageRollbackInfo target = new PackageRollbackInfo(packageName,
+                            new PackageRollbackInfo.PackageVersion(higherVersionCode),
+                            new PackageRollbackInfo.PackageVersion(lowerVersionCode));
+                    RollbackInfo rollback = new RollbackInfo(target);
+                    recentlyExecutedRollbacks.add(rollback);
+                }
+            } catch (IOException | JSONException e) {
+                // TODO: What to do here? Surely we shouldn't just forget about
+                // everything after the point of exception?
+                Log.e(TAG, "Failed to read recently executed rollbacks", e);
+            }
+        }
+
+        return recentlyExecutedRollbacks;
+    }
+
+    /**
+     * Creates a new RollbackData instance with backupDir assigned.
+     */
+    RollbackData createAvailableRollback() throws IOException {
+        File backupDir = Files.createTempDirectory(mAvailableRollbacksDir.toPath(), null).toFile();
+        return new RollbackData(backupDir);
+    }
+
+    /**
+     * Returns the directory where the code for a package should be stored for
+     * given rollback <code>data</code> and <code>packageName</code>.
+     */
+    File packageCodePathForAvailableRollback(RollbackData data, String packageName) {
+        return new File(data.backupDir, packageName);
+    }
+
+    /**
+     * Writes the metadata for an available rollback to persistent storage.
+     */
+    void saveAvailableRollback(RollbackData data) throws IOException {
+        try {
+            JSONObject dataJson = new JSONObject();
+            JSONArray packagesJson = new JSONArray();
+            for (PackageRollbackInfo info : data.packages) {
+                JSONObject infoJson = new JSONObject();
+                infoJson.put("packageName", info.packageName);
+                infoJson.put("higherVersionCode", info.higherVersion.versionCode);
+                infoJson.put("lowerVersionCode", info.lowerVersion.versionCode);
+                packagesJson.put(infoJson);
+            }
+            dataJson.put("packages", packagesJson);
+            dataJson.put("timestamp", data.timestamp.toString());
+
+            PrintWriter pw = new PrintWriter(new File(data.backupDir, "rollback.json"));
+            pw.println(dataJson.toString());
+            pw.close();
+        } catch (JSONException e) {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Removes all persistant storage associated with the given available
+     * rollback.
+     */
+    void deleteAvailableRollback(RollbackData data) {
+        removeFile(data.backupDir);
+    }
+
+    /**
+     * Writes the list of recently executed rollbacks to storage.
+     */
+    void saveRecentlyExecutedRollbacks(List<RollbackInfo> recentlyExecutedRollbacks) {
+        try {
+            JSONObject json = new JSONObject();
+            JSONArray array = new JSONArray();
+            json.put("recentlyExecuted", array);
+
+            for (int i = 0; i < recentlyExecutedRollbacks.size(); ++i) {
+                RollbackInfo rollback = recentlyExecutedRollbacks.get(i);
+                JSONObject element = new JSONObject();
+                element.put("packageName", rollback.targetPackage.packageName);
+                element.put("higherVersionCode", rollback.targetPackage.higherVersion.versionCode);
+                element.put("lowerVersionCode", rollback.targetPackage.lowerVersion.versionCode);
+                array.put(element);
+            }
+
+            PrintWriter pw = new PrintWriter(mRecentlyExecutedRollbacksFile);
+            pw.println(json.toString());
+            pw.close();
+        } catch (IOException | JSONException e) {
+            // TODO: What to do here?
+            Log.e(TAG, "Failed to save recently executed rollbacks", e);
+        }
+    }
+
+    /**
+     * Reads the metadata for a rollback from the given directory.
+     * @throws IOException in case of error reading the data.
+     */
+    private RollbackData loadRollbackData(File backupDir) throws IOException {
+        try {
+            RollbackData data = new RollbackData(backupDir);
+            File rollbackJsonFile = new File(backupDir, "rollback.json");
+            JSONObject dataJson = new JSONObject(
+                    IoUtils.readFileAsString(rollbackJsonFile.getAbsolutePath()));
+            JSONArray packagesJson = dataJson.getJSONArray("packages");
+            for (int i = 0; i < packagesJson.length(); ++i) {
+                JSONObject infoJson = packagesJson.getJSONObject(i);
+                String packageName = infoJson.getString("packageName");
+                long higherVersionCode = infoJson.getLong("higherVersionCode");
+                long lowerVersionCode = infoJson.getLong("lowerVersionCode");
+                data.packages.add(new PackageRollbackInfo(packageName,
+                        new PackageRollbackInfo.PackageVersion(higherVersionCode),
+                        new PackageRollbackInfo.PackageVersion(lowerVersionCode)));
+            }
+
+            data.timestamp = Instant.parse(dataJson.getString("timestamp"));
+            return data;
+        } catch (JSONException | DateTimeParseException e) {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Deletes a file completely.
+     * If the file is a directory, its contents are deleted as well.
+     * Has no effect if the directory does not exist.
+     */
+    private void removeFile(File file) {
+        if (file.isDirectory()) {
+            for (File child : file.listFiles()) {
+                removeFile(child);
+            }
+        }
+        if (file.exists()) {
+            file.delete();
+        }
+    }
+}