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.LauncherModel;
10 import com.android.launcher3.Utilities;
11 import com.android.launcher3.config.ProviderConfig;
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     private static final String FILE_NAME_PREFIX = "log-";
34     private static final DateFormat DATE_FORMAT =
35             DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
36 
37     private static final long MAX_LOG_FILE_SIZE = 4 << 20;  // 4 mb
38 
39     private static Handler sHandler = null;
40     private static File sLogsDirectory = null;
41 
setDir(File logsDir)42     public static void setDir(File logsDir) {
43         if (ProviderConfig.IS_DOGFOOD_BUILD || Utilities.IS_DEBUG_DEVICE) {
44             synchronized (DATE_FORMAT) {
45                 // If the target directory changes, stop any active thread.
46                 if (sHandler != null && !logsDir.equals(sLogsDirectory)) {
47                     ((HandlerThread) sHandler.getLooper().getThread()).quit();
48                     sHandler = null;
49                 }
50             }
51         }
52         sLogsDirectory = logsDir;
53     }
54 
d(String tag, String msg, Exception e)55     public static void d(String tag, String msg, Exception e) {
56         Log.d(tag, msg, e);
57         print(tag, msg, e);
58     }
59 
d(String tag, String msg)60     public static void d(String tag, String msg) {
61         Log.d(tag, msg);
62         print(tag, msg);
63     }
64 
e(String tag, String msg, Exception e)65     public static void e(String tag, String msg, Exception e) {
66         Log.e(tag, msg, e);
67         print(tag, msg, e);
68     }
69 
e(String tag, String msg)70     public static void e(String tag, String msg) {
71         Log.e(tag, msg);
72         print(tag, msg);
73     }
74 
print(String tag, String msg)75     public static void print(String tag, String msg) {
76         print(tag, msg, null);
77     }
78 
print(String tag, String msg, Exception e)79     public static void print(String tag, String msg, Exception e) {
80         if (!ProviderConfig.IS_DOGFOOD_BUILD) {
81             return;
82         }
83         String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg);
84         if (e != null) {
85             out += "\n" + Log.getStackTraceString(e);
86         }
87         Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget();
88     }
89 
getHandler()90     private static Handler getHandler() {
91         synchronized (DATE_FORMAT) {
92             if (sHandler == null) {
93                 HandlerThread thread = new HandlerThread("file-logger");
94                 thread.start();
95                 sHandler = new Handler(thread.getLooper(), 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 (!ProviderConfig.IS_DOGFOOD_BUILD) {
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             Utilities.closeSilently(mCurrentWriter);
134             mCurrentWriter = null;
135         }
136 
137         @Override
handleMessage(Message msg)138         public boolean handleMessage(Message msg) {
139             if (sLogsDirectory == null || !ProviderConfig.IS_DOGFOOD_BUILD) {
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                 Utilities.closeSilently(in);
222             }
223         }
224     }
225 }
226