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.annotation.Nullable;
20 
21 import java.io.BufferedInputStream;
22 import java.io.DataInputStream;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.IOException;
26 import java.io.RandomAccessFile;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.function.Consumer;
30 
31 /**
32  * A journal of packages that have indicated that their data has changed (and therefore should be
33  * backed up in the next scheduled K/V backup pass).
34  *
35  * <p>This information is persisted to the filesystem so that it is not lost in the event of a
36  * reboot.
37  */
38 public class DataChangedJournal {
39     private static final String FILE_NAME_PREFIX = "journal";
40 
41     /**
42      * Journals tend to be on the order of a few kilobytes, hence setting the buffer size to 8kb.
43      */
44     private static final int BUFFER_SIZE_BYTES = 8 * 1024;
45 
46     private final File mFile;
47 
48     /**
49      * Constructs an instance that reads from and writes to the given file.
50      */
DataChangedJournal(File file)51     DataChangedJournal(File file) {
52         mFile = file;
53     }
54 
55     /**
56      * Adds the given package to the journal.
57      *
58      * @param packageName The name of the package whose data has changed.
59      * @throws IOException if there is an IO error writing to the journal file.
60      */
addPackage(String packageName)61     public void addPackage(String packageName) throws IOException {
62         try (RandomAccessFile out = new RandomAccessFile(mFile, "rws")) {
63             out.seek(out.length());
64             out.writeUTF(packageName);
65         }
66     }
67 
68     /**
69      * Invokes {@link Consumer#accept(Object)} with every package name in the journal file.
70      *
71      * @param consumer The callback.
72      * @throws IOException If there is an IO error reading from the file.
73      */
forEach(Consumer<String> consumer)74     public void forEach(Consumer<String> consumer) throws IOException {
75         try (
76             BufferedInputStream bufferedInputStream = new BufferedInputStream(
77                     new FileInputStream(mFile), BUFFER_SIZE_BYTES);
78             DataInputStream dataInputStream = new DataInputStream(bufferedInputStream)
79         ) {
80             while (dataInputStream.available() > 0) {
81                 String packageName = dataInputStream.readUTF();
82                 consumer.accept(packageName);
83             }
84         }
85     }
86 
87     /**
88      * Returns a list with the packages in this journal.
89      *
90      * @throws IOException If there is an IO error reading from the file.
91      */
getPackages()92     public List<String> getPackages() throws IOException {
93         List<String> packages = new ArrayList<>();
94         forEach(packages::add);
95         return packages;
96     }
97 
98     /**
99      * Deletes the journal from the filesystem.
100      *
101      * @return {@code true} if successfully deleted journal.
102      */
delete()103     public boolean delete() {
104         return mFile.delete();
105     }
106 
107     @Override
equals(@ullable Object object)108     public boolean equals(@Nullable Object object) {
109         if (object instanceof DataChangedJournal) {
110             DataChangedJournal that = (DataChangedJournal) object;
111             try {
112                 return this.mFile.getCanonicalPath().equals(that.mFile.getCanonicalPath());
113             } catch (IOException exception) {
114                 return false;
115             }
116         }
117         return false;
118     }
119 
120     @Override
toString()121     public String toString() {
122         return mFile.toString();
123     }
124 
125     /**
126      * Creates a new journal with a random file name in the given journal directory.
127      *
128      * @param journalDirectory The directory where journals are kept.
129      * @return The journal.
130      * @throws IOException if there is an IO error creating the file.
131      */
newJournal(File journalDirectory)132     static DataChangedJournal newJournal(File journalDirectory) throws IOException {
133         return new DataChangedJournal(
134                 File.createTempFile(FILE_NAME_PREFIX, null, journalDirectory));
135     }
136 
137     /**
138      * Returns a list of journals in the given journal directory.
139      */
listJournals(File journalDirectory)140     static ArrayList<DataChangedJournal> listJournals(File journalDirectory) {
141         ArrayList<DataChangedJournal> journals = new ArrayList<>();
142         for (File file : journalDirectory.listFiles()) {
143             journals.add(new DataChangedJournal(file));
144         }
145         return journals;
146     }
147 }
148