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