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