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