1 /* 2 * Copyright (C) 2017 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.dialer.persistentlog; 18 19 import android.content.Context; 20 import android.os.Handler; 21 import android.os.HandlerThread; 22 import android.support.annotation.AnyThread; 23 import android.support.annotation.NonNull; 24 import android.support.annotation.VisibleForTesting; 25 import android.support.annotation.WorkerThread; 26 import android.support.v4.os.UserManagerCompat; 27 import com.android.dialer.common.Assert; 28 import com.android.dialer.common.LogUtil; 29 import java.io.IOException; 30 import java.nio.charset.StandardCharsets; 31 import java.util.ArrayList; 32 import java.util.Calendar; 33 import java.util.List; 34 import java.util.concurrent.CountDownLatch; 35 import java.util.concurrent.LinkedBlockingQueue; 36 37 /** 38 * Logs data that is persisted across app termination and device reboot. The logs are stored as 39 * rolling files in cache with a limit of {@link #LOG_FILE_SIZE_LIMIT} * {@link 40 * #LOG_FILE_COUNT_LIMIT}. The log writing is batched and there is a {@link #FLUSH_DELAY_MILLIS} 41 * delay before the logs are committed to disk to avoid excessive IO. If the app is terminated 42 * before the logs are committed it will be lost. {@link 43 * com.google.android.apps.dialer.crashreporter.SilentCrashReporter} is expected to handle such 44 * cases. 45 * 46 * <p>{@link #logText(String, String)} should be used to log ad-hoc text logs. TODO: switch 47 * to structured logging 48 */ 49 public final class PersistentLogger { 50 51 private static final int FLUSH_DELAY_MILLIS = 200; 52 private static final String LOG_FOLDER = "plain_text"; 53 private static final int MESSAGE_FLUSH = 1; 54 55 @VisibleForTesting static final int LOG_FILE_SIZE_LIMIT = 64 * 1024; 56 @VisibleForTesting static final int LOG_FILE_COUNT_LIMIT = 8; 57 58 private static PersistentLogFileHandler fileHandler; 59 60 private static HandlerThread loggerThread; 61 private static Handler loggerThreadHandler; 62 63 private static final LinkedBlockingQueue<byte[]> messageQueue = new LinkedBlockingQueue<>(); 64 65 private PersistentLogger() {} 66 67 public static void initialize(Context context) { 68 fileHandler = 69 new PersistentLogFileHandler(LOG_FOLDER, LOG_FILE_SIZE_LIMIT, LOG_FILE_COUNT_LIMIT); 70 loggerThread = new HandlerThread("PersistentLogger"); 71 loggerThread.start(); 72 loggerThreadHandler = 73 new Handler( 74 loggerThread.getLooper(), 75 (message) -> { 76 if (message.what == MESSAGE_FLUSH) { 77 if (messageQueue.isEmpty()) { 78 return true; 79 } 80 loggerThreadHandler.removeMessages(MESSAGE_FLUSH); 81 List<byte[]> messages = new ArrayList<>(); 82 messageQueue.drainTo(messages); 83 if (!UserManagerCompat.isUserUnlocked(context)) { 84 return true; 85 } 86 try { 87 fileHandler.writeLogs(messages); 88 } catch (IOException e) { 89 LogUtil.e("PersistentLogger.MESSAGE_FLUSH", "error writing message", e); 90 } 91 } 92 return true; 93 }); 94 loggerThreadHandler.post(() -> fileHandler.initialize(context)); 95 } 96 97 static HandlerThread getLoggerThread() { 98 return loggerThread; 99 } 100 101 @AnyThread 102 public static void logText(String tag, String string) { 103 log(buildTextLog(tag, string)); 104 } 105 106 @VisibleForTesting 107 @AnyThread 108 static void log(byte[] data) { 109 messageQueue.add(data); 110 loggerThreadHandler.sendEmptyMessageDelayed(MESSAGE_FLUSH, FLUSH_DELAY_MILLIS); 111 } 112 113 /** Dump the log as human readable string. Blocks until the dump is finished. */ 114 @NonNull 115 @WorkerThread 116 public static String dumpLogToString() { 117 Assert.isWorkerThread(); 118 DumpStringRunnable dumpStringRunnable = new DumpStringRunnable(); 119 loggerThreadHandler.post(dumpStringRunnable); 120 try { 121 return dumpStringRunnable.get(); 122 } catch (InterruptedException e) { 123 Thread.currentThread().interrupt(); 124 return "Cannot dump logText: " + e; 125 } 126 } 127 128 private static class DumpStringRunnable implements Runnable { 129 private String result; 130 private final CountDownLatch latch = new CountDownLatch(1); 131 132 @Override 133 public void run() { 134 result = dumpLogToStringInternal(); 135 latch.countDown(); 136 } 137 138 public String get() throws InterruptedException { 139 latch.await(); 140 return result; 141 } 142 } 143 144 @NonNull 145 @WorkerThread 146 private static String dumpLogToStringInternal() { 147 StringBuilder result = new StringBuilder(); 148 List<byte[]> logs; 149 try { 150 logs = readLogs(); 151 } catch (IOException e) { 152 return "Cannot dump logText: " + e; 153 } 154 155 for (byte[] log : logs) { 156 result.append(new String(log, StandardCharsets.UTF_8)).append("\n"); 157 } 158 return result.toString(); 159 } 160 161 @NonNull 162 @WorkerThread 163 @VisibleForTesting 164 static List<byte[]> readLogs() throws IOException { 165 Assert.isWorkerThread(); 166 return fileHandler.getLogs(); 167 } 168 169 private static byte[] buildTextLog(String tag, String string) { 170 Calendar c = Calendar.getInstance(); 171 return String.format("%tm-%td %tH:%tM:%tS.%tL - %s - %s", c, c, c, c, c, c, tag, string) 172 .getBytes(StandardCharsets.UTF_8); 173 } 174 } 175