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