/*
|
* 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 static com.android.server.rollback.RollbackData.rollbackStateFromString;
|
import static com.android.server.rollback.RollbackData.rollbackStateToString;
|
|
import android.annotation.NonNull;
|
import android.content.pm.VersionedPackage;
|
import android.content.rollback.PackageRollbackInfo;
|
import android.content.rollback.PackageRollbackInfo.RestoreInfo;
|
import android.content.rollback.RollbackInfo;
|
import android.util.IntArray;
|
import android.util.Log;
|
import android.util.SparseLongArray;
|
|
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.text.ParseException;
|
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 rollbacks:
|
// /data/rollback/
|
// XXX/
|
// rollback.json
|
// com.package.A/
|
// base.apk
|
// com.package.B/
|
// base.apk
|
// YYY/
|
// rollback.json
|
//
|
// * XXX, YYY are the rollbackIds for the corresponding rollbacks.
|
// * rollback.json contains all relevant metadata for the rollback.
|
//
|
// TODO: Use AtomicFile for all the .json files?
|
private final File mRollbackDataDir;
|
|
RollbackStore(File rollbackDataDir) {
|
mRollbackDataDir = rollbackDataDir;
|
}
|
|
/**
|
* Reads the rollback data from persistent storage.
|
*/
|
List<RollbackData> loadAllRollbackData() {
|
List<RollbackData> rollbacks = new ArrayList<>();
|
mRollbackDataDir.mkdirs();
|
for (File rollbackDir : mRollbackDataDir.listFiles()) {
|
if (rollbackDir.isDirectory()) {
|
try {
|
rollbacks.add(loadRollbackData(rollbackDir));
|
} catch (IOException e) {
|
Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
|
removeFile(rollbackDir);
|
}
|
}
|
}
|
return rollbacks;
|
}
|
|
/**
|
* Converts an {@code JSONArray} of integers to an {@code IntArray}.
|
*/
|
private static @NonNull IntArray convertToIntArray(@NonNull JSONArray jsonArray)
|
throws JSONException {
|
if (jsonArray.length() == 0) {
|
return new IntArray();
|
}
|
|
final int[] ret = new int[jsonArray.length()];
|
for (int i = 0; i < ret.length; ++i) {
|
ret[i] = jsonArray.getInt(i);
|
}
|
|
return IntArray.wrap(ret);
|
}
|
|
/**
|
* Converts an {@code IntArray} into an {@code JSONArray} of integers.
|
*/
|
private static @NonNull JSONArray convertToJsonArray(@NonNull IntArray intArray) {
|
JSONArray jsonArray = new JSONArray();
|
for (int i = 0; i < intArray.size(); ++i) {
|
jsonArray.put(intArray.get(i));
|
}
|
|
return jsonArray;
|
}
|
|
private static @NonNull JSONArray convertToJsonArray(@NonNull List<RestoreInfo> list)
|
throws JSONException {
|
JSONArray jsonArray = new JSONArray();
|
for (RestoreInfo ri : list) {
|
JSONObject jo = new JSONObject();
|
jo.put("userId", ri.userId);
|
jo.put("appId", ri.appId);
|
jo.put("seInfo", ri.seInfo);
|
jsonArray.put(jo);
|
}
|
|
return jsonArray;
|
}
|
|
private static @NonNull ArrayList<RestoreInfo> convertToRestoreInfoArray(
|
@NonNull JSONArray array) throws JSONException {
|
ArrayList<RestoreInfo> restoreInfos = new ArrayList<>();
|
|
for (int i = 0; i < array.length(); ++i) {
|
JSONObject jo = array.getJSONObject(i);
|
restoreInfos.add(new RestoreInfo(
|
jo.getInt("userId"),
|
jo.getInt("appId"),
|
jo.getString("seInfo")));
|
}
|
|
return restoreInfos;
|
}
|
|
private static @NonNull JSONArray ceSnapshotInodesToJson(
|
@NonNull SparseLongArray ceSnapshotInodes) throws JSONException {
|
JSONArray array = new JSONArray();
|
for (int i = 0; i < ceSnapshotInodes.size(); i++) {
|
JSONObject entryJson = new JSONObject();
|
entryJson.put("userId", ceSnapshotInodes.keyAt(i));
|
entryJson.put("ceSnapshotInode", ceSnapshotInodes.valueAt(i));
|
array.put(entryJson);
|
}
|
return array;
|
}
|
|
private static @NonNull SparseLongArray ceSnapshotInodesFromJson(JSONArray json)
|
throws JSONException {
|
SparseLongArray ceSnapshotInodes = new SparseLongArray(json.length());
|
for (int i = 0; i < json.length(); i++) {
|
JSONObject entry = json.getJSONObject(i);
|
ceSnapshotInodes.append(entry.getInt("userId"), entry.getLong("ceSnapshotInode"));
|
}
|
return ceSnapshotInodes;
|
}
|
|
private static JSONObject rollbackInfoToJson(RollbackInfo rollback) throws JSONException {
|
JSONObject json = new JSONObject();
|
json.put("rollbackId", rollback.getRollbackId());
|
json.put("packages", toJson(rollback.getPackages()));
|
json.put("isStaged", rollback.isStaged());
|
json.put("causePackages", versionedPackagesToJson(rollback.getCausePackages()));
|
json.put("committedSessionId", rollback.getCommittedSessionId());
|
return json;
|
}
|
|
private static RollbackInfo rollbackInfoFromJson(JSONObject json) throws JSONException {
|
return new RollbackInfo(
|
json.getInt("rollbackId"),
|
packageRollbackInfosFromJson(json.getJSONArray("packages")),
|
json.getBoolean("isStaged"),
|
versionedPackagesFromJson(json.getJSONArray("causePackages")),
|
json.getInt("committedSessionId"));
|
}
|
|
/**
|
* Creates a new RollbackData instance for a non-staged rollback with
|
* backupDir assigned.
|
*/
|
RollbackData createNonStagedRollback(int rollbackId) {
|
File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId));
|
return new RollbackData(rollbackId, backupDir, -1);
|
}
|
|
/**
|
* Creates a new RollbackData instance for a staged rollback with
|
* backupDir assigned.
|
*/
|
RollbackData createStagedRollback(int rollbackId, int stagedSessionId) {
|
File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId));
|
return new RollbackData(rollbackId, backupDir, stagedSessionId);
|
}
|
|
/**
|
* Creates a backup copy of an apk or apex for a package.
|
* For packages containing splits, this method should be called for each
|
* of the package's split apks in addition to the base apk.
|
*/
|
static void backupPackageCodePath(RollbackData data, String packageName, String codePath)
|
throws IOException {
|
File sourceFile = new File(codePath);
|
File targetDir = new File(data.backupDir, packageName);
|
targetDir.mkdirs();
|
File targetFile = new File(targetDir, sourceFile.getName());
|
|
// TODO: Copy by hard link instead to save on cpu and storage space?
|
Files.copy(sourceFile.toPath(), targetFile.toPath());
|
}
|
|
/**
|
* Returns the apk or apex files backed up for the given package.
|
* Includes the base apk and any splits. Returns null if none found.
|
*/
|
static File[] getPackageCodePaths(RollbackData data, String packageName) {
|
File targetDir = new File(data.backupDir, packageName);
|
File[] files = targetDir.listFiles();
|
if (files == null || files.length == 0) {
|
return null;
|
}
|
return files;
|
}
|
|
/**
|
* Deletes all backed up apks and apex files associated with the given
|
* rollback.
|
*/
|
static void deletePackageCodePaths(RollbackData data) {
|
for (PackageRollbackInfo info : data.info.getPackages()) {
|
File targetDir = new File(data.backupDir, info.getPackageName());
|
removeFile(targetDir);
|
}
|
}
|
|
/**
|
* Saves the rollback data to persistent storage.
|
*/
|
void saveRollbackData(RollbackData data) throws IOException {
|
try {
|
JSONObject dataJson = new JSONObject();
|
dataJson.put("info", rollbackInfoToJson(data.info));
|
dataJson.put("timestamp", data.timestamp.toString());
|
dataJson.put("stagedSessionId", data.stagedSessionId);
|
dataJson.put("state", rollbackStateToString(data.state));
|
dataJson.put("apkSessionId", data.apkSessionId);
|
dataJson.put("restoreUserDataInProgress", data.restoreUserDataInProgress);
|
|
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 rollback data.
|
*/
|
void deleteRollbackData(RollbackData data) {
|
removeFile(data.backupDir);
|
}
|
|
/**
|
* Reads the metadata for a rollback from the given directory.
|
* @throws IOException in case of error reading the data.
|
*/
|
private static RollbackData loadRollbackData(File backupDir) throws IOException {
|
try {
|
File rollbackJsonFile = new File(backupDir, "rollback.json");
|
JSONObject dataJson = new JSONObject(
|
IoUtils.readFileAsString(rollbackJsonFile.getAbsolutePath()));
|
|
return new RollbackData(
|
rollbackInfoFromJson(dataJson.getJSONObject("info")),
|
backupDir,
|
Instant.parse(dataJson.getString("timestamp")),
|
dataJson.getInt("stagedSessionId"),
|
rollbackStateFromString(dataJson.getString("state")),
|
dataJson.getInt("apkSessionId"),
|
dataJson.getBoolean("restoreUserDataInProgress"));
|
} catch (JSONException | DateTimeParseException | ParseException e) {
|
throw new IOException(e);
|
}
|
}
|
|
private static JSONObject toJson(VersionedPackage pkg) throws JSONException {
|
JSONObject json = new JSONObject();
|
json.put("packageName", pkg.getPackageName());
|
json.put("longVersionCode", pkg.getLongVersionCode());
|
return json;
|
}
|
|
private static VersionedPackage versionedPackageFromJson(JSONObject json) throws JSONException {
|
String packageName = json.getString("packageName");
|
long longVersionCode = json.getLong("longVersionCode");
|
return new VersionedPackage(packageName, longVersionCode);
|
}
|
|
private static JSONObject toJson(PackageRollbackInfo info) throws JSONException {
|
JSONObject json = new JSONObject();
|
json.put("versionRolledBackFrom", toJson(info.getVersionRolledBackFrom()));
|
json.put("versionRolledBackTo", toJson(info.getVersionRolledBackTo()));
|
|
IntArray pendingBackups = info.getPendingBackups();
|
List<RestoreInfo> pendingRestores = info.getPendingRestores();
|
IntArray installedUsers = info.getInstalledUsers();
|
json.put("pendingBackups", convertToJsonArray(pendingBackups));
|
json.put("pendingRestores", convertToJsonArray(pendingRestores));
|
|
json.put("isApex", info.isApex());
|
|
json.put("installedUsers", convertToJsonArray(installedUsers));
|
json.put("ceSnapshotInodes", ceSnapshotInodesToJson(info.getCeSnapshotInodes()));
|
|
return json;
|
}
|
|
private static PackageRollbackInfo packageRollbackInfoFromJson(JSONObject json)
|
throws JSONException {
|
VersionedPackage versionRolledBackFrom = versionedPackageFromJson(
|
json.getJSONObject("versionRolledBackFrom"));
|
VersionedPackage versionRolledBackTo = versionedPackageFromJson(
|
json.getJSONObject("versionRolledBackTo"));
|
|
final IntArray pendingBackups = convertToIntArray(
|
json.getJSONArray("pendingBackups"));
|
final ArrayList<RestoreInfo> pendingRestores = convertToRestoreInfoArray(
|
json.getJSONArray("pendingRestores"));
|
|
final boolean isApex = json.getBoolean("isApex");
|
|
final IntArray installedUsers = convertToIntArray(json.getJSONArray("installedUsers"));
|
final SparseLongArray ceSnapshotInodes = ceSnapshotInodesFromJson(
|
json.getJSONArray("ceSnapshotInodes"));
|
|
return new PackageRollbackInfo(versionRolledBackFrom, versionRolledBackTo,
|
pendingBackups, pendingRestores, isApex, installedUsers, ceSnapshotInodes);
|
}
|
|
private static JSONArray versionedPackagesToJson(List<VersionedPackage> packages)
|
throws JSONException {
|
JSONArray json = new JSONArray();
|
for (VersionedPackage pkg : packages) {
|
json.put(toJson(pkg));
|
}
|
return json;
|
}
|
|
private static List<VersionedPackage> versionedPackagesFromJson(JSONArray json)
|
throws JSONException {
|
List<VersionedPackage> packages = new ArrayList<>();
|
for (int i = 0; i < json.length(); ++i) {
|
packages.add(versionedPackageFromJson(json.getJSONObject(i)));
|
}
|
return packages;
|
}
|
|
private static JSONArray toJson(List<PackageRollbackInfo> infos) throws JSONException {
|
JSONArray json = new JSONArray();
|
for (PackageRollbackInfo info : infos) {
|
json.put(toJson(info));
|
}
|
return json;
|
}
|
|
private static List<PackageRollbackInfo> packageRollbackInfosFromJson(JSONArray json)
|
throws JSONException {
|
List<PackageRollbackInfo> infos = new ArrayList<>();
|
for (int i = 0; i < json.length(); ++i) {
|
infos.add(packageRollbackInfoFromJson(json.getJSONObject(i)));
|
}
|
return infos;
|
}
|
|
/**
|
* 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 static void removeFile(File file) {
|
if (file.isDirectory()) {
|
for (File child : file.listFiles()) {
|
removeFile(child);
|
}
|
}
|
if (file.exists()) {
|
file.delete();
|
}
|
}
|
}
|