1 package com.android.launcher3.logging; 2 3 import static com.android.launcher3.util.Executors.createAndStartNewLooper; 4 5 import android.os.Handler; 6 import android.os.HandlerThread; 7 import android.os.Message; 8 import android.util.Log; 9 import android.util.Pair; 10 11 import androidx.annotation.VisibleForTesting; 12 13 import com.android.launcher3.util.IOUtils; 14 15 import java.io.BufferedReader; 16 import java.io.File; 17 import java.io.FileReader; 18 import java.io.FileWriter; 19 import java.io.PrintWriter; 20 import java.text.DateFormat; 21 import java.util.Calendar; 22 import java.util.Date; 23 import java.util.concurrent.CountDownLatch; 24 import java.util.concurrent.TimeUnit; 25 26 /** 27 * Wrapper around {@link Log} to allow writing to a file. 28 * This class can safely be called from main thread. 29 * 30 * Note: This should only be used for logging errors which have a persistent effect on user's data, 31 * but whose effect may not be visible immediately. 32 */ 33 public final class FileLog { 34 35 protected static final boolean ENABLED = true; 36 private static final String FILE_NAME_PREFIX = "log-"; 37 private static final DateFormat DATE_FORMAT = 38 DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); 39 40 private static final long MAX_LOG_FILE_SIZE = 8 << 20; // 4 mb 41 42 private static Handler sHandler = null; 43 private static File sLogsDirectory = null; 44 45 public static final int LOG_DAYS = 4; 46 setDir(File logsDir)47 public static void setDir(File logsDir) { 48 if (ENABLED) { 49 synchronized (DATE_FORMAT) { 50 // If the target directory changes, stop any active thread. 51 if (sHandler != null && !logsDir.equals(sLogsDirectory)) { 52 ((HandlerThread) sHandler.getLooper().getThread()).quit(); 53 sHandler = null; 54 } 55 } 56 } 57 sLogsDirectory = logsDir; 58 } 59 d(String tag, String msg, Exception e)60 public static void d(String tag, String msg, Exception e) { 61 Log.d(tag, msg, e); 62 print(tag, msg, e); 63 } 64 d(String tag, String msg)65 public static void d(String tag, String msg) { 66 Log.d(tag, msg); 67 print(tag, msg); 68 } 69 e(String tag, String msg, Exception e)70 public static void e(String tag, String msg, Exception e) { 71 Log.e(tag, msg, e); 72 print(tag, msg, e); 73 } 74 e(String tag, String msg)75 public static void e(String tag, String msg) { 76 Log.e(tag, msg); 77 print(tag, msg); 78 } 79 print(String tag, String msg)80 public static void print(String tag, String msg) { 81 print(tag, msg, null); 82 } 83 print(String tag, String msg, Exception e)84 public static void print(String tag, String msg, Exception e) { 85 if (!ENABLED) { 86 return; 87 } 88 String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg); 89 if (e != null) { 90 out += "\n" + Log.getStackTraceString(e); 91 } 92 Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget(); 93 } 94 95 @VisibleForTesting getHandler()96 static Handler getHandler() { 97 synchronized (DATE_FORMAT) { 98 if (sHandler == null) { 99 sHandler = new Handler(createAndStartNewLooper("file-logger"), 100 new LogWriterCallback()); 101 } 102 } 103 return sHandler; 104 } 105 106 /** 107 * Blocks until all the pending logs are written to the disk 108 * @param out if not null, all the persisted logs are copied to the writer. 109 */ flushAll(PrintWriter out)110 public static boolean flushAll(PrintWriter out) throws InterruptedException { 111 if (!ENABLED) { 112 return false; 113 } 114 CountDownLatch latch = new CountDownLatch(1); 115 Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH, 116 Pair.create(out, latch)).sendToTarget(); 117 118 latch.await(2, TimeUnit.SECONDS); 119 return latch.getCount() == 0; 120 } 121 122 /** 123 * Writes logs to the file. 124 * Log files are named log-0 for even days of the year and log-1 for odd days of the year. 125 * Logs older than 36 hours are purged. 126 */ 127 private static class LogWriterCallback implements Handler.Callback { 128 129 private static final long CLOSE_DELAY = 5000; // 5 seconds 130 131 private static final int MSG_WRITE = 1; 132 private static final int MSG_CLOSE = 2; 133 private static final int MSG_FLUSH = 3; 134 135 private String mCurrentFileName = null; 136 private PrintWriter mCurrentWriter = null; 137 closeWriter()138 private void closeWriter() { 139 IOUtils.closeSilently(mCurrentWriter); 140 mCurrentWriter = null; 141 } 142 143 @Override handleMessage(Message msg)144 public boolean handleMessage(Message msg) { 145 if (sLogsDirectory == null || !ENABLED) { 146 return true; 147 } 148 switch (msg.what) { 149 case MSG_WRITE: { 150 Calendar cal = Calendar.getInstance(); 151 // suffix with 0 or 1 based on the day of the year. 152 String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % LOG_DAYS); 153 154 if (!fileName.equals(mCurrentFileName)) { 155 closeWriter(); 156 } 157 158 try { 159 if (mCurrentWriter == null) { 160 mCurrentFileName = fileName; 161 162 boolean append = false; 163 File logFile = new File(sLogsDirectory, fileName); 164 if (logFile.exists()) { 165 Calendar modifiedTime = Calendar.getInstance(); 166 modifiedTime.setTimeInMillis(logFile.lastModified()); 167 168 // If the file was modified more that 36 hours ago, purge the file. 169 // We use instead of 24 to account for day-365 followed by day-1 170 modifiedTime.add(Calendar.HOUR, 36); 171 append = cal.before(modifiedTime) 172 && logFile.length() < MAX_LOG_FILE_SIZE; 173 } 174 mCurrentWriter = new PrintWriter(new FileWriter(logFile, append)); 175 } 176 177 mCurrentWriter.println((String) msg.obj); 178 mCurrentWriter.flush(); 179 180 // Auto close file stream after some time. 181 sHandler.removeMessages(MSG_CLOSE); 182 sHandler.sendEmptyMessageDelayed(MSG_CLOSE, CLOSE_DELAY); 183 } catch (Exception e) { 184 Log.e("FileLog", "Error writing logs to file", e); 185 // Close stream, will try reopening during next log 186 closeWriter(); 187 } 188 return true; 189 } 190 case MSG_CLOSE: { 191 closeWriter(); 192 return true; 193 } 194 case MSG_FLUSH: { 195 closeWriter(); 196 Pair<PrintWriter, CountDownLatch> p = 197 (Pair<PrintWriter, CountDownLatch>) msg.obj; 198 199 if (p.first != null) { 200 for (int i = 0; i < LOG_DAYS; i++) { 201 dumpFile(p.first, FILE_NAME_PREFIX + i); 202 } 203 } 204 p.second.countDown(); 205 return true; 206 } 207 } 208 return true; 209 } 210 } 211 212 private static void dumpFile(PrintWriter out, String fileName) { 213 File logFile = new File(sLogsDirectory, fileName); 214 if (logFile.exists()) { 215 216 BufferedReader in = null; 217 try { 218 in = new BufferedReader(new FileReader(logFile)); 219 out.println(); 220 out.println("--- logfile: " + fileName + " ---"); 221 String line; 222 while ((line = in.readLine()) != null) { 223 out.println(line); 224 } 225 } catch (Exception e) { 226 // ignore 227 } finally { 228 IOUtils.closeSilently(in); 229 } 230 } 231 } 232 233 /** 234 * Gets files used for FileLog 235 */ 236 public static File[] getLogFiles() { 237 try { 238 flushAll(null); 239 } catch (InterruptedException e) { } 240 File[] files = new File[LOG_DAYS]; 241 for (int i = 0; i < LOG_DAYS; i++) { 242 files[i] = new File(sLogsDirectory, FILE_NAME_PREFIX + i); 243 } 244 return files; 245 } 246 } 247