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