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();
+ }
+ }
+}