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     void delete(@NonNull String fileName) {
89         synchronized (this) {
90             mScheduledFileDataMap.remove(fileName);
91         }
92         final File file = getFile(fileName);
93         if (!file.exists()) {
94             return;
95         }
96         if (!file.delete()) {
97             Slog.e(TAG, "Failed to delete file: " + file.getPath());
98         }
99     }
100 
101     @WorkerThread
writeTo(@onNull String fileName, @NonNull T data)102     void writeTo(@NonNull String fileName, @NonNull T data) {
103         final File file = getFile(fileName);
104         final AtomicFile atomicFile = new AtomicFile(file);
105 
106         FileOutputStream fileOutputStream = null;
107         try {
108             fileOutputStream = atomicFile.startWrite();
109         } catch (IOException e) {
110             Slog.e(TAG, "Failed to write to protobuf file.", e);
111             return;
112         }
113 
114         try {
115             final ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
116             protoStreamWriter().write(protoOutputStream, data);
117             protoOutputStream.flush();
118             atomicFile.finishWrite(fileOutputStream);
119             fileOutputStream = null;
120         } finally {
121             // When fileInputStream is null (successful write), this will no-op.
122             atomicFile.failWrite(fileOutputStream);
123         }
124     }
125 
126     @WorkerThread
127     @Nullable
read(@onNull String fileName)128     T read(@NonNull String fileName) {
129         File[] files = mRootDir.listFiles(
130                 pathname -> pathname.isFile() && pathname.getName().equals(fileName));
131         if (files == null || files.length == 0) {
132             return null;
133         } else if (files.length > 1) {
134             // This can't possibly happen, but validity check.
135             Slog.w(TAG, "Found multiple files with the same name: " + Arrays.toString(files));
136         }
137         return parseFile(files[0]);
138     }
139 
140     /**
141      * Schedules the specified data to be flushed to a file in the future. Subsequent
142      * calls for the same file before the flush occurs will replace the previous data but will not
143      * reset when the flush will occur. All unique files will be flushed at the same time.
144      */
145     @MainThread
scheduleSave(@onNull String fileName, @NonNull T data)146     synchronized void scheduleSave(@NonNull String fileName, @NonNull T data) {
147         mScheduledFileDataMap.put(fileName, data);
148 
149         if (mScheduledExecutorService.isShutdown()) {
150             Slog.e(TAG, "Worker is shutdown, failed to schedule data saving.");
151             return;
152         }
153 
154         // Skip scheduling another flush when one is pending.
155         if (mScheduledFuture != null) {
156             return;
157         }
158 
159         mScheduledFuture = mScheduledExecutorService.schedule(this::flushScheduledData,
160                 DEFAULT_DISK_WRITE_DELAY, TimeUnit.MILLISECONDS);
161     }
162 
163     /**
164      * Saves specified data immediately on a background thread, and blocks until its completed. This
165      * is useful for when device is powering off.
166      */
167     @MainThread
saveImmediately(@onNull String fileName, @NonNull T data)168     void saveImmediately(@NonNull String fileName, @NonNull T data) {
169         synchronized (this) {
170             mScheduledFileDataMap.put(fileName, data);
171         }
172         triggerScheduledFlushEarly();
173     }
174 
175     @MainThread
triggerScheduledFlushEarly()176     private void triggerScheduledFlushEarly() {
177         synchronized (this) {
178             if (mScheduledFileDataMap.isEmpty() || mScheduledExecutorService.isShutdown()) {
179                 return;
180             }
181             // Cancel existing future.
182             if (mScheduledFuture != null) {
183                 mScheduledFuture.cancel(true);
184             }
185         }
186 
187         // Submit flush and blocks until it completes. Blocking will prevent the device from
188         // shutting down before flushing completes.
189         Future<?> future = mScheduledExecutorService.submit(this::flushScheduledData);
190         try {
191             future.get(SHUTDOWN_DISK_WRITE_TIMEOUT, TimeUnit.MILLISECONDS);
192         } catch (InterruptedException | ExecutionException | TimeoutException e) {
193             Slog.e(TAG, "Failed to save data immediately.", e);
194         }
195     }
196 
197     @WorkerThread
flushScheduledData()198     private synchronized void flushScheduledData() {
199         if (mScheduledFileDataMap.isEmpty()) {
200             mScheduledFuture = null;
201             return;
202         }
203         for (String fileName : mScheduledFileDataMap.keySet()) {
204             T data = mScheduledFileDataMap.get(fileName);
205             writeTo(fileName, data);
206         }
207         mScheduledFileDataMap.clear();
208         mScheduledFuture = null;
209     }
210 
211     @WorkerThread
212     @Nullable
parseFile(@onNull File file)213     private T parseFile(@NonNull File file) {
214         final AtomicFile atomicFile = new AtomicFile(file);
215         try (FileInputStream fileInputStream = atomicFile.openRead()) {
216             final ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
217             return protoStreamReader().read(protoInputStream);
218         } catch (IOException e) {
219             Slog.e(TAG, "Failed to parse protobuf file.", e);
220         }
221         return null;
222     }
223 
224     @NonNull
getFile(String fileName)225     private File getFile(String fileName) {
226         return new File(mRootDir, fileName);
227     }
228 
229     /**
230      * {@code ProtoStreamWriter} writes {@code T} fields to {@link ProtoOutputStream}.
231      *
232      * @param <T> is the data class representation of a protobuf.
233      */
234     interface ProtoStreamWriter<T> {
235 
236         /**
237          * Writes {@code T} to {@link ProtoOutputStream}.
238          */
write(@onNull ProtoOutputStream protoOutputStream, @NonNull T data)239         void write(@NonNull ProtoOutputStream protoOutputStream, @NonNull T data);
240     }
241 
242     /**
243      * {@code ProtoStreamReader} reads {@link ProtoInputStream} and translate it to {@code T}.
244      *
245      * @param <T> is the data class representation of a protobuf.
246      */
247     interface ProtoStreamReader<T> {
248         /**
249          * Reads {@link ProtoInputStream} and translates it to {@code T}.
250          */
251         @Nullable
read(@onNull ProtoInputStream protoInputStream)252         T read(@NonNull ProtoInputStream protoInputStream);
253     }
254 }
255