blob: 7672cd0040ec5dba7436c93b2fb9e4e72527c9e6 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.people.data;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Base class for reading and writing protobufs on disk from a root directory. Callers should
* ensure that the root directory is unlocked before doing I/O operations using this class.
*
* @param <T> is the data class representation of a protobuf.
*/
abstract class AbstractProtoDiskReadWriter<T> {
private static final String TAG = AbstractProtoDiskReadWriter.class.getSimpleName();
// Common disk write delay that will be appropriate for most scenarios.
private static final long DEFAULT_DISK_WRITE_DELAY = 2L * DateUtils.MINUTE_IN_MILLIS;
private static final long SHUTDOWN_DISK_WRITE_TIMEOUT = 5L * DateUtils.SECOND_IN_MILLIS;
private final File mRootDir;
private final ScheduledExecutorService mScheduledExecutorService;
@GuardedBy("this")
private ScheduledFuture<?> mScheduledFuture;
// File name -> data class
@GuardedBy("this")
private Map<String, T> mScheduledFileDataMap = new ArrayMap<>();
/**
* Child class shall provide a {@link ProtoStreamWriter} to facilitate the writing of data as a
* protobuf on disk.
*/
abstract ProtoStreamWriter<T> protoStreamWriter();
/**
* Child class shall provide a {@link ProtoStreamReader} to facilitate the reading of protobuf
* data on disk.
*/
abstract ProtoStreamReader<T> protoStreamReader();
AbstractProtoDiskReadWriter(@NonNull File rootDir,
@NonNull ScheduledExecutorService scheduledExecutorService) {
mRootDir = rootDir;
mScheduledExecutorService = scheduledExecutorService;
}
@WorkerThread
synchronized void delete(@NonNull String fileName) {
mScheduledFileDataMap.remove(fileName);
final File file = getFile(fileName);
if (!file.exists()) {
return;
}
if (!file.delete()) {
Slog.e(TAG, "Failed to delete file: " + file.getPath());
}
}
@WorkerThread
void writeTo(@NonNull String fileName, @NonNull T data) {
final File file = getFile(fileName);
final AtomicFile atomicFile = new AtomicFile(file);
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = atomicFile.startWrite();
} catch (IOException e) {
Slog.e(TAG, "Failed to write to protobuf file.", e);
return;
}
try {
final ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
protoStreamWriter().write(protoOutputStream, data);
protoOutputStream.flush();
atomicFile.finishWrite(fileOutputStream);
fileOutputStream = null;
} finally {
// When fileInputStream is null (successful write), this will no-op.
atomicFile.failWrite(fileOutputStream);
}
}
@WorkerThread
@Nullable
T read(@NonNull String fileName) {
File[] files = mRootDir.listFiles(
pathname -> pathname.isFile() && pathname.getName().equals(fileName));
if (files == null || files.length == 0) {
return null;
} else if (files.length > 1) {
// This can't possibly happen, but sanity check.
Slog.w(TAG, "Found multiple files with the same name: " + Arrays.toString(files));
}
return parseFile(files[0]);
}
/**
* Reads all files in directory and returns a map with file names as keys and parsed file
* contents as values.
*/
@WorkerThread
@Nullable
Map<String, T> readAll() {
File[] files = mRootDir.listFiles(File::isFile);
if (files == null) {
return null;
}
Map<String, T> results = new ArrayMap<>();
for (File file : files) {
T result = parseFile(file);
if (result != null) {
results.put(file.getName(), result);
}
}
return results;
}
/**
* Schedules the specified data to be flushed to a file in the future. Subsequent
* calls for the same file before the flush occurs will replace the previous data but will not
* reset when the flush will occur. All unique files will be flushed at the same time.
*/
@MainThread
synchronized void scheduleSave(@NonNull String fileName, @NonNull T data) {
mScheduledFileDataMap.put(fileName, data);
if (mScheduledExecutorService.isShutdown()) {
Slog.e(TAG, "Worker is shutdown, failed to schedule data saving.");
return;
}
// Skip scheduling another flush when one is pending.
if (mScheduledFuture != null) {
return;
}
mScheduledFuture = mScheduledExecutorService.schedule(this::flushScheduledData,
DEFAULT_DISK_WRITE_DELAY, TimeUnit.MILLISECONDS);
}
/**
* Saves specified data immediately on a background thread, and blocks until its completed. This
* is useful for when device is powering off.
*/
@MainThread
synchronized void saveImmediately(@NonNull String fileName, @NonNull T data) {
mScheduledFileDataMap.put(fileName, data);
triggerScheduledFlushEarly();
}
@MainThread
private synchronized void triggerScheduledFlushEarly() {
if (mScheduledFileDataMap.isEmpty() || mScheduledExecutorService.isShutdown()) {
return;
}
// Cancel existing future.
if (mScheduledFuture != null) {
// We shouldn't need to interrupt as this method and threaded task
// #flushScheduledData are both synchronized.
mScheduledFuture.cancel(true);
}
// Submit flush and blocks until it completes. Blocking will prevent the device from
// shutting down before flushing completes.
Future<?> future = mScheduledExecutorService.submit(this::flushScheduledData);
try {
future.get(SHUTDOWN_DISK_WRITE_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
Slog.e(TAG, "Failed to save data immediately.", e);
}
}
@WorkerThread
private synchronized void flushScheduledData() {
if (mScheduledFileDataMap.isEmpty()) {
mScheduledFuture = null;
return;
}
for (String fileName : mScheduledFileDataMap.keySet()) {
T data = mScheduledFileDataMap.get(fileName);
writeTo(fileName, data);
}
mScheduledFileDataMap.clear();
mScheduledFuture = null;
}
@WorkerThread
@Nullable
private T parseFile(@NonNull File file) {
final AtomicFile atomicFile = new AtomicFile(file);
try (FileInputStream fileInputStream = atomicFile.openRead()) {
final ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
return protoStreamReader().read(protoInputStream);
} catch (IOException e) {
Slog.e(TAG, "Failed to parse protobuf file.", e);
}
return null;
}
@NonNull
private File getFile(String fileName) {
return new File(mRootDir, fileName);
}
/**
* {@code ProtoStreamWriter} writes {@code T} fields to {@link ProtoOutputStream}.
*
* @param <T> is the data class representation of a protobuf.
*/
interface ProtoStreamWriter<T> {
/**
* Writes {@code T} to {@link ProtoOutputStream}.
*/
void write(@NonNull ProtoOutputStream protoOutputStream, @NonNull T data);
}
/**
* {@code ProtoStreamReader} reads {@link ProtoInputStream} and translate it to {@code T}.
*
* @param <T> is the data class representation of a protobuf.
*/
interface ProtoStreamReader<T> {
/**
* Reads {@link ProtoInputStream} and translates it to {@code T}.
*/
@Nullable
T read(@NonNull ProtoInputStream protoInputStream);
}
}