1 /*
2  * Copyright (C) 2017 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 
17 package com.android.server.backup;
18 
19 import android.util.Slog;
20 
21 import com.android.internal.annotations.GuardedBy;
22 
23 import java.io.BufferedInputStream;
24 import java.io.DataInputStream;
25 import java.io.EOFException;
26 import java.io.File;
27 import java.io.FileInputStream;
28 import java.io.IOException;
29 import java.io.RandomAccessFile;
30 import java.util.HashSet;
31 import java.util.Set;
32 
33 /**
34  * Records which apps have been backed up on this device, persisting it to disk so that it can be
35  * read at subsequent boots. This class is threadsafe.
36  *
37  * <p>This is used to decide, when restoring a package at install time, whether it has been
38  * previously backed up on the current device. If it has been previously backed up it should
39  * restore from the same restore set that the current device has been backing up to. If it has not
40  * been previously backed up, it should restore from the ancestral restore set (i.e., the restore
41  * set that the user's previous device was backing up to).
42  *
43  * <p>NB: this is always backed by the same files within the state directory supplied at
44  * construction.
45  */
46 final class ProcessedPackagesJournal {
47     private static final String TAG = "ProcessedPackagesJournal";
48     private static final String JOURNAL_FILE_NAME = "processed";
49     private static final boolean DEBUG = BackupManagerService.DEBUG;
50 
51     // using HashSet instead of ArraySet since we expect 100-500 elements range
52     @GuardedBy("mProcessedPackages")
53     private final Set<String> mProcessedPackages = new HashSet<>();
54     // TODO: at some point consider splitting the bookkeeping to be per-transport
55     private final File mStateDirectory;
56 
57     /**
58      * Constructs a new journal.
59      *
60      * After constructing the object one should call {@link #init()} to load state from disk if
61      * it has been previously persisted.
62      *
63      * @param stateDirectory The directory in which backup state (including journals) is stored.
64      */
ProcessedPackagesJournal(File stateDirectory)65     ProcessedPackagesJournal(File stateDirectory) {
66         mStateDirectory = stateDirectory;
67     }
68 
69     /**
70      * Loads state from disk if it has been previously persisted.
71      */
init()72     void init() {
73         synchronized (mProcessedPackages) {
74             loadFromDisk();
75         }
76     }
77 
78     /**
79      * Returns {@code true} if {@code packageName} has previously been backed up.
80      */
hasBeenProcessed(String packageName)81     boolean hasBeenProcessed(String packageName) {
82         synchronized (mProcessedPackages) {
83             return mProcessedPackages.contains(packageName);
84         }
85     }
86 
addPackage(String packageName)87     void addPackage(String packageName) {
88         synchronized (mProcessedPackages) {
89             if (!mProcessedPackages.add(packageName)) {
90                 // This package has already been processed - no need to add it to the journal.
91                 return;
92             }
93 
94             File journalFile = new File(mStateDirectory, JOURNAL_FILE_NAME);
95 
96             try (RandomAccessFile out = new RandomAccessFile(journalFile, "rws")) {
97                 out.seek(out.length());
98                 out.writeUTF(packageName);
99             } catch (IOException e) {
100                 Slog.e(TAG, "Can't log backup of " + packageName + " to " + journalFile);
101             }
102         }
103     }
104 
105     /**
106      * A copy of the current state of the journal.
107      *
108      * <p>Used only for dumping out information for logging. {@link #hasBeenProcessed(String)}
109      * should be used for efficiently checking whether a package has been backed up before by this
110      * device.
111      *
112      * @return The current set of packages that have been backed up previously.
113      */
getPackagesCopy()114     Set<String> getPackagesCopy() {
115         synchronized (mProcessedPackages) {
116             return new HashSet<>(mProcessedPackages);
117         }
118     }
119 
reset()120     void reset() {
121         synchronized (mProcessedPackages) {
122             mProcessedPackages.clear();
123             File journalFile = new File(mStateDirectory, JOURNAL_FILE_NAME);
124             journalFile.delete();
125         }
126     }
127 
loadFromDisk()128     private void loadFromDisk() {
129         File journalFile = new File(mStateDirectory, JOURNAL_FILE_NAME);
130 
131         if (!journalFile.exists()) {
132             return;
133         }
134 
135         try (DataInputStream oldJournal = new DataInputStream(
136                 new BufferedInputStream(new FileInputStream(journalFile)))) {
137             while (true) {
138                 String packageName = oldJournal.readUTF();
139                 if (DEBUG) {
140                     Slog.v(TAG, "   + " + packageName);
141                 }
142                 mProcessedPackages.add(packageName);
143             }
144         } catch (EOFException e) {
145             // Successfully loaded journal file
146         } catch (IOException e) {
147             Slog.e(TAG, "Error reading processed packages journal", e);
148         }
149     }
150 }
151