1package com.android.launcher3.logging;
2
3import android.os.Handler;
4import android.os.HandlerThread;
5import android.os.Message;
6import android.util.Log;
7import android.util.Pair;
8
9import com.android.launcher3.LauncherModel;
10import com.android.launcher3.Utilities;
11import com.android.launcher3.config.ProviderConfig;
12
13import java.io.BufferedReader;
14import java.io.File;
15import java.io.FileReader;
16import java.io.FileWriter;
17import java.io.PrintWriter;
18import java.text.DateFormat;
19import java.util.Calendar;
20import java.util.Date;
21import java.util.concurrent.CountDownLatch;
22import 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 */
31public 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
42    public static void setDir(File logsDir) {
43        if (ProviderConfig.IS_DOGFOOD_BUILD) {
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
55    public static void d(String tag, String msg, Exception e) {
56        Log.d(tag, msg, e);
57        print(tag, msg, e);
58    }
59
60    public static void d(String tag, String msg) {
61        Log.d(tag, msg);
62        print(tag, msg);
63    }
64
65    public static void e(String tag, String msg, Exception e) {
66        Log.e(tag, msg, e);
67        print(tag, msg, e);
68    }
69
70    public static void e(String tag, String msg) {
71        Log.e(tag, msg);
72        print(tag, msg);
73    }
74
75    public static void print(String tag, String msg) {
76        print(tag, msg, null);
77    }
78
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
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     */
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
132        private void closeWriter() {
133            Utilities.closeSilently(mCurrentWriter);
134            mCurrentWriter = null;
135        }
136
137        @Override
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