| package com.android.launcher3.logging; |
| |
| import static com.android.launcher3.util.Executors.createAndStartNewLooper; |
| |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Message; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import androidx.annotation.VisibleForTesting; |
| |
| import com.android.launcher3.util.IOUtils; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.PrintWriter; |
| import java.text.DateFormat; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Wrapper around {@link Log} to allow writing to a file. |
| * This class can safely be called from main thread. |
| * |
| * Note: This should only be used for logging errors which have a persistent effect on user's data, |
| * but whose effect may not be visible immediately. |
| */ |
| public final class FileLog { |
| |
| protected static final boolean ENABLED = true; |
| private static final String FILE_NAME_PREFIX = "log-"; |
| private static final DateFormat DATE_FORMAT = |
| DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); |
| |
| private static final long MAX_LOG_FILE_SIZE = 8 << 20; // 4 mb |
| |
| private static Handler sHandler = null; |
| private static File sLogsDirectory = null; |
| |
| public static final int LOG_DAYS = 4; |
| |
| public static void setDir(File logsDir) { |
| if (ENABLED) { |
| synchronized (DATE_FORMAT) { |
| // If the target directory changes, stop any active thread. |
| if (sHandler != null && !logsDir.equals(sLogsDirectory)) { |
| ((HandlerThread) sHandler.getLooper().getThread()).quit(); |
| sHandler = null; |
| } |
| } |
| } |
| sLogsDirectory = logsDir; |
| } |
| |
| public static void d(String tag, String msg, Exception e) { |
| Log.d(tag, msg, e); |
| print(tag, msg, e); |
| } |
| |
| public static void d(String tag, String msg) { |
| Log.d(tag, msg); |
| print(tag, msg); |
| } |
| |
| public static void i(String tag, String msg, Exception e) { |
| Log.i(tag, msg, e); |
| print(tag, msg, e); |
| } |
| |
| public static void i(String tag, String msg) { |
| Log.i(tag, msg); |
| print(tag, msg); |
| } |
| |
| public static void w(String tag, String msg, Exception e) { |
| Log.w(tag, msg, e); |
| print(tag, msg, e); |
| } |
| |
| public static void w(String tag, String msg) { |
| Log.w(tag, msg); |
| print(tag, msg); |
| } |
| |
| public static void e(String tag, String msg, Exception e) { |
| Log.e(tag, msg, e); |
| print(tag, msg, e); |
| } |
| |
| public static void e(String tag, String msg) { |
| Log.e(tag, msg); |
| print(tag, msg); |
| } |
| |
| public static void print(String tag, String msg) { |
| print(tag, msg, null); |
| } |
| |
| public static void print(String tag, String msg, Exception e) { |
| if (!ENABLED) { |
| return; |
| } |
| String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg); |
| if (e != null) { |
| out += "\n" + Log.getStackTraceString(e); |
| } |
| Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget(); |
| } |
| |
| @VisibleForTesting |
| static Handler getHandler() { |
| synchronized (DATE_FORMAT) { |
| if (sHandler == null) { |
| sHandler = new Handler(createAndStartNewLooper("file-logger"), |
| new LogWriterCallback()); |
| } |
| } |
| return sHandler; |
| } |
| |
| /** |
| * Blocks until all the pending logs are written to the disk |
| * @param out if not null, all the persisted logs are copied to the writer. |
| */ |
| public static boolean flushAll(PrintWriter out) throws InterruptedException { |
| if (!ENABLED) { |
| return false; |
| } |
| CountDownLatch latch = new CountDownLatch(1); |
| Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH, |
| Pair.create(out, latch)).sendToTarget(); |
| |
| latch.await(2, TimeUnit.SECONDS); |
| return latch.getCount() == 0; |
| } |
| |
| /** |
| * Writes logs to the file. |
| * Log files are named log-0 for even days of the year and log-1 for odd days of the year. |
| * Logs older than 36 hours are purged. |
| */ |
| private static class LogWriterCallback implements Handler.Callback { |
| |
| private static final long CLOSE_DELAY = 5000; // 5 seconds |
| |
| private static final int MSG_WRITE = 1; |
| private static final int MSG_CLOSE = 2; |
| private static final int MSG_FLUSH = 3; |
| |
| private String mCurrentFileName = null; |
| private PrintWriter mCurrentWriter = null; |
| |
| private void closeWriter() { |
| IOUtils.closeSilently(mCurrentWriter); |
| mCurrentWriter = null; |
| } |
| |
| @Override |
| public boolean handleMessage(Message msg) { |
| if (sLogsDirectory == null || !ENABLED) { |
| return true; |
| } |
| switch (msg.what) { |
| case MSG_WRITE: { |
| Calendar cal = Calendar.getInstance(); |
| // suffix with 0 or 1 based on the day of the year. |
| String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % LOG_DAYS); |
| |
| if (!fileName.equals(mCurrentFileName)) { |
| closeWriter(); |
| } |
| |
| try { |
| if (mCurrentWriter == null) { |
| mCurrentFileName = fileName; |
| |
| boolean append = false; |
| File logFile = new File(sLogsDirectory, fileName); |
| if (logFile.exists()) { |
| Calendar modifiedTime = Calendar.getInstance(); |
| modifiedTime.setTimeInMillis(logFile.lastModified()); |
| |
| // If the file was modified more that 36 hours ago, purge the file. |
| // We use instead of 24 to account for day-365 followed by day-1 |
| modifiedTime.add(Calendar.HOUR, 36); |
| append = cal.before(modifiedTime) |
| && logFile.length() < MAX_LOG_FILE_SIZE; |
| } |
| mCurrentWriter = new PrintWriter(new FileWriter(logFile, append)); |
| } |
| |
| mCurrentWriter.println((String) msg.obj); |
| mCurrentWriter.flush(); |
| |
| // Auto close file stream after some time. |
| sHandler.removeMessages(MSG_CLOSE); |
| sHandler.sendEmptyMessageDelayed(MSG_CLOSE, CLOSE_DELAY); |
| } catch (Exception e) { |
| Log.e("FileLog", "Error writing logs to file", e); |
| // Close stream, will try reopening during next log |
| closeWriter(); |
| } |
| return true; |
| } |
| case MSG_CLOSE: { |
| closeWriter(); |
| return true; |
| } |
| case MSG_FLUSH: { |
| closeWriter(); |
| Pair<PrintWriter, CountDownLatch> p = |
| (Pair<PrintWriter, CountDownLatch>) msg.obj; |
| |
| if (p.first != null) { |
| for (int i = 0; i < LOG_DAYS; i++) { |
| dumpFile(p.first, FILE_NAME_PREFIX + i); |
| } |
| } |
| p.second.countDown(); |
| return true; |
| } |
| } |
| return true; |
| } |
| } |
| |
| private static void dumpFile(PrintWriter out, String fileName) { |
| File logFile = new File(sLogsDirectory, fileName); |
| if (logFile.exists()) { |
| |
| BufferedReader in = null; |
| try { |
| in = new BufferedReader(new FileReader(logFile)); |
| out.println(); |
| out.println("--- logfile: " + fileName + " ---"); |
| String line; |
| while ((line = in.readLine()) != null) { |
| out.println(line); |
| } |
| } catch (Exception e) { |
| // ignore |
| } finally { |
| IOUtils.closeSilently(in); |
| } |
| } |
| } |
| |
| /** |
| * Gets files used for FileLog |
| */ |
| public static File[] getLogFiles() { |
| try { |
| flushAll(null); |
| } catch (InterruptedException e) { } |
| File[] files = new File[LOG_DAYS]; |
| for (int i = 0; i < LOG_DAYS; i++) { |
| files[i] = new File(sLogsDirectory, FILE_NAME_PREFIX + i); |
| } |
| return files; |
| } |
| } |