• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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