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