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