/*
* Copyright (C) 2017 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.dialer.persistentlog;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.support.v4.os.UserManagerCompat;
import com.android.dialer.common.LogUtil;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Handles serialization of byte arrays and read/write them to multiple rotating files. If a logText
* file exceeds {@code fileSizeLimit} after a write, a new file will be used. if the total number of
* files exceeds {@code fileCountLimit} the oldest ones will be deleted. The logs are stored in the
* cache but the file index is stored in the data (clearing data will also clear the cache). The
* logs will be stored under /cache_dir/persistent_log/{@code subfolder}, so multiple independent
* logs can be created.
*
*
This class is NOT thread safe. All methods expect the constructor must be called on the same
* worker thread.
*/
final class PersistentLogFileHandler {
private static final String LOG_DIRECTORY = "persistent_log";
private static final String NEXT_FILE_INDEX_PREFIX = "persistent_long_next_file_index_";
private static final byte[] ENTRY_PREFIX = {'P'};
private static final byte[] ENTRY_POSTFIX = {'L'};
private static class LogCorruptionException extends Exception {
public LogCorruptionException(String message) {
super(message);
}
}
private File logDirectory;
private final String subfolder;
private final int fileSizeLimit;
private final int fileCountLimit;
private SharedPreferences sharedPreferences;
private File outputFile;
private Context context;
@MainThread
PersistentLogFileHandler(String subfolder, int fileSizeLimit, int fileCountLimit) {
this.subfolder = subfolder;
this.fileSizeLimit = fileSizeLimit;
this.fileCountLimit = fileCountLimit;
}
/** Must be called right after the logger thread is created. */
@WorkerThread
void initialize(Context context) {
this.context = context;
logDirectory = new File(new File(context.getCacheDir(), LOG_DIRECTORY), subfolder);
initializeSharedPreference(context);
}
@WorkerThread
private boolean initializeSharedPreference(Context context) {
if (sharedPreferences == null && UserManagerCompat.isUserUnlocked(context)) {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
return true;
}
return sharedPreferences != null;
}
/**
* Write the list of byte arrays to the current log file, prefixing each entry with its' length. A
* new file will only be selected when the batch is completed, so the resulting file might be
* larger then {@code fileSizeLimit}
*/
@WorkerThread
void writeLogs(List logs) throws IOException {
if (outputFile == null) {
selectNextFileToWrite();
}
outputFile.createNewFile();
try (DataOutputStream outputStream =
new DataOutputStream(new FileOutputStream(outputFile, true))) {
for (byte[] log : logs) {
outputStream.write(ENTRY_PREFIX);
outputStream.writeInt(log.length);
outputStream.write(log);
outputStream.write(ENTRY_POSTFIX);
}
outputStream.close();
if (outputFile.length() > fileSizeLimit) {
selectNextFileToWrite();
}
}
}
void writeRawLogsForTest(byte[] data) throws IOException {
if (outputFile == null) {
selectNextFileToWrite();
}
outputFile.createNewFile();
try (DataOutputStream outputStream =
new DataOutputStream(new FileOutputStream(outputFile, true))) {
outputStream.write(data);
outputStream.close();
if (outputFile.length() > fileSizeLimit) {
selectNextFileToWrite();
}
}
}
/** Concatenate all log files in chronicle order and return a byte array. */
@WorkerThread
@NonNull
private byte[] readBlob() throws IOException {
File[] files = getLogFiles();
ByteBuffer byteBuffer = ByteBuffer.allocate(getTotalSize(files));
for (File file : files) {
byteBuffer.put(readAllBytes(file));
}
return byteBuffer.array();
}
private static int getTotalSize(File[] files) {
int sum = 0;
for (File file : files) {
sum += (int) file.length();
}
return sum;
}
/** Parses the content of all files back to individual byte arrays. */
@WorkerThread
@NonNull
List getLogs() throws IOException {
byte[] blob = readBlob();
List logs = new ArrayList<>();
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(blob))) {
byte[] log = readLog(input);
while (log != null) {
logs.add(log);
log = readLog(input);
}
} catch (LogCorruptionException e) {
LogUtil.e("PersistentLogFileHandler.getLogs", "logs corrupted, deleting", e);
deleteLogs();
return new ArrayList<>();
}
return logs;
}
private void deleteLogs() throws IOException {
for (File file : getLogFiles()) {
file.delete();
}
selectNextFileToWrite();
}
@WorkerThread
private void selectNextFileToWrite() throws IOException {
File[] files = getLogFiles();
if (files.length == 0 || files[files.length - 1].length() > fileSizeLimit) {
if (files.length >= fileCountLimit) {
for (int i = 0; i <= files.length - fileCountLimit; i++) {
files[i].delete();
}
}
outputFile = new File(logDirectory, String.valueOf(getAndIncrementNextFileIndex()));
} else {
outputFile = files[files.length - 1];
}
}
@NonNull
@WorkerThread
private File[] getLogFiles() {
logDirectory.mkdirs();
File[] files = logDirectory.listFiles();
if (files == null) {
files = new File[0];
}
Arrays.sort(
files,
(File lhs, File rhs) ->
Long.compare(Long.valueOf(lhs.getName()), Long.valueOf(rhs.getName())));
return files;
}
@Nullable
@WorkerThread
private byte[] readLog(DataInputStream inputStream) throws IOException, LogCorruptionException {
try {
byte[] prefix = new byte[ENTRY_PREFIX.length];
if (inputStream.read(prefix) == -1) {
// EOF
return null;
}
if (!Arrays.equals(prefix, ENTRY_PREFIX)) {
throw new LogCorruptionException("entry prefix mismatch");
}
int dataLength = inputStream.readInt();
if (dataLength > fileSizeLimit) {
throw new LogCorruptionException("data length over max size");
}
byte[] data = new byte[dataLength];
inputStream.read(data);
byte[] postfix = new byte[ENTRY_POSTFIX.length];
inputStream.read(postfix);
if (!Arrays.equals(postfix, ENTRY_POSTFIX)) {
throw new LogCorruptionException("entry postfix mismatch");
}
return data;
} catch (EOFException e) {
return null;
}
}
@NonNull
@WorkerThread
private static byte[] readAllBytes(File file) throws IOException {
byte[] result = new byte[(int) file.length()];
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
randomAccessFile.readFully(result);
}
return result;
}
@WorkerThread
private int getAndIncrementNextFileIndex() throws IOException {
if (!initializeSharedPreference(context)) {
throw new IOException("Shared preference is not available");
}
int index = sharedPreferences.getInt(getNextFileKey(), 0);
sharedPreferences.edit().putInt(getNextFileKey(), index + 1).commit();
return index;
}
@AnyThread
private String getNextFileKey() {
return NEXT_FILE_INDEX_PREFIX + subfolder;
}
}