summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/persistentlog
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialer/persistentlog')
-rw-r--r--java/com/android/dialer/persistentlog/PersistentLogFileHandler.java198
-rw-r--r--java/com/android/dialer/persistentlog/PersistentLogger.java170
2 files changed, 368 insertions, 0 deletions
diff --git a/java/com/android/dialer/persistentlog/PersistentLogFileHandler.java b/java/com/android/dialer/persistentlog/PersistentLogFileHandler.java
new file mode 100644
index 000000000..bb51fa881
--- /dev/null
+++ b/java/com/android/dialer/persistentlog/PersistentLogFileHandler.java
@@ -0,0 +1,198 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build.VERSION_CODES;
+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 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.
+ *
+ * <p>This class is NOT thread safe. All methods expect the constructor must be called on the same
+ * worker thread.
+ */
+@SuppressWarnings("AndroidApiChecker") // lambdas
+@TargetApi(VERSION_CODES.N)
+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 File logDirectory;
+ private final String subfolder;
+ private final int fileSizeLimit;
+ private final int fileCountLimit;
+
+ private SharedPreferences sharedPreferences;
+
+ private File outputFile;
+
+ @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) {
+ logDirectory = new File(new File(context.getCacheDir(), LOG_DIRECTORY), subfolder);
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ /**
+ * 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<byte[]> logs) throws IOException {
+ if (outputFile == null) {
+ selectNextFileToWrite();
+ }
+ outputFile.createNewFile();
+ try (DataOutputStream outputStream =
+ new DataOutputStream(new FileOutputStream(outputFile, true))) {
+ for (byte[] log : logs) {
+ outputStream.writeInt(log.length);
+ outputStream.write(log);
+ }
+ 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(Arrays.stream(files).mapToInt(file -> (int) file.length()).sum());
+ for (File file : files) {
+ byteBuffer.put(readAllBytes(file));
+ }
+ return byteBuffer.array();
+ }
+
+ /** Parses the content of all files back to individual byte arrays. */
+ @WorkerThread
+ @NonNull
+ List<byte[]> getLogs() throws IOException {
+ byte[] blob = readBlob();
+ List<byte[]> logs = new ArrayList<>();
+ try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(blob))) {
+ byte[] log = readLog(input);
+ while (log != null) {
+ logs.add(log);
+ log = readLog(input);
+ }
+ }
+ return logs;
+ }
+
+ @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();
+ Arrays.sort(
+ files,
+ (File lhs, File rhs) ->
+ Long.compare(Long.valueOf(lhs.getName()), Long.valueOf(rhs.getName())));
+ return files;
+ }
+
+ @Nullable
+ @WorkerThread
+ private static byte[] readLog(DataInputStream inputStream) throws IOException {
+ try {
+ byte[] data = new byte[inputStream.readInt()];
+ inputStream.read(data);
+ 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() {
+ 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;
+ }
+}
diff --git a/java/com/android/dialer/persistentlog/PersistentLogger.java b/java/com/android/dialer/persistentlog/PersistentLogger.java
new file mode 100644
index 000000000..049eb9687
--- /dev/null
+++ b/java/com/android/dialer/persistentlog/PersistentLogger.java
@@ -0,0 +1,170 @@
+/*
+ * 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.os.Handler;
+import android.os.HandlerThread;
+import android.support.annotation.AnyThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Logs data that is persisted across app termination and device reboot. The logs are stored as
+ * rolling files in cache with a limit of {@link #LOG_FILE_SIZE_LIMIT} * {@link
+ * #LOG_FILE_COUNT_LIMIT}. The log writing is batched and there is a {@link #FLUSH_DELAY_MILLIS}
+ * delay before the logs are committed to disk to avoid excessive IO. If the app is terminated
+ * before the logs are committed it will be lost. {@link
+ * com.google.android.apps.dialer.crashreporter.SilentCrashReporter} is expected to handle such
+ * cases.
+ *
+ * <p>{@link #logText(String, String)} should be used to log ad-hoc text logs. TODO: switch
+ * to structured logging
+ */
+public final class PersistentLogger {
+
+ private static final int FLUSH_DELAY_MILLIS = 200;
+ private static final String LOG_FOLDER = "plain_text";
+ private static final int MESSAGE_FLUSH = 1;
+
+ @VisibleForTesting static final int LOG_FILE_SIZE_LIMIT = 64 * 1024;
+ @VisibleForTesting static final int LOG_FILE_COUNT_LIMIT = 8;
+
+ private static PersistentLogFileHandler fileHandler;
+
+ private static HandlerThread loggerThread;
+ private static Handler loggerThreadHandler;
+
+ private static final LinkedBlockingQueue<byte[]> messageQueue = new LinkedBlockingQueue<>();
+
+ private PersistentLogger() {}
+
+ public static void initialize(Context context) {
+ fileHandler =
+ new PersistentLogFileHandler(LOG_FOLDER, LOG_FILE_SIZE_LIMIT, LOG_FILE_COUNT_LIMIT);
+ loggerThread = new HandlerThread("PersistentLogger");
+ loggerThread.start();
+ loggerThreadHandler =
+ new Handler(
+ loggerThread.getLooper(),
+ (message) -> {
+ if (message.what == MESSAGE_FLUSH) {
+ if (messageQueue.isEmpty()) {
+ return true;
+ }
+ loggerThreadHandler.removeMessages(MESSAGE_FLUSH);
+ List<byte[]> messages = new ArrayList<>();
+ messageQueue.drainTo(messages);
+ try {
+ fileHandler.writeLogs(messages);
+ } catch (IOException e) {
+ LogUtil.e("PersistentLogger.MESSAGE_FLUSH", "error writing message", e);
+ }
+ }
+ return true;
+ });
+ loggerThreadHandler.post(() -> fileHandler.initialize(context));
+ }
+
+ static HandlerThread getLoggerThread() {
+ return loggerThread;
+ }
+
+ @AnyThread
+ public static void logText(String tag, String string) {
+ log(buildTextLog(tag, string));
+ }
+
+ @VisibleForTesting
+ @AnyThread
+ static void log(byte[] data) {
+ messageQueue.add(data);
+ loggerThreadHandler.sendEmptyMessageDelayed(MESSAGE_FLUSH, FLUSH_DELAY_MILLIS);
+ }
+
+ /** Dump the log as human readable string. Blocks until the dump is finished. */
+ @NonNull
+ @WorkerThread
+ public static String dumpLogToString() {
+ Assert.isWorkerThread();
+ DumpStringRunnable dumpStringRunnable = new DumpStringRunnable();
+ loggerThreadHandler.post(dumpStringRunnable);
+ try {
+ return dumpStringRunnable.get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "Cannot dump logText: " + e;
+ }
+ }
+
+ private static class DumpStringRunnable implements Runnable {
+ private String result;
+ private final CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void run() {
+ result = dumpLogToStringInternal();
+ latch.countDown();
+ }
+
+ public String get() throws InterruptedException {
+ latch.await();
+ return result;
+ }
+ }
+
+ @NonNull
+ @WorkerThread
+ private static String dumpLogToStringInternal() {
+ StringBuilder result = new StringBuilder();
+ List<byte[]> logs;
+ try {
+ logs = readLogs();
+ } catch (IOException e) {
+ return "Cannot dump logText: " + e;
+ }
+
+ for (byte[] log : logs) {
+ result.append(new String(log, StandardCharsets.UTF_8)).append("\n");
+ }
+ return result.toString();
+ }
+
+ @NonNull
+ @WorkerThread
+ @VisibleForTesting
+ static List<byte[]> readLogs() throws IOException {
+ Assert.isWorkerThread();
+ return fileHandler.getLogs();
+ }
+
+ private static byte[] buildTextLog(String tag, String string) {
+ Calendar c = Calendar.getInstance();
+ return String.format("%tm-%td %tH:%tM:%tS.%tL - %s - %s", c, c, c, c, c, c, tag, string)
+ .getBytes(StandardCharsets.UTF_8);
+ }
+}