blob: 4f8f6856bb6f59c64ceda9a4864e4cebb56d198f [file] [log] [blame]
Richard Uhler28e73232019-01-21 16:48:55 +00001/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.rollback;
18
Narayan Kamathc034fe92019-01-23 10:48:17 +000019import android.annotation.NonNull;
Richard Uhlera7e9b2d2019-01-22 17:20:58 +000020import android.content.pm.VersionedPackage;
Richard Uhler28e73232019-01-21 16:48:55 +000021import android.content.rollback.PackageRollbackInfo;
Narayan Kamathc034fe92019-01-23 10:48:17 +000022import android.content.rollback.PackageRollbackInfo.RestoreInfo;
Richard Uhler28e73232019-01-21 16:48:55 +000023import android.content.rollback.RollbackInfo;
Narayan Kamathc034fe92019-01-23 10:48:17 +000024import android.util.IntArray;
Richard Uhler28e73232019-01-21 16:48:55 +000025import android.util.Log;
Nikita Ioffe952aa7b2019-01-28 19:49:56 +000026import android.util.SparseLongArray;
Richard Uhler28e73232019-01-21 16:48:55 +000027
28import libcore.io.IoUtils;
29
30import org.json.JSONArray;
31import org.json.JSONException;
32import org.json.JSONObject;
33
34import java.io.File;
35import java.io.IOException;
36import java.io.PrintWriter;
Richard Uhler1f571c62019-01-31 15:16:46 +000037import java.nio.file.Files;
Richard Uhler6f8a33b2019-02-26 10:40:36 +000038import java.text.ParseException;
Richard Uhler28e73232019-01-21 16:48:55 +000039import java.time.Instant;
40import java.time.format.DateTimeParseException;
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * Helper class for loading and saving rollback data to persistent storage.
46 */
47class RollbackStore {
48 private static final String TAG = "RollbackManager";
49
50 // Assuming the rollback data directory is /data/rollback, we use the
Richard Uhler6f8a33b2019-02-26 10:40:36 +000051 // following directory structure to store persisted data for rollbacks:
Richard Uhler28e73232019-01-21 16:48:55 +000052 // /data/rollback/
Richard Uhler6f8a33b2019-02-26 10:40:36 +000053 // XXX/
54 // rollback.json
55 // com.package.A/
56 // base.apk
57 // com.package.B/
58 // base.apk
59 // YYY/
60 // rollback.json
Richard Uhler28e73232019-01-21 16:48:55 +000061 //
Richard Uhlerb9d54472019-01-22 12:50:08 +000062 // * XXX, YYY are the rollbackIds for the corresponding rollbacks.
Richard Uhler6f8a33b2019-02-26 10:40:36 +000063 // * rollback.json contains all relevant metadata for the rollback.
Richard Uhler28e73232019-01-21 16:48:55 +000064 //
65 // TODO: Use AtomicFile for all the .json files?
66 private final File mRollbackDataDir;
Richard Uhler28e73232019-01-21 16:48:55 +000067
68 RollbackStore(File rollbackDataDir) {
69 mRollbackDataDir = rollbackDataDir;
Richard Uhler28e73232019-01-21 16:48:55 +000070 }
71
72 /**
Richard Uhler6f8a33b2019-02-26 10:40:36 +000073 * Reads the rollback data from persistent storage.
Richard Uhler28e73232019-01-21 16:48:55 +000074 */
Richard Uhler6f8a33b2019-02-26 10:40:36 +000075 List<RollbackData> loadAllRollbackData() {
76 List<RollbackData> rollbacks = new ArrayList<>();
77 mRollbackDataDir.mkdirs();
78 for (File rollbackDir : mRollbackDataDir.listFiles()) {
Richard Uhler28e73232019-01-21 16:48:55 +000079 if (rollbackDir.isDirectory()) {
80 try {
Richard Uhler6f8a33b2019-02-26 10:40:36 +000081 rollbacks.add(loadRollbackData(rollbackDir));
Richard Uhler28e73232019-01-21 16:48:55 +000082 } catch (IOException e) {
Richard Uhler28e73232019-01-21 16:48:55 +000083 Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
84 removeFile(rollbackDir);
85 }
86 }
87 }
Richard Uhler6f8a33b2019-02-26 10:40:36 +000088 return rollbacks;
Richard Uhler28e73232019-01-21 16:48:55 +000089 }
90
91 /**
Narayan Kamathc034fe92019-01-23 10:48:17 +000092 * Converts an {@code JSONArray} of integers to an {@code IntArray}.
93 */
94 private static @NonNull IntArray convertToIntArray(@NonNull JSONArray jsonArray)
95 throws JSONException {
96 if (jsonArray.length() == 0) {
97 return new IntArray();
98 }
99
100 final int[] ret = new int[jsonArray.length()];
101 for (int i = 0; i < ret.length; ++i) {
102 ret[i] = jsonArray.getInt(i);
103 }
104
105 return IntArray.wrap(ret);
106 }
107
108 /**
109 * Converts an {@code IntArray} into an {@code JSONArray} of integers.
110 */
111 private static @NonNull JSONArray convertToJsonArray(@NonNull IntArray intArray) {
112 JSONArray jsonArray = new JSONArray();
113 for (int i = 0; i < intArray.size(); ++i) {
114 jsonArray.put(intArray.get(i));
115 }
116
117 return jsonArray;
118 }
119
120 private static @NonNull JSONArray convertToJsonArray(@NonNull List<RestoreInfo> list)
121 throws JSONException {
122 JSONArray jsonArray = new JSONArray();
123 for (RestoreInfo ri : list) {
124 JSONObject jo = new JSONObject();
125 jo.put("userId", ri.userId);
126 jo.put("appId", ri.appId);
127 jo.put("seInfo", ri.seInfo);
128 jsonArray.put(jo);
129 }
130
131 return jsonArray;
132 }
133
134 private static @NonNull ArrayList<RestoreInfo> convertToRestoreInfoArray(
135 @NonNull JSONArray array) throws JSONException {
136 ArrayList<RestoreInfo> restoreInfos = new ArrayList<>();
137
138 for (int i = 0; i < array.length(); ++i) {
139 JSONObject jo = array.getJSONObject(i);
140 restoreInfos.add(new RestoreInfo(
141 jo.getInt("userId"),
142 jo.getInt("appId"),
143 jo.getString("seInfo")));
144 }
145
146 return restoreInfos;
147 }
148
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000149 private static @NonNull JSONArray ceSnapshotInodesToJson(
150 @NonNull SparseLongArray ceSnapshotInodes) throws JSONException {
151 JSONArray array = new JSONArray();
152 for (int i = 0; i < ceSnapshotInodes.size(); i++) {
153 JSONObject entryJson = new JSONObject();
154 entryJson.put("userId", ceSnapshotInodes.keyAt(i));
155 entryJson.put("ceSnapshotInode", ceSnapshotInodes.valueAt(i));
156 array.put(entryJson);
157 }
158 return array;
159 }
160
161 private static @NonNull SparseLongArray ceSnapshotInodesFromJson(JSONArray json)
162 throws JSONException {
163 SparseLongArray ceSnapshotInodes = new SparseLongArray(json.length());
164 for (int i = 0; i < json.length(); i++) {
165 JSONObject entry = json.getJSONObject(i);
166 ceSnapshotInodes.append(entry.getInt("userId"), entry.getLong("ceSnapshotInode"));
167 }
168 return ceSnapshotInodes;
169 }
170
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000171 private static JSONObject rollbackInfoToJson(RollbackInfo rollback) throws JSONException {
172 JSONObject json = new JSONObject();
173 json.put("rollbackId", rollback.getRollbackId());
174 json.put("packages", toJson(rollback.getPackages()));
175 json.put("isStaged", rollback.isStaged());
176 json.put("causePackages", versionedPackagesToJson(rollback.getCausePackages()));
177 json.put("committedSessionId", rollback.getCommittedSessionId());
178 return json;
179 }
180
181 private static RollbackInfo rollbackInfoFromJson(JSONObject json) throws JSONException {
182 return new RollbackInfo(
183 json.getInt("rollbackId"),
184 packageRollbackInfosFromJson(json.getJSONArray("packages")),
185 json.getBoolean("isStaged"),
186 versionedPackagesFromJson(json.getJSONArray("causePackages")),
187 json.getInt("committedSessionId"));
188 }
189
Narayan Kamathc034fe92019-01-23 10:48:17 +0000190 /**
Richard Uhlercca637a2019-02-27 11:50:48 +0000191 * Creates a new RollbackData instance for a non-staged rollback with
192 * backupDir assigned.
Richard Uhler28e73232019-01-21 16:48:55 +0000193 */
Richard Uhlercca637a2019-02-27 11:50:48 +0000194 RollbackData createNonStagedRollback(int rollbackId) throws IOException {
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000195 File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId));
Richard Uhlercca637a2019-02-27 11:50:48 +0000196 return new RollbackData(rollbackId, backupDir, -1);
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000197 }
198
Richard Uhlercca637a2019-02-27 11:50:48 +0000199 /**
200 * Creates a new RollbackData instance for a staged rollback with
201 * backupDir assigned.
202 */
203 RollbackData createStagedRollback(int rollbackId, int stagedSessionId)
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000204 throws IOException {
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000205 File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId));
Richard Uhlercca637a2019-02-27 11:50:48 +0000206 return new RollbackData(rollbackId, backupDir, stagedSessionId);
Richard Uhler28e73232019-01-21 16:48:55 +0000207 }
208
209 /**
Richard Uhlerab009ea2019-02-25 12:11:05 +0000210 * Creates a backup copy of an apk or apex for a package.
211 * For packages containing splits, this method should be called for each
212 * of the package's split apks in addition to the base apk.
Richard Uhler28e73232019-01-21 16:48:55 +0000213 */
Richard Uhlerab009ea2019-02-25 12:11:05 +0000214 static void backupPackageCodePath(RollbackData data, String packageName, String codePath)
Richard Uhler1f571c62019-01-31 15:16:46 +0000215 throws IOException {
216 File sourceFile = new File(codePath);
217 File targetDir = new File(data.backupDir, packageName);
218 targetDir.mkdirs();
219 File targetFile = new File(targetDir, sourceFile.getName());
220
221 // TODO: Copy by hard link instead to save on cpu and storage space?
222 Files.copy(sourceFile.toPath(), targetFile.toPath());
223 }
224
225 /**
Richard Uhlerab009ea2019-02-25 12:11:05 +0000226 * Returns the apk or apex files backed up for the given package.
227 * Includes the base apk and any splits. Returns null if none found.
Richard Uhler1f571c62019-01-31 15:16:46 +0000228 */
Richard Uhlerab009ea2019-02-25 12:11:05 +0000229 static File[] getPackageCodePaths(RollbackData data, String packageName) {
Richard Uhler1f571c62019-01-31 15:16:46 +0000230 File targetDir = new File(data.backupDir, packageName);
231 File[] files = targetDir.listFiles();
Richard Uhlerab009ea2019-02-25 12:11:05 +0000232 if (files == null || files.length == 0) {
Richard Uhler1f571c62019-01-31 15:16:46 +0000233 return null;
234 }
Richard Uhlerab009ea2019-02-25 12:11:05 +0000235 return files;
Richard Uhler28e73232019-01-21 16:48:55 +0000236 }
237
238 /**
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000239 * Deletes all backed up apks and apex files associated with the given
240 * rollback.
241 */
242 static void deletePackageCodePaths(RollbackData data) {
243 for (PackageRollbackInfo info : data.info.getPackages()) {
244 File targetDir = new File(data.backupDir, info.getPackageName());
245 removeFile(targetDir);
246 }
247 }
248
249 /**
Richard Uhleraad02c02019-02-27 12:57:20 +0000250 * Saves the rollback data to persistent storage.
Richard Uhler28e73232019-01-21 16:48:55 +0000251 */
Richard Uhleraad02c02019-02-27 12:57:20 +0000252 void saveRollbackData(RollbackData data) throws IOException {
Richard Uhler28e73232019-01-21 16:48:55 +0000253 try {
254 JSONObject dataJson = new JSONObject();
Richard Uhlercca637a2019-02-27 11:50:48 +0000255 dataJson.put("info", rollbackInfoToJson(data.info));
Richard Uhler28e73232019-01-21 16:48:55 +0000256 dataJson.put("timestamp", data.timestamp.toString());
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000257 dataJson.put("stagedSessionId", data.stagedSessionId);
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000258 dataJson.put("state", rollbackStateToString(data.state));
Richard Uhlerba13ab22019-02-05 15:27:12 +0000259 dataJson.put("apkSessionId", data.apkSessionId);
Richard Uhler479a2962019-02-27 10:59:10 +0000260 dataJson.put("restoreUserDataInProgress", data.restoreUserDataInProgress);
Richard Uhler28e73232019-01-21 16:48:55 +0000261
262 PrintWriter pw = new PrintWriter(new File(data.backupDir, "rollback.json"));
263 pw.println(dataJson.toString());
264 pw.close();
265 } catch (JSONException e) {
266 throw new IOException(e);
267 }
268 }
269
270 /**
Richard Uhleraad02c02019-02-27 12:57:20 +0000271 * Removes all persistant storage associated with the given rollback data.
Richard Uhler28e73232019-01-21 16:48:55 +0000272 */
Richard Uhleraad02c02019-02-27 12:57:20 +0000273 void deleteRollbackData(RollbackData data) {
Richard Uhler28e73232019-01-21 16:48:55 +0000274 removeFile(data.backupDir);
275 }
276
277 /**
Richard Uhler28e73232019-01-21 16:48:55 +0000278 * Reads the metadata for a rollback from the given directory.
279 * @throws IOException in case of error reading the data.
280 */
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000281 private static RollbackData loadRollbackData(File backupDir) throws IOException {
Richard Uhler28e73232019-01-21 16:48:55 +0000282 try {
Richard Uhler28e73232019-01-21 16:48:55 +0000283 File rollbackJsonFile = new File(backupDir, "rollback.json");
284 JSONObject dataJson = new JSONObject(
285 IoUtils.readFileAsString(rollbackJsonFile.getAbsolutePath()));
Richard Uhlerb9d54472019-01-22 12:50:08 +0000286
Richard Uhlercca637a2019-02-27 11:50:48 +0000287 return new RollbackData(
288 rollbackInfoFromJson(dataJson.getJSONObject("info")),
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000289 backupDir,
Richard Uhlercca637a2019-02-27 11:50:48 +0000290 Instant.parse(dataJson.getString("timestamp")),
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000291 dataJson.getInt("stagedSessionId"),
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000292 rollbackStateFromString(dataJson.getString("state")),
Richard Uhlercca637a2019-02-27 11:50:48 +0000293 dataJson.getInt("apkSessionId"),
294 dataJson.getBoolean("restoreUserDataInProgress"));
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000295 } catch (JSONException | DateTimeParseException | ParseException e) {
Richard Uhler28e73232019-01-21 16:48:55 +0000296 throw new IOException(e);
297 }
298 }
299
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000300 private static JSONObject toJson(VersionedPackage pkg) throws JSONException {
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000301 JSONObject json = new JSONObject();
302 json.put("packageName", pkg.getPackageName());
303 json.put("longVersionCode", pkg.getLongVersionCode());
304 return json;
305 }
306
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000307 private static VersionedPackage versionedPackageFromJson(JSONObject json) throws JSONException {
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000308 String packageName = json.getString("packageName");
309 long longVersionCode = json.getLong("longVersionCode");
310 return new VersionedPackage(packageName, longVersionCode);
311 }
312
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000313 private static JSONObject toJson(PackageRollbackInfo info) throws JSONException {
Richard Uhler0a79b322019-01-23 13:51:07 +0000314 JSONObject json = new JSONObject();
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000315 json.put("versionRolledBackFrom", toJson(info.getVersionRolledBackFrom()));
316 json.put("versionRolledBackTo", toJson(info.getVersionRolledBackTo()));
Narayan Kamathc034fe92019-01-23 10:48:17 +0000317
318 IntArray pendingBackups = info.getPendingBackups();
319 List<RestoreInfo> pendingRestores = info.getPendingRestores();
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000320 IntArray installedUsers = info.getInstalledUsers();
Narayan Kamathc034fe92019-01-23 10:48:17 +0000321 json.put("pendingBackups", convertToJsonArray(pendingBackups));
322 json.put("pendingRestores", convertToJsonArray(pendingRestores));
323
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000324 json.put("isApex", info.isApex());
325
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000326 json.put("installedUsers", convertToJsonArray(installedUsers));
327 json.put("ceSnapshotInodes", ceSnapshotInodesToJson(info.getCeSnapshotInodes()));
328
Richard Uhler0a79b322019-01-23 13:51:07 +0000329 return json;
330 }
331
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000332 private static PackageRollbackInfo packageRollbackInfoFromJson(JSONObject json)
333 throws JSONException {
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000334 VersionedPackage versionRolledBackFrom = versionedPackageFromJson(
335 json.getJSONObject("versionRolledBackFrom"));
336 VersionedPackage versionRolledBackTo = versionedPackageFromJson(
337 json.getJSONObject("versionRolledBackTo"));
Narayan Kamathc034fe92019-01-23 10:48:17 +0000338
339 final IntArray pendingBackups = convertToIntArray(
340 json.getJSONArray("pendingBackups"));
341 final ArrayList<RestoreInfo> pendingRestores = convertToRestoreInfoArray(
342 json.getJSONArray("pendingRestores"));
343
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000344 final boolean isApex = json.getBoolean("isApex");
345
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000346 final IntArray installedUsers = convertToIntArray(json.getJSONArray("installedUsers"));
347 final SparseLongArray ceSnapshotInodes = ceSnapshotInodesFromJson(
348 json.getJSONArray("ceSnapshotInodes"));
349
Narayan Kamathc034fe92019-01-23 10:48:17 +0000350 return new PackageRollbackInfo(versionRolledBackFrom, versionRolledBackTo,
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000351 pendingBackups, pendingRestores, isApex, installedUsers, ceSnapshotInodes);
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000352 }
353
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000354 private static JSONArray versionedPackagesToJson(List<VersionedPackage> packages)
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000355 throws JSONException {
356 JSONArray json = new JSONArray();
357 for (VersionedPackage pkg : packages) {
358 json.put(toJson(pkg));
359 }
360 return json;
361 }
362
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000363 private static List<VersionedPackage> versionedPackagesFromJson(JSONArray json)
364 throws JSONException {
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000365 List<VersionedPackage> packages = new ArrayList<>();
366 for (int i = 0; i < json.length(); ++i) {
367 packages.add(versionedPackageFromJson(json.getJSONObject(i)));
368 }
369 return packages;
Richard Uhler0a79b322019-01-23 13:51:07 +0000370 }
371
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000372 private static JSONArray toJson(List<PackageRollbackInfo> infos) throws JSONException {
Richard Uhler0a79b322019-01-23 13:51:07 +0000373 JSONArray json = new JSONArray();
374 for (PackageRollbackInfo info : infos) {
375 json.put(toJson(info));
376 }
377 return json;
378 }
379
Richard Uhlerf16ebd02019-02-27 11:18:45 +0000380 private static List<PackageRollbackInfo> packageRollbackInfosFromJson(JSONArray json)
Richard Uhler0a79b322019-01-23 13:51:07 +0000381 throws JSONException {
382 List<PackageRollbackInfo> infos = new ArrayList<>();
383 for (int i = 0; i < json.length(); ++i) {
384 infos.add(packageRollbackInfoFromJson(json.getJSONObject(i)));
385 }
386 return infos;
387 }
388
Richard Uhler28e73232019-01-21 16:48:55 +0000389 /**
390 * Deletes a file completely.
391 * If the file is a directory, its contents are deleted as well.
392 * Has no effect if the directory does not exist.
393 */
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000394 private static void removeFile(File file) {
Richard Uhler28e73232019-01-21 16:48:55 +0000395 if (file.isDirectory()) {
396 for (File child : file.listFiles()) {
397 removeFile(child);
398 }
399 }
400 if (file.exists()) {
401 file.delete();
402 }
403 }
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000404
405 private static String rollbackStateToString(@RollbackData.RollbackState int state) {
406 switch (state) {
Richard Uhlere9aaf632019-03-01 16:03:01 +0000407 case RollbackData.ROLLBACK_STATE_ENABLING: return "enabling";
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000408 case RollbackData.ROLLBACK_STATE_AVAILABLE: return "available";
409 case RollbackData.ROLLBACK_STATE_COMMITTED: return "committed";
410 }
411 throw new AssertionError("Invalid rollback state: " + state);
412 }
413
414 private static @RollbackData.RollbackState int rollbackStateFromString(String state)
415 throws ParseException {
416 switch (state) {
Richard Uhlere9aaf632019-03-01 16:03:01 +0000417 case "enabling": return RollbackData.ROLLBACK_STATE_ENABLING;
Richard Uhler6f8a33b2019-02-26 10:40:36 +0000418 case "available": return RollbackData.ROLLBACK_STATE_AVAILABLE;
419 case "committed": return RollbackData.ROLLBACK_STATE_COMMITTED;
420 }
421 throw new ParseException("Invalid rollback state: " + state, 0);
422 }
Richard Uhler28e73232019-01-21 16:48:55 +0000423}