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.config.FeatureFlags;
14 import com.android.launcher3.util.IOUtils;
15 
16 import java.io.BufferedReader;
17 import java.io.File;
18 import java.io.FileReader;
19 import java.io.FileWriter;
20 import java.io.PrintWriter;
21 import java.text.DateFormat;
22 import java.util.Calendar;
23 import java.util.Date;
24 import java.util.concurrent.CountDownLatch;
25 import java.util.concurrent.TimeUnit;
26 
27 /**
28  * Wrapper around {@link Log} to allow writing to a file.
29  * This class can safely be called from main thread.
30  *
31  * Note: This should only be used for logging errors which have a persistent effect on user's data,
32  * but whose effect may not be visible immediately.
33  */
34 public final class FileLog {
35 
36     protected static final boolean ENABLED = true;
37     private static final String FILE_NAME_PREFIX = "log-";
38     private static final DateFormat DATE_FORMAT =
39             DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
40 
41     private static final long MAX_LOG_FILE_SIZE = 8 << 20;  // 4 mb
42 
43     private static Handler sHandler = null;
44     private static File sLogsDirectory = null;
45 
46     public static final int LOG_DAYS = FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() ? 10 : 4;
47 
setDir(File logsDir)48     public static void setDir(File logsDir) {
49         if (ENABLED) {
50             synchronized (DATE_FORMAT) {
51                 // If the target directory changes, stop any active thread.
52                 if (sHandler != null && !logsDir.equals(sLogsDirectory)) {
53                     ((HandlerThread) sHandler.getLooper().getThread()).quit();
54                     sHandler = null;
55                 }
56             }
57         }
58         sLogsDirectory = logsDir;
59     }
60 
d(String tag, String msg, Exception e)61     public static void d(String tag, String msg, Exception e) {
62         Log.d(tag, msg, e);
63         print(tag, msg, e);
64     }
65 
d(String tag, String msg)66     public static void d(String tag, String msg) {
67         Log.d(tag, msg);
68         print(tag, msg);
69     }
70 
e(String tag, String msg, Exception e)71     public static void e(String tag, String msg, Exception e) {
72         Log.e(tag, msg, e);
73         print(tag, msg, e);
74     }
75 
e(String tag, String msg)76     public static void e(String tag, String msg) {
77         Log.e(tag, msg);
78         print(tag, msg);
79     }
80 
print(String tag, String msg)81     public static void print(String tag, String msg) {
82         print(tag, msg, null);
83     }
84 
print(String tag, String msg, Exception e)85     public static void print(String tag, String msg, Exception e) {
86         if (!ENABLED) {
87             return;
88         }
89         String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg);
90         if (e != null) {
91             out += "\n" + Log.getStackTraceString(e);
92         }
93         Message.obtain(getHandler(), LogWriterCallback.MSG_WRITE, out).sendToTarget();
94     }
95 
96     @VisibleForTesting
getHandler()97     static Handler getHandler() {
98         synchronized (DATE_FORMAT) {
99             if (sHandler == null) {
100                 sHandler = new Handler(createAndStartNewLooper("file-logger"),
101                         new LogWriterCallback());
102             }
103         }
104         return sHandler;
105     }
106 
107     /**
108      * Blocks until all the pending logs are written to the disk
109      * @param out if not null, all the persisted logs are copied to the writer.
110      */
flushAll(PrintWriter out)111     public static boolean flushAll(PrintWriter out) throws InterruptedException {
112         if (!ENABLED) {
113             return false;
114         }
115         CountDownLatch latch = new CountDownLatch(1);
116         Message.obtain(getHandler(), LogWriterCallback.MSG_FLUSH,
117                 Pair.create(out, latch)).sendToTarget();
118 
119         latch.await(2, TimeUnit.SECONDS);
120         return latch.getCount() == 0;
121     }
122 
123     /**
124      * Writes logs to the file.
125      * Log files are named log-0 for even days of the year and log-1 for odd days of the year.
126      * Logs older than 36 hours are purged.
127      */
128     private static class LogWriterCallback implements Handler.Callback {
129 
130         private static final long CLOSE_DELAY = 5000;  // 5 seconds
131 
132         private static final int MSG_WRITE = 1;
133         private static final int MSG_CLOSE = 2;
134         private static final int MSG_FLUSH = 3;
135 
136         private String mCurrentFileName = null;
137         private PrintWriter mCurrentWriter = null;
138 
closeWriter()139         private void closeWriter() {
140             IOUtils.closeSilently(mCurrentWriter);
141             mCurrentWriter = null;
142         }
143 
144         @Override
handleMessage(Message msg)145         public boolean handleMessage(Message msg) {
146             if (sLogsDirectory == null || !ENABLED) {
147                 return true;
148             }
149             switch (msg.what) {
150                 case MSG_WRITE: {
151                     Calendar cal = Calendar.getInstance();
152                     // suffix with 0 or 1 based on the day of the year.
153                     String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % LOG_DAYS);
154 
155                     if (!fileName.equals(mCurrentFileName)) {
156                         closeWriter();
157                     }
158 
159                     try {
160                         if (mCurrentWriter == null) {
161                             mCurrentFileName = fileName;
162 
163                             boolean append = false;
164                             File logFile = new File(sLogsDirectory, fileName);
165                             if (logFile.exists()) {
166                                 Calendar modifiedTime = Calendar.getInstance();
167                                 modifiedTime.setTimeInMillis(logFile.lastModified());
168 
169                                 // If the file was modified more that 36 hours ago, purge the file.
170                                 // We use instead of 24 to account for day-365 followed by day-1
171                                 modifiedTime.add(Calendar.HOUR, 36);
172                                 append = cal.before(modifiedTime)
173                                         && logFile.length() < MAX_LOG_FILE_SIZE;
174                             }
175                             mCurrentWriter = new PrintWriter(new FileWriter(logFile, append));
176                         }
177 
178                         mCurrentWriter.println((String) msg.obj);
179                         mCurrentWriter.flush();
180 
181                         // Auto close file stream after some time.
182                         sHandler.removeMessages(MSG_CLOSE);
183                         sHandler.sendEmptyMessageDelayed(MSG_CLOSE, CLOSE_DELAY);
184                     } catch (Exception e) {
185                         Log.e("FileLog", "Error writing logs to file", e);
186                         // Close stream, will try reopening during next log
187                         closeWriter();
188                     }
189                     return true;
190                 }
191                 case MSG_CLOSE: {
192                     closeWriter();
193                     return true;
194                 }
195                 case MSG_FLUSH: {
196                     closeWriter();
197                     Pair<PrintWriter, CountDownLatch> p =
198                             (Pair<PrintWriter, CountDownLatch>) msg.obj;
199 
200                     if (p.first != null) {
201                         for (int i = 0; i < LOG_DAYS; i++) {
202                             dumpFile(p.first, FILE_NAME_PREFIX + i);
203                         }
204                     }
205                     p.second.countDown();
206                     return true;
207                 }
208             }
209             return true;
210         }
211     }
212 
213     private static void dumpFile(PrintWriter out, String fileName) {
214         File logFile = new File(sLogsDirectory, fileName);
215         if (logFile.exists()) {
216 
217             BufferedReader in = null;
218             try {
219                 in = new BufferedReader(new FileReader(logFile));
220                 out.println();
221                 out.println("--- logfile: " + fileName + " ---");
222                 String line;
223                 while ((line = in.readLine()) != null) {
224                     out.println(line);
225                 }
226             } catch (Exception e) {
227                 // ignore
228             } finally {
229                 IOUtils.closeSilently(in);
230             }
231         }
232     }
233 
234     /**
235      * Gets files used for FileLog
236      */
237     public static File[] getLogFiles() {
238         try {
239             flushAll(null);
240         } catch (InterruptedException e) { }
241         File[] files = new File[LOG_DAYS];
242         for (int i = 0; i < LOG_DAYS; i++) {
243             files[i] = new File(sLogsDirectory, FILE_NAME_PREFIX + i);
244         }
245         return files;
246     }
247 }
248