• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
e(String tag, String msg, Exception e)70     public static void e(String tag, String msg, Exception e) {
71         Log.e(tag, msg, e);
72         print(tag, msg, e);
73     }
74 
e(String tag, String msg)75     public static void e(String tag, String msg) {
76         Log.e(tag, msg);
77         print(tag, msg);
78     }
79 
print(String tag, String msg)80     public static void print(String tag, String msg) {
81         print(tag, msg, null);
82     }
83 
print(String tag, String msg, Exception e)84     public static void print(String tag, String msg, Exception e) {
85         if (!ENABLED) {
86             return;
87         }
88         String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg);
89         if (e != null) {
90             out += "\n" + Log.getStackTraceString(e);
91         }
92         Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget();
93     }
94 
95     @VisibleForTesting
getHandler()96     static Handler getHandler() {
97         synchronized (DATE_FORMAT) {
98             if (sHandler == null) {
99                 sHandler = new Handler(createAndStartNewLooper("file-logger"),
100                         new LogWriterCallback());
101             }
102         }
103         return sHandler;
104     }
105 
106     /**
107      * Blocks until all the pending logs are written to the disk
108      * @param out if not null, all the persisted logs are copied to the writer.
109      */
flushAll(PrintWriter out)110     public static boolean flushAll(PrintWriter out) throws InterruptedException {
111         if (!ENABLED) {
112             return false;
113         }
114         CountDownLatch latch = new CountDownLatch(1);
115         Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH,
116                 Pair.create(out, latch)).sendToTarget();
117 
118         latch.await(2, TimeUnit.SECONDS);
119         return latch.getCount() == 0;
120     }
121 
122     /**
123      * Writes logs to the file.
124      * Log files are named log-0 for even days of the year and log-1 for odd days of the year.
125      * Logs older than 36 hours are purged.
126      */
127     private static class LogWriterCallback implements Handler.Callback {
128 
129         private static final long CLOSE_DELAY = 5000;  // 5 seconds
130 
131         private static final int MSG_WRITE = 1;
132         private static final int MSG_CLOSE = 2;
133         private static final int MSG_FLUSH = 3;
134 
135         private String mCurrentFileName = null;
136         private PrintWriter mCurrentWriter = null;
137 
closeWriter()138         private void closeWriter() {
139             IOUtils.closeSilently(mCurrentWriter);
140             mCurrentWriter = null;
141         }
142 
143         @Override
handleMessage(Message msg)144         public boolean handleMessage(Message msg) {
145             if (sLogsDirectory == null || !ENABLED) {
146                 return true;
147             }
148             switch (msg.what) {
149                 case MSG_WRITE: {
150                     Calendar cal = Calendar.getInstance();
151                     // suffix with 0 or 1 based on the day of the year.
152                     String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % LOG_DAYS);
153 
154                     if (!fileName.equals(mCurrentFileName)) {
155                         closeWriter();
156                     }
157 
158                     try {
159                         if (mCurrentWriter == null) {
160                             mCurrentFileName = fileName;
161 
162                             boolean append = false;
163                             File logFile = new File(sLogsDirectory, fileName);
164                             if (logFile.exists()) {
165                                 Calendar modifiedTime = Calendar.getInstance();
166                                 modifiedTime.setTimeInMillis(logFile.lastModified());
167 
168                                 // If the file was modified more that 36 hours ago, purge the file.
169                                 // We use instead of 24 to account for day-365 followed by day-1
170                                 modifiedTime.add(Calendar.HOUR, 36);
171                                 append = cal.before(modifiedTime)
172                                         && logFile.length() < MAX_LOG_FILE_SIZE;
173                             }
174                             mCurrentWriter = new PrintWriter(new FileWriter(logFile, append));
175                         }
176 
177                         mCurrentWriter.println((String) msg.obj);
178                         mCurrentWriter.flush();
179 
180                         // Auto close file stream after some time.
181                         sHandler.removeMessages(MSG_CLOSE);
182                         sHandler.sendEmptyMessageDelayed(MSG_CLOSE, CLOSE_DELAY);
183                     } catch (Exception e) {
184                         Log.e("FileLog", "Error writing logs to file", e);
185                         // Close stream, will try reopening during next log
186                         closeWriter();
187                     }
188                     return true;
189                 }
190                 case MSG_CLOSE: {
191                     closeWriter();
192                     return true;
193                 }
194                 case MSG_FLUSH: {
195                     closeWriter();
196                     Pair<PrintWriter, CountDownLatch> p =
197                             (Pair<PrintWriter, CountDownLatch>) msg.obj;
198 
199                     if (p.first != null) {
200                         for (int i = 0; i < LOG_DAYS; i++) {
201                             dumpFile(p.first, FILE_NAME_PREFIX + i);
202                         }
203                     }
204                     p.second.countDown();
205                     return true;
206                 }
207             }
208             return true;
209         }
210     }
211 
212     private static void dumpFile(PrintWriter out, String fileName) {
213         File logFile = new File(sLogsDirectory, fileName);
214         if (logFile.exists()) {
215 
216             BufferedReader in = null;
217             try {
218                 in = new BufferedReader(new FileReader(logFile));
219                 out.println();
220                 out.println("--- logfile: " + fileName + " ---");
221                 String line;
222                 while ((line = in.readLine()) != null) {
223                     out.println(line);
224                 }
225             } catch (Exception e) {
226                 // ignore
227             } finally {
228                 IOUtils.closeSilently(in);
229             }
230         }
231     }
232 
233     /**
234      * Gets files used for FileLog
235      */
236     public static File[] getLogFiles() {
237         try {
238             flushAll(null);
239         } catch (InterruptedException e) { }
240         File[] files = new File[LOG_DAYS];
241         for (int i = 0; i < LOG_DAYS; i++) {
242             files[i] = new File(sLogsDirectory, FILE_NAME_PREFIX + i);
243         }
244         return files;
245     }
246 }
247