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