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 com.android.launcher3.util.IOUtils;
12 
13 import java.io.BufferedReader;
14 import java.io.File;
15 import java.io.FileReader;
16 import java.io.FileWriter;
17 import java.io.PrintWriter;
18 import java.text.DateFormat;
19 import java.util.Calendar;
20 import java.util.Date;
21 import java.util.concurrent.CountDownLatch;
22 import java.util.concurrent.TimeUnit;
23 
24 /**
25  * Wrapper around {@link Log} to allow writing to a file.
26  * This class can safely be called from main thread.
27  *
28  * Note: This should only be used for logging errors which have a persistent effect on user's data,
29  * but whose effect may not be visible immediately.
30  */
31 public final class FileLog {
32 
33     protected static final boolean ENABLED = true;
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                 sHandler = new Handler(createAndStartNewLooper("file-logger"),
95                         new LogWriterCallback());
96             }
97         }
98         return sHandler;
99     }
100 
101     /**
102      * Blocks until all the pending logs are written to the disk
103      * @param out if not null, all the persisted logs are copied to the writer.
104      */
flushAll(PrintWriter out)105     public static void flushAll(PrintWriter out) throws InterruptedException {
106         if (!ENABLED) {
107             return;
108         }
109         CountDownLatch latch = new CountDownLatch(1);
110         Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH,
111                 Pair.create(out, latch)).sendToTarget();
112 
113         latch.await(2, TimeUnit.SECONDS);
114     }
115 
116     /**
117      * Writes logs to the file.
118      * Log files are named log-0 for even days of the year and log-1 for odd days of the year.
119      * Logs older than 36 hours are purged.
120      */
121     private static class LogWriterCallback implements Handler.Callback {
122 
123         private static final long CLOSE_DELAY = 5000;  // 5 seconds
124 
125         private static final int MSG_WRITE = 1;
126         private static final int MSG_CLOSE = 2;
127         private static final int MSG_FLUSH = 3;
128 
129         private String mCurrentFileName = null;
130         private PrintWriter mCurrentWriter = null;
131 
closeWriter()132         private void closeWriter() {
133             IOUtils.closeSilently(mCurrentWriter);
134             mCurrentWriter = null;
135         }
136 
137         @Override
handleMessage(Message msg)138         public boolean handleMessage(Message msg) {
139             if (sLogsDirectory == null || !ENABLED) {
140                 return true;
141             }
142             switch (msg.what) {
143                 case MSG_WRITE: {
144                     Calendar cal = Calendar.getInstance();
145                     // suffix with 0 or 1 based on the day of the year.
146                     String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) & 1);
147 
148                     if (!fileName.equals(mCurrentFileName)) {
149                         closeWriter();
150                     }
151 
152                     try {
153                         if (mCurrentWriter == null) {
154                             mCurrentFileName = fileName;
155 
156                             boolean append = false;
157                             File logFile = new File(sLogsDirectory, fileName);
158                             if (logFile.exists()) {
159                                 Calendar modifiedTime = Calendar.getInstance();
160                                 modifiedTime.setTimeInMillis(logFile.lastModified());
161 
162                                 // If the file was modified more that 36 hours ago, purge the file.
163                                 // We use instead of 24 to account for day-365 followed by day-1
164                                 modifiedTime.add(Calendar.HOUR, 36);
165                                 append = cal.before(modifiedTime)
166                                         && logFile.length() < MAX_LOG_FILE_SIZE;
167                             }
168                             mCurrentWriter = new PrintWriter(new FileWriter(logFile, append));
169                         }
170 
171                         mCurrentWriter.println((String) msg.obj);
172                         mCurrentWriter.flush();
173 
174                         // Auto close file stream after some time.
175                         sHandler.removeMessages(MSG_CLOSE);
176                         sHandler.sendEmptyMessageDelayed(MSG_CLOSE, CLOSE_DELAY);
177                     } catch (Exception e) {
178                         Log.e("FileLog", "Error writing logs to file", e);
179                         // Close stream, will try reopening during next log
180                         closeWriter();
181                     }
182                     return true;
183                 }
184                 case MSG_CLOSE: {
185                     closeWriter();
186                     return true;
187                 }
188                 case MSG_FLUSH: {
189                     closeWriter();
190                     Pair<PrintWriter, CountDownLatch> p =
191                             (Pair<PrintWriter, CountDownLatch>) msg.obj;
192 
193                     if (p.first != null) {
194                         dumpFile(p.first, FILE_NAME_PREFIX + 0);
195                         dumpFile(p.first, FILE_NAME_PREFIX + 1);
196                     }
197                     p.second.countDown();
198                     return true;
199                 }
200             }
201             return true;
202         }
203     }
204 
205     private static void dumpFile(PrintWriter out, String fileName) {
206         File logFile = new File(sLogsDirectory, fileName);
207         if (logFile.exists()) {
208 
209             BufferedReader in = null;
210             try {
211                 in = new BufferedReader(new FileReader(logFile));
212                 out.println();
213                 out.println("--- logfile: " + fileName + " ---");
214                 String line;
215                 while ((line = in.readLine()) != null) {
216                     out.println(line);
217                 }
218             } catch (Exception e) {
219                 // ignore
220             } finally {
221                 IOUtils.closeSilently(in);
222             }
223         }
224     }
225 }
226