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 i(String tag, String msg, Exception e)70 public static void i(String tag, String msg, Exception e) { 71 Log.i(tag, msg, e); 72 print(tag, msg, e); 73 } 74 i(String tag, String msg)75 public static void i(String tag, String msg) { 76 Log.i(tag, msg); 77 print(tag, msg); 78 } 79 w(String tag, String msg, Exception e)80 public static void w(String tag, String msg, Exception e) { 81 Log.w(tag, msg, e); 82 print(tag, msg, e); 83 } 84 w(String tag, String msg)85 public static void w(String tag, String msg) { 86 Log.w(tag, msg); 87 print(tag, msg); 88 } 89 e(String tag, String msg, Exception e)90 public static void e(String tag, String msg, Exception e) { 91 Log.e(tag, msg, e); 92 print(tag, msg, e); 93 } 94 e(String tag, String msg)95 public static void e(String tag, String msg) { 96 Log.e(tag, msg); 97 print(tag, msg); 98 } 99 print(String tag, String msg)100 public static void print(String tag, String msg) { 101 print(tag, msg, null); 102 } 103 print(String tag, String msg, Exception e)104 public static void print(String tag, String msg, Exception e) { 105 if (!ENABLED) { 106 return; 107 } 108 String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg); 109 if (e != null) { 110 out += "\n" + Log.getStackTraceString(e); 111 } 112 Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget(); 113 } 114 115 @VisibleForTesting getHandler()116 static Handler getHandler() { 117 synchronized (DATE_FORMAT) { 118 if (sHandler == null) { 119 sHandler = new Handler(createAndStartNewLooper("file-logger"), 120 new LogWriterCallback()); 121 } 122 } 123 return sHandler; 124 } 125 126 /** 127 * Blocks until all the pending logs are written to the disk 128 * @param out if not null, all the persisted logs are copied to the writer. 129 */ flushAll(PrintWriter out)130 public static boolean flushAll(PrintWriter out) throws InterruptedException { 131 if (!ENABLED) { 132 return false; 133 } 134 CountDownLatch latch = new CountDownLatch(1); 135 Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH, 136 Pair.create(out, latch)).sendToTarget(); 137 138 latch.await(2, TimeUnit.SECONDS); 139 return latch.getCount() == 0; 140 } 141 142 /** 143 * Writes logs to the file. 144 * Log files are named log-0 for even days of the year and log-1 for odd days of the year. 145 * Logs older than 36 hours are purged. 146 */ 147 private static class LogWriterCallback implements Handler.Callback { 148 149 private static final long CLOSE_DELAY = 5000; // 5 seconds 150 151 private static final int MSG_WRITE = 1; 152 private static final int MSG_CLOSE = 2; 153 private static final int MSG_FLUSH = 3; 154 155 private String mCurrentFileName = null; 156 private PrintWriter mCurrentWriter = null; 157 closeWriter()158 private void closeWriter() { 159 IOUtils.closeSilently(mCurrentWriter); 160 mCurrentWriter = null; 161 } 162 163 @Override handleMessage(Message msg)164 public boolean handleMessage(Message msg) { 165 if (sLogsDirectory == null || !ENABLED) { 166 return true; 167 } 168 switch (msg.what) { 169 case MSG_WRITE: { 170 Calendar cal = Calendar.getInstance(); 171 // suffix with 0 or 1 based on the day of the year. 172 String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % LOG_DAYS); 173 174 if (!fileName.equals(mCurrentFileName)) { 175 closeWriter(); 176 } 177 178 try { 179 if (mCurrentWriter == null) { 180 mCurrentFileName = fileName; 181 182 boolean append = false; 183 File logFile = new File(sLogsDirectory, fileName); 184 if (logFile.exists()) { 185 Calendar modifiedTime = Calendar.getInstance(); 186 modifiedTime.setTimeInMillis(logFile.lastModified()); 187 188 // If the file was modified more that 36 hours ago, purge the file. 189 // We use instead of 24 to account for day-365 followed by day-1 190 modifiedTime.add(Calendar.HOUR, 36); 191 append = cal.before(modifiedTime) 192 && logFile.length() < MAX_LOG_FILE_SIZE; 193 } 194 mCurrentWriter = new PrintWriter(new FileWriter(logFile, append)); 195 } 196 197 mCurrentWriter.println((String) msg.obj); 198 mCurrentWriter.flush(); 199 200 // Auto close file stream after some time. 201 sHandler.removeMessages(MSG_CLOSE); 202 sHandler.sendEmptyMessageDelayed(MSG_CLOSE, CLOSE_DELAY); 203 } catch (Exception e) { 204 Log.e("FileLog", "Error writing logs to file", e); 205 // Close stream, will try reopening during next log 206 closeWriter(); 207 } 208 return true; 209 } 210 case MSG_CLOSE: { 211 closeWriter(); 212 return true; 213 } 214 case MSG_FLUSH: { 215 closeWriter(); 216 Pair<PrintWriter, CountDownLatch> p = 217 (Pair<PrintWriter, CountDownLatch>) msg.obj; 218 219 if (p.first != null) { 220 for (int i = 0; i < LOG_DAYS; i++) { 221 dumpFile(p.first, FILE_NAME_PREFIX + i); 222 } 223 } 224 p.second.countDown(); 225 return true; 226 } 227 } 228 return true; 229 } 230 } 231 232 private static void dumpFile(PrintWriter out, String fileName) { 233 File logFile = new File(sLogsDirectory, fileName); 234 if (logFile.exists()) { 235 236 BufferedReader in = null; 237 try { 238 in = new BufferedReader(new FileReader(logFile)); 239 out.println(); 240 out.println("--- logfile: " + fileName + " ---"); 241 String line; 242 while ((line = in.readLine()) != null) { 243 out.println(line); 244 } 245 } catch (Exception e) { 246 // ignore 247 } finally { 248 IOUtils.closeSilently(in); 249 } 250 } 251 } 252 253 /** 254 * Gets files used for FileLog 255 */ 256 public static File[] getLogFiles() { 257 try { 258 flushAll(null); 259 } catch (InterruptedException e) { } 260 File[] files = new File[LOG_DAYS]; 261 for (int i = 0; i < LOG_DAYS; i++) { 262 files[i] = new File(sLogsDirectory, FILE_NAME_PREFIX + i); 263 } 264 return files; 265 } 266 } 267