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