blob: bb4e89eca5dac5f5637b5a2bc90cea2e2b3e45dd [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 Uhler28e73232019-01-21 16:48:55 +000038import java.time.Instant;
39import java.time.format.DateTimeParseException;
40import java.util.ArrayList;
41import java.util.List;
42
43/**
44 * Helper class for loading and saving rollback data to persistent storage.
45 */
46class RollbackStore {
47 private static final String TAG = "RollbackManager";
48
49 // Assuming the rollback data directory is /data/rollback, we use the
50 // following directory structure to store persisted data for available and
51 // recently executed rollbacks:
52 // /data/rollback/
53 // available/
54 // XXX/
55 // rollback.json
56 // com.package.A/
57 // base.apk
58 // com.package.B/
59 // base.apk
60 // YYY/
61 // rollback.json
62 // com.package.C/
63 // base.apk
64 // recently_executed.json
65 //
Richard Uhlerb9d54472019-01-22 12:50:08 +000066 // * XXX, YYY are the rollbackIds for the corresponding rollbacks.
Richard Uhler28e73232019-01-21 16:48:55 +000067 // * rollback.json contains all relevant metadata for the rollback. This
68 // file is not written until the rollback is made available.
69 //
70 // TODO: Use AtomicFile for all the .json files?
71 private final File mRollbackDataDir;
72 private final File mAvailableRollbacksDir;
73 private final File mRecentlyExecutedRollbacksFile;
74
75 RollbackStore(File rollbackDataDir) {
76 mRollbackDataDir = rollbackDataDir;
77 mAvailableRollbacksDir = new File(mRollbackDataDir, "available");
78 mRecentlyExecutedRollbacksFile = new File(mRollbackDataDir, "recently_executed.json");
79 }
80
81 /**
82 * Reads the list of available rollbacks from persistent storage.
83 */
84 List<RollbackData> loadAvailableRollbacks() {
85 List<RollbackData> availableRollbacks = new ArrayList<>();
86 mAvailableRollbacksDir.mkdirs();
87 for (File rollbackDir : mAvailableRollbacksDir.listFiles()) {
88 if (rollbackDir.isDirectory()) {
89 try {
90 RollbackData data = loadRollbackData(rollbackDir);
91 availableRollbacks.add(data);
92 } catch (IOException e) {
93 // Note: Deleting the rollbackDir here will cause pending
94 // rollbacks to be deleted. This should only ever happen
95 // if reloadPersistedData is called while there are
96 // pending rollbacks. The reloadPersistedData method is
97 // currently only for testing, so that should be okay.
98 Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
99 removeFile(rollbackDir);
100 }
101 }
102 }
103 return availableRollbacks;
104 }
105
106 /**
Narayan Kamathc034fe92019-01-23 10:48:17 +0000107 * Converts an {@code JSONArray} of integers to an {@code IntArray}.
108 */
109 private static @NonNull IntArray convertToIntArray(@NonNull JSONArray jsonArray)
110 throws JSONException {
111 if (jsonArray.length() == 0) {
112 return new IntArray();
113 }
114
115 final int[] ret = new int[jsonArray.length()];
116 for (int i = 0; i < ret.length; ++i) {
117 ret[i] = jsonArray.getInt(i);
118 }
119
120 return IntArray.wrap(ret);
121 }
122
123 /**
124 * Converts an {@code IntArray} into an {@code JSONArray} of integers.
125 */
126 private static @NonNull JSONArray convertToJsonArray(@NonNull IntArray intArray) {
127 JSONArray jsonArray = new JSONArray();
128 for (int i = 0; i < intArray.size(); ++i) {
129 jsonArray.put(intArray.get(i));
130 }
131
132 return jsonArray;
133 }
134
135 private static @NonNull JSONArray convertToJsonArray(@NonNull List<RestoreInfo> list)
136 throws JSONException {
137 JSONArray jsonArray = new JSONArray();
138 for (RestoreInfo ri : list) {
139 JSONObject jo = new JSONObject();
140 jo.put("userId", ri.userId);
141 jo.put("appId", ri.appId);
142 jo.put("seInfo", ri.seInfo);
143 jsonArray.put(jo);
144 }
145
146 return jsonArray;
147 }
148
149 private static @NonNull ArrayList<RestoreInfo> convertToRestoreInfoArray(
150 @NonNull JSONArray array) throws JSONException {
151 ArrayList<RestoreInfo> restoreInfos = new ArrayList<>();
152
153 for (int i = 0; i < array.length(); ++i) {
154 JSONObject jo = array.getJSONObject(i);
155 restoreInfos.add(new RestoreInfo(
156 jo.getInt("userId"),
157 jo.getInt("appId"),
158 jo.getString("seInfo")));
159 }
160
161 return restoreInfos;
162 }
163
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000164 private static @NonNull JSONArray ceSnapshotInodesToJson(
165 @NonNull SparseLongArray ceSnapshotInodes) throws JSONException {
166 JSONArray array = new JSONArray();
167 for (int i = 0; i < ceSnapshotInodes.size(); i++) {
168 JSONObject entryJson = new JSONObject();
169 entryJson.put("userId", ceSnapshotInodes.keyAt(i));
170 entryJson.put("ceSnapshotInode", ceSnapshotInodes.valueAt(i));
171 array.put(entryJson);
172 }
173 return array;
174 }
175
176 private static @NonNull SparseLongArray ceSnapshotInodesFromJson(JSONArray json)
177 throws JSONException {
178 SparseLongArray ceSnapshotInodes = new SparseLongArray(json.length());
179 for (int i = 0; i < json.length(); i++) {
180 JSONObject entry = json.getJSONObject(i);
181 ceSnapshotInodes.append(entry.getInt("userId"), entry.getLong("ceSnapshotInode"));
182 }
183 return ceSnapshotInodes;
184 }
185
Narayan Kamathc034fe92019-01-23 10:48:17 +0000186 /**
Richard Uhler28e73232019-01-21 16:48:55 +0000187 * Reads the list of recently executed rollbacks from persistent storage.
188 */
189 List<RollbackInfo> loadRecentlyExecutedRollbacks() {
190 List<RollbackInfo> recentlyExecutedRollbacks = new ArrayList<>();
191 if (mRecentlyExecutedRollbacksFile.exists()) {
192 try {
193 // TODO: How to cope with changes to the format of this file from
194 // when RollbackStore is updated in the future?
195 String jsonString = IoUtils.readFileAsString(
196 mRecentlyExecutedRollbacksFile.getAbsolutePath());
197 JSONObject object = new JSONObject(jsonString);
198 JSONArray array = object.getJSONArray("recentlyExecuted");
199 for (int i = 0; i < array.length(); ++i) {
200 JSONObject element = array.getJSONObject(i);
Richard Uhlerb9d54472019-01-22 12:50:08 +0000201 int rollbackId = element.getInt("rollbackId");
Richard Uhler0a79b322019-01-23 13:51:07 +0000202 List<PackageRollbackInfo> packages = packageRollbackInfosFromJson(
203 element.getJSONArray("packages"));
Richard Uhlerccf035d2019-02-04 14:04:52 +0000204 boolean isStaged = element.getBoolean("isStaged");
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000205 List<VersionedPackage> causePackages = versionedPackagesFromJson(
206 element.getJSONArray("causePackages"));
Richard Uhlerccf035d2019-02-04 14:04:52 +0000207 int committedSessionId = element.getInt("committedSessionId");
208 RollbackInfo rollback = new RollbackInfo(rollbackId, packages, isStaged,
209 causePackages, committedSessionId);
Richard Uhler28e73232019-01-21 16:48:55 +0000210 recentlyExecutedRollbacks.add(rollback);
211 }
212 } catch (IOException | JSONException e) {
213 // TODO: What to do here? Surely we shouldn't just forget about
214 // everything after the point of exception?
215 Log.e(TAG, "Failed to read recently executed rollbacks", e);
216 }
217 }
218
219 return recentlyExecutedRollbacks;
220 }
221
222 /**
223 * Creates a new RollbackData instance with backupDir assigned.
224 */
Richard Uhlerb9d54472019-01-22 12:50:08 +0000225 RollbackData createAvailableRollback(int rollbackId) throws IOException {
226 File backupDir = new File(mAvailableRollbacksDir, Integer.toString(rollbackId));
Richard Uhler60ac7062019-02-05 13:25:39 +0000227 return new RollbackData(rollbackId, backupDir, -1, true);
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000228 }
229
230 RollbackData createPendingStagedRollback(int rollbackId, int stagedSessionId)
231 throws IOException {
232 File backupDir = new File(mAvailableRollbacksDir, Integer.toString(rollbackId));
Richard Uhler60ac7062019-02-05 13:25:39 +0000233 return new RollbackData(rollbackId, backupDir, stagedSessionId, false);
Richard Uhler28e73232019-01-21 16:48:55 +0000234 }
235
236 /**
Richard Uhlerab009ea2019-02-25 12:11:05 +0000237 * Creates a backup copy of an apk or apex for a package.
238 * For packages containing splits, this method should be called for each
239 * of the package's split apks in addition to the base apk.
Richard Uhler28e73232019-01-21 16:48:55 +0000240 */
Richard Uhlerab009ea2019-02-25 12:11:05 +0000241 static void backupPackageCodePath(RollbackData data, String packageName, String codePath)
Richard Uhler1f571c62019-01-31 15:16:46 +0000242 throws IOException {
243 File sourceFile = new File(codePath);
244 File targetDir = new File(data.backupDir, packageName);
245 targetDir.mkdirs();
246 File targetFile = new File(targetDir, sourceFile.getName());
247
248 // TODO: Copy by hard link instead to save on cpu and storage space?
249 Files.copy(sourceFile.toPath(), targetFile.toPath());
250 }
251
252 /**
Richard Uhlerab009ea2019-02-25 12:11:05 +0000253 * Returns the apk or apex files backed up for the given package.
254 * Includes the base apk and any splits. Returns null if none found.
Richard Uhler1f571c62019-01-31 15:16:46 +0000255 */
Richard Uhlerab009ea2019-02-25 12:11:05 +0000256 static File[] getPackageCodePaths(RollbackData data, String packageName) {
Richard Uhler1f571c62019-01-31 15:16:46 +0000257 File targetDir = new File(data.backupDir, packageName);
258 File[] files = targetDir.listFiles();
Richard Uhlerab009ea2019-02-25 12:11:05 +0000259 if (files == null || files.length == 0) {
Richard Uhler1f571c62019-01-31 15:16:46 +0000260 return null;
261 }
Richard Uhlerab009ea2019-02-25 12:11:05 +0000262 return files;
Richard Uhler28e73232019-01-21 16:48:55 +0000263 }
264
265 /**
266 * Writes the metadata for an available rollback to persistent storage.
267 */
268 void saveAvailableRollback(RollbackData data) throws IOException {
269 try {
270 JSONObject dataJson = new JSONObject();
Richard Uhlerb9d54472019-01-22 12:50:08 +0000271 dataJson.put("rollbackId", data.rollbackId);
Richard Uhler0a79b322019-01-23 13:51:07 +0000272 dataJson.put("packages", toJson(data.packages));
Richard Uhler28e73232019-01-21 16:48:55 +0000273 dataJson.put("timestamp", data.timestamp.toString());
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000274 dataJson.put("stagedSessionId", data.stagedSessionId);
Richard Uhler60ac7062019-02-05 13:25:39 +0000275 dataJson.put("isAvailable", data.isAvailable);
Richard Uhlerba13ab22019-02-05 15:27:12 +0000276 dataJson.put("apkSessionId", data.apkSessionId);
Richard Uhler28e73232019-01-21 16:48:55 +0000277
278 PrintWriter pw = new PrintWriter(new File(data.backupDir, "rollback.json"));
279 pw.println(dataJson.toString());
280 pw.close();
281 } catch (JSONException e) {
282 throw new IOException(e);
283 }
284 }
285
286 /**
287 * Removes all persistant storage associated with the given available
288 * rollback.
289 */
290 void deleteAvailableRollback(RollbackData data) {
Richard Uhler28e73232019-01-21 16:48:55 +0000291 removeFile(data.backupDir);
292 }
293
294 /**
295 * Writes the list of recently executed rollbacks to storage.
296 */
297 void saveRecentlyExecutedRollbacks(List<RollbackInfo> recentlyExecutedRollbacks) {
298 try {
299 JSONObject json = new JSONObject();
300 JSONArray array = new JSONArray();
301 json.put("recentlyExecuted", array);
302
303 for (int i = 0; i < recentlyExecutedRollbacks.size(); ++i) {
304 RollbackInfo rollback = recentlyExecutedRollbacks.get(i);
305 JSONObject element = new JSONObject();
Richard Uhlerb9d54472019-01-22 12:50:08 +0000306 element.put("rollbackId", rollback.getRollbackId());
Richard Uhler0a79b322019-01-23 13:51:07 +0000307 element.put("packages", toJson(rollback.getPackages()));
Richard Uhlerccf035d2019-02-04 14:04:52 +0000308 element.put("isStaged", rollback.isStaged());
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000309 element.put("causePackages", versionedPackagesToJson(rollback.getCausePackages()));
Richard Uhlerccf035d2019-02-04 14:04:52 +0000310 element.put("committedSessionId", rollback.getCommittedSessionId());
Richard Uhler28e73232019-01-21 16:48:55 +0000311 array.put(element);
312 }
313
314 PrintWriter pw = new PrintWriter(mRecentlyExecutedRollbacksFile);
315 pw.println(json.toString());
316 pw.close();
317 } catch (IOException | JSONException e) {
318 // TODO: What to do here?
319 Log.e(TAG, "Failed to save recently executed rollbacks", e);
320 }
321 }
322
323 /**
324 * Reads the metadata for a rollback from the given directory.
325 * @throws IOException in case of error reading the data.
326 */
327 private RollbackData loadRollbackData(File backupDir) throws IOException {
328 try {
Richard Uhler28e73232019-01-21 16:48:55 +0000329 File rollbackJsonFile = new File(backupDir, "rollback.json");
330 JSONObject dataJson = new JSONObject(
331 IoUtils.readFileAsString(rollbackJsonFile.getAbsolutePath()));
Richard Uhlerb9d54472019-01-22 12:50:08 +0000332
333 int rollbackId = dataJson.getInt("rollbackId");
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000334 int stagedSessionId = dataJson.getInt("stagedSessionId");
Richard Uhler60ac7062019-02-05 13:25:39 +0000335 boolean isAvailable = dataJson.getBoolean("isAvailable");
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000336 RollbackData data = new RollbackData(rollbackId, backupDir,
Richard Uhler60ac7062019-02-05 13:25:39 +0000337 stagedSessionId, isAvailable);
Richard Uhler0a79b322019-01-23 13:51:07 +0000338 data.packages.addAll(packageRollbackInfosFromJson(dataJson.getJSONArray("packages")));
Richard Uhler28e73232019-01-21 16:48:55 +0000339 data.timestamp = Instant.parse(dataJson.getString("timestamp"));
Richard Uhlerba13ab22019-02-05 15:27:12 +0000340 data.apkSessionId = dataJson.getInt("apkSessionId");
Richard Uhler28e73232019-01-21 16:48:55 +0000341 return data;
342 } catch (JSONException | DateTimeParseException e) {
343 throw new IOException(e);
344 }
345 }
346
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000347 private JSONObject toJson(VersionedPackage pkg) throws JSONException {
348 JSONObject json = new JSONObject();
349 json.put("packageName", pkg.getPackageName());
350 json.put("longVersionCode", pkg.getLongVersionCode());
351 return json;
352 }
353
354 private VersionedPackage versionedPackageFromJson(JSONObject json) throws JSONException {
355 String packageName = json.getString("packageName");
356 long longVersionCode = json.getLong("longVersionCode");
357 return new VersionedPackage(packageName, longVersionCode);
358 }
359
Richard Uhler0a79b322019-01-23 13:51:07 +0000360 private JSONObject toJson(PackageRollbackInfo info) throws JSONException {
361 JSONObject json = new JSONObject();
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000362 json.put("versionRolledBackFrom", toJson(info.getVersionRolledBackFrom()));
363 json.put("versionRolledBackTo", toJson(info.getVersionRolledBackTo()));
Narayan Kamathc034fe92019-01-23 10:48:17 +0000364
365 IntArray pendingBackups = info.getPendingBackups();
366 List<RestoreInfo> pendingRestores = info.getPendingRestores();
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000367 IntArray installedUsers = info.getInstalledUsers();
Narayan Kamathc034fe92019-01-23 10:48:17 +0000368 json.put("pendingBackups", convertToJsonArray(pendingBackups));
369 json.put("pendingRestores", convertToJsonArray(pendingRestores));
370
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000371 json.put("isApex", info.isApex());
372
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000373 json.put("installedUsers", convertToJsonArray(installedUsers));
374 json.put("ceSnapshotInodes", ceSnapshotInodesToJson(info.getCeSnapshotInodes()));
375
Richard Uhler0a79b322019-01-23 13:51:07 +0000376 return json;
377 }
378
379 private PackageRollbackInfo packageRollbackInfoFromJson(JSONObject json) throws JSONException {
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000380 VersionedPackage versionRolledBackFrom = versionedPackageFromJson(
381 json.getJSONObject("versionRolledBackFrom"));
382 VersionedPackage versionRolledBackTo = versionedPackageFromJson(
383 json.getJSONObject("versionRolledBackTo"));
Narayan Kamathc034fe92019-01-23 10:48:17 +0000384
385 final IntArray pendingBackups = convertToIntArray(
386 json.getJSONArray("pendingBackups"));
387 final ArrayList<RestoreInfo> pendingRestores = convertToRestoreInfoArray(
388 json.getJSONArray("pendingRestores"));
389
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000390 final boolean isApex = json.getBoolean("isApex");
391
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000392 final IntArray installedUsers = convertToIntArray(json.getJSONArray("installedUsers"));
393 final SparseLongArray ceSnapshotInodes = ceSnapshotInodesFromJson(
394 json.getJSONArray("ceSnapshotInodes"));
395
Narayan Kamathc034fe92019-01-23 10:48:17 +0000396 return new PackageRollbackInfo(versionRolledBackFrom, versionRolledBackTo,
Nikita Ioffe952aa7b2019-01-28 19:49:56 +0000397 pendingBackups, pendingRestores, isApex, installedUsers, ceSnapshotInodes);
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000398 }
399
400 private JSONArray versionedPackagesToJson(List<VersionedPackage> packages)
401 throws JSONException {
402 JSONArray json = new JSONArray();
403 for (VersionedPackage pkg : packages) {
404 json.put(toJson(pkg));
405 }
406 return json;
407 }
408
409 private List<VersionedPackage> versionedPackagesFromJson(JSONArray json) throws JSONException {
410 List<VersionedPackage> packages = new ArrayList<>();
411 for (int i = 0; i < json.length(); ++i) {
412 packages.add(versionedPackageFromJson(json.getJSONObject(i)));
413 }
414 return packages;
Richard Uhler0a79b322019-01-23 13:51:07 +0000415 }
416
417 private JSONArray toJson(List<PackageRollbackInfo> infos) throws JSONException {
418 JSONArray json = new JSONArray();
419 for (PackageRollbackInfo info : infos) {
420 json.put(toJson(info));
421 }
422 return json;
423 }
424
425 private List<PackageRollbackInfo> packageRollbackInfosFromJson(JSONArray json)
426 throws JSONException {
427 List<PackageRollbackInfo> infos = new ArrayList<>();
428 for (int i = 0; i < json.length(); ++i) {
429 infos.add(packageRollbackInfoFromJson(json.getJSONObject(i)));
430 }
431 return infos;
432 }
433
Richard Uhler28e73232019-01-21 16:48:55 +0000434 /**
435 * Deletes a file completely.
436 * If the file is a directory, its contents are deleted as well.
437 * Has no effect if the directory does not exist.
438 */
439 private void removeFile(File file) {
440 if (file.isDirectory()) {
441 for (File child : file.listFiles()) {
442 removeFile(child);
443 }
444 }
445 if (file.exists()) {
446 file.delete();
447 }
448 }
449}