ronnie
2022-10-14 1504bb53e29d3d46222c0b3ea994fc494b48e153
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
/*
 * 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();
        }
    }
}