• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.people.data;
18 
19 import android.annotation.MainThread;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.WorkerThread;
23 import android.text.format.DateUtils;
24 import android.util.ArrayMap;
25 import android.util.AtomicFile;
26 import android.util.Slog;
27 import android.util.proto.ProtoInputStream;
28 import android.util.proto.ProtoOutputStream;
29 
30 import com.android.internal.annotations.GuardedBy;
31 
32 import java.io.File;
33 import java.io.FileInputStream;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.util.Arrays;
37 import java.util.Map;
38 import java.util.concurrent.ExecutionException;
39 import java.util.concurrent.Future;
40 import java.util.concurrent.ScheduledExecutorService;
41 import java.util.concurrent.ScheduledFuture;
42 import java.util.concurrent.TimeUnit;
43 import java.util.concurrent.TimeoutException;
44 
45 /**
46  * Base class for reading and writing protobufs on disk from a root directory. Callers should
47  * ensure that the root directory is unlocked before doing I/O operations using this class.
48  *
49  * @param <T> is the data class representation of a protobuf.
50  */
51 abstract class AbstractProtoDiskReadWriter<T> {
52 
53     private static final String TAG = AbstractProtoDiskReadWriter.class.getSimpleName();
54 
55     // Common disk write delay that will be appropriate for most scenarios.
56     private static final long DEFAULT_DISK_WRITE_DELAY = 2L * DateUtils.MINUTE_IN_MILLIS;
57     private static final long SHUTDOWN_DISK_WRITE_TIMEOUT = 5L * DateUtils.SECOND_IN_MILLIS;
58 
59     private final File mRootDir;
60     private final ScheduledExecutorService mScheduledExecutorService;
61 
62     @GuardedBy("this")
63     private ScheduledFuture<?> mScheduledFuture;
64 
65     // File name -> data class
66     @GuardedBy("this")
67     private Map<String, T> mScheduledFileDataMap = new ArrayMap<>();
68 
69     /**
70      * Child class shall provide a {@link ProtoStreamWriter} to facilitate the writing of data as a
71      * protobuf on disk.
72      */
protoStreamWriter()73     abstract ProtoStreamWriter<T> protoStreamWriter();
74 
75     /**
76      * Child class shall provide a {@link ProtoStreamReader} to facilitate the reading of protobuf
77      * data on disk.
78      */
protoStreamReader()79     abstract ProtoStreamReader<T> protoStreamReader();
80 
AbstractProtoDiskReadWriter(@onNull File rootDir, @NonNull ScheduledExecutorService scheduledExecutorService)81     AbstractProtoDiskReadWriter(@NonNull File rootDir,
82             @NonNull ScheduledExecutorService scheduledExecutorService) {
83         mRootDir = rootDir;
84         mScheduledExecutorService = scheduledExecutorService;
85     }
86 
87     @WorkerThread
delete(@onNull String fileName)88     synchronized void delete(@NonNull String fileName) {
89         mScheduledFileDataMap.remove(fileName);
90         final File file = getFile(fileName);
91         if (!file.exists()) {
92             return;
93         }
94         if (!file.delete()) {
95             Slog.e(TAG, "Failed to delete file: " + file.getPath());
96         }
97     }
98 
99     @WorkerThread
writeTo(@onNull String fileName, @NonNull T data)100     void writeTo(@NonNull String fileName, @NonNull T data) {
101         final File file = getFile(fileName);
102         final AtomicFile atomicFile = new AtomicFile(file);
103 
104         FileOutputStream fileOutputStream = null;
105         try {
106             fileOutputStream = atomicFile.startWrite();
107         } catch (IOException e) {
108             Slog.e(TAG, "Failed to write to protobuf file.", e);
109             return;
110         }
111 
112         try {
113             final ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
114             protoStreamWriter().write(protoOutputStream, data);
115             protoOutputStream.flush();
116             atomicFile.finishWrite(fileOutputStream);
117             fileOutputStream = null;
118         } finally {
119             // When fileInputStream is null (successful write), this will no-op.
120             atomicFile.failWrite(fileOutputStream);
121         }
122     }
123 
124     @WorkerThread
125     @Nullable
read(@onNull String fileName)126     T read(@NonNull String fileName) {
127         File[] files = mRootDir.listFiles(
128                 pathname -> pathname.isFile() && pathname.getName().equals(fileName));
129         if (files == null || files.length == 0) {
130             return null;
131         } else if (files.length > 1) {
132             // This can't possibly happen, but sanity check.
133             Slog.w(TAG, "Found multiple files with the same name: " + Arrays.toString(files));
134         }
135         return parseFile(files[0]);
136     }
137 
138     /**
139      * Reads all files in directory and returns a map with file names as keys and parsed file
140      * contents as values.
141      */
142     @WorkerThread
143     @Nullable
readAll()144     Map<String, T> readAll() {
145         File[] files = mRootDir.listFiles(File::isFile);
146         if (files == null) {
147             return null;
148         }
149 
150         Map<String, T> results = new ArrayMap<>();
151         for (File file : files) {
152             T result = parseFile(file);
153             if (result != null) {
154                 results.put(file.getName(), result);
155             }
156         }
157         return results;
158     }
159 
160     /**
161      * Schedules the specified data to be flushed to a file in the future. Subsequent
162      * calls for the same file before the flush occurs will replace the previous data but will not
163      * reset when the flush will occur. All unique files will be flushed at the same time.
164      */
165     @MainThread
scheduleSave(@onNull String fileName, @NonNull T data)166     synchronized void scheduleSave(@NonNull String fileName, @NonNull T data) {
167         mScheduledFileDataMap.put(fileName, data);
168 
169         if (mScheduledExecutorService.isShutdown()) {
170             Slog.e(TAG, "Worker is shutdown, failed to schedule data saving.");
171             return;
172         }
173 
174         // Skip scheduling another flush when one is pending.
175         if (mScheduledFuture != null) {
176             return;
177         }
178 
179         mScheduledFuture = mScheduledExecutorService.schedule(this::flushScheduledData,
180                 DEFAULT_DISK_WRITE_DELAY, TimeUnit.MILLISECONDS);
181     }
182 
183     /**
184      * Saves specified data immediately on a background thread, and blocks until its completed. This
185      * is useful for when device is powering off.
186      */
187     @MainThread
saveImmediately(@onNull String fileName, @NonNull T data)188     synchronized void saveImmediately(@NonNull String fileName, @NonNull T data) {
189         mScheduledFileDataMap.put(fileName, data);
190         triggerScheduledFlushEarly();
191     }
192 
193     @MainThread
triggerScheduledFlushEarly()194     private synchronized void triggerScheduledFlushEarly() {
195         if (mScheduledFileDataMap.isEmpty() || mScheduledExecutorService.isShutdown()) {
196             return;
197         }
198         // Cancel existing future.
199         if (mScheduledFuture != null) {
200 
201             // We shouldn't need to interrupt as this method and threaded task
202             // #flushScheduledData are both synchronized.
203             mScheduledFuture.cancel(true);
204         }
205 
206         // Submit flush and blocks until it completes. Blocking will prevent the device from
207         // shutting down before flushing completes.
208         Future<?> future = mScheduledExecutorService.submit(this::flushScheduledData);
209         try {
210             future.get(SHUTDOWN_DISK_WRITE_TIMEOUT, TimeUnit.MILLISECONDS);
211         } catch (InterruptedException | ExecutionException | TimeoutException e) {
212             Slog.e(TAG, "Failed to save data immediately.", e);
213         }
214     }
215 
216     @WorkerThread
flushScheduledData()217     private synchronized void flushScheduledData() {
218         if (mScheduledFileDataMap.isEmpty()) {
219             mScheduledFuture = null;
220             return;
221         }
222         for (String fileName : mScheduledFileDataMap.keySet()) {
223             T data = mScheduledFileDataMap.get(fileName);
224             writeTo(fileName, data);
225         }
226         mScheduledFileDataMap.clear();
227         mScheduledFuture = null;
228     }
229 
230     @WorkerThread
231     @Nullable
parseFile(@onNull File file)232     private T parseFile(@NonNull File file) {
233         final AtomicFile atomicFile = new AtomicFile(file);
234         try (FileInputStream fileInputStream = atomicFile.openRead()) {
235             final ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
236             return protoStreamReader().read(protoInputStream);
237         } catch (IOException e) {
238             Slog.e(TAG, "Failed to parse protobuf file.", e);
239         }
240         return null;
241     }
242 
243     @NonNull
getFile(String fileName)244     private File getFile(String fileName) {
245         return new File(mRootDir, fileName);
246     }
247 
248     /**
249      * {@code ProtoStreamWriter} writes {@code T} fields to {@link ProtoOutputStream}.
250      *
251      * @param <T> is the data class representation of a protobuf.
252      */
253     interface ProtoStreamWriter<T> {
254 
255         /**
256          * Writes {@code T} to {@link ProtoOutputStream}.
257          */
write(@onNull ProtoOutputStream protoOutputStream, @NonNull T data)258         void write(@NonNull ProtoOutputStream protoOutputStream, @NonNull T data);
259     }
260 
261     /**
262      * {@code ProtoStreamReader} reads {@link ProtoInputStream} and translate it to {@code T}.
263      *
264      * @param <T> is the data class representation of a protobuf.
265      */
266     interface ProtoStreamReader<T> {
267         /**
268          * Reads {@link ProtoInputStream} and translates it to {@code T}.
269          */
270         @Nullable
read(@onNull ProtoInputStream protoInputStream)271         T read(@NonNull ProtoInputStream protoInputStream);
272     }
273 }
274