1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.messaging.util;
18
19import android.os.Process;
20import android.util.Log;
21
22import com.android.messaging.Factory;
23
24import java.io.BufferedReader;
25import java.io.File;
26import java.io.FileNotFoundException;
27import java.io.FileReader;
28import java.io.IOException;
29import java.io.PrintWriter;
30import java.text.SimpleDateFormat;
31import java.util.logging.FileHandler;
32import java.util.logging.Formatter;
33import java.util.logging.Handler;
34import java.util.logging.Level;
35import java.util.logging.Logger;
36
37/**
38 * Save the app's own log to dump along with adb bugreport
39 */
40public abstract class LogSaver {
41    /**
42     * Writes the accumulated log entries, from oldest to newest, to the specified PrintWriter.
43     * Log lines are emitted in much the same form as logcat -v threadtime -- specifically,
44     * lines will include a timestamp, pid, tid, level, and tag.
45     *
46     * @param writer The PrintWriter to output
47     */
48    public abstract void dump(PrintWriter writer);
49
50    /**
51     * Log a line
52     *
53     * @param level The log level to use
54     * @param tag The log tag
55     * @param msg The message of the log line
56     */
57    public abstract void log(int level, String tag, String msg);
58
59    /**
60     * Check if the LogSaver still matches the current Gservices settings
61     *
62     * @return true if matches, false otherwise
63     */
64    public abstract boolean isCurrent();
65
66    private LogSaver() {
67    }
68
69    public static LogSaver newInstance() {
70        final boolean persistent = BugleGservices.get().getBoolean(
71                BugleGservicesKeys.PERSISTENT_LOGSAVER,
72                BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT);
73        if (persistent) {
74            final int setSize = BugleGservices.get().getInt(
75                    BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE,
76                    BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE_DEFAULT);
77            final int fileLimitBytes = BugleGservices.get().getInt(
78                    BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES,
79                    BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES_DEFAULT);
80            return new DiskLogSaver(setSize, fileLimitBytes);
81        } else {
82            final int size = BugleGservices.get().getInt(
83                    BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT,
84                    BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT_DEFAULT);
85            return new MemoryLogSaver(size);
86        }
87    }
88
89    /**
90     * A circular in-memory log to be used to log potentially verbose logs. The logs will be
91     * persisted in memory in the application and can be dumped by various dump() methods.
92     * For example, adb shell dumpsys activity provider com.android.messaging.
93     * The dump will also show up in bugreports.
94     */
95    private static final class MemoryLogSaver extends LogSaver {
96        /**
97         * Record to store a single log entry. Stores timestamp, tid, level, tag, and message.
98         * It can be reused when the circular log rolls over. This avoids creating new objects.
99         */
100        private static class LogRecord {
101            int mTid;
102            String mLevelString;
103            long mTimeMillis;     // from System.currentTimeMillis
104            String mTag;
105            String mMessage;
106
107            LogRecord() {
108            }
109
110            void set(int tid, int level, long time, String tag, String message) {
111                this.mTid = tid;
112                this.mTimeMillis = time;
113                this.mTag = tag;
114                this.mMessage = message;
115                this.mLevelString = getLevelString(level);
116            }
117        }
118
119        private final int mSize;
120        private final CircularArray<LogRecord> mLogList;
121        private final Object mLock;
122
123        private final SimpleDateFormat mSdf = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
124
125        public MemoryLogSaver(final int size) {
126            mSize = size;
127            mLogList = new CircularArray<LogRecord>(size);
128            mLock = new Object();
129        }
130
131        @Override
132        public void dump(PrintWriter writer) {
133            int pid = Process.myPid();
134            synchronized (mLock) {
135                for (int i = 0; i < mLogList.count(); i++) {
136                    LogRecord rec = mLogList.get(i);
137                    writer.println(String.format("%s %5d %5d %s %s: %s",
138                            mSdf.format(rec.mTimeMillis),
139                            pid, rec.mTid, rec.mLevelString, rec.mTag, rec.mMessage));
140                }
141            }
142        }
143
144        @Override
145        public void log(int level, String tag, String msg) {
146            synchronized (mLock) {
147                LogRecord rec = mLogList.getFree();
148                if (rec == null) {
149                    rec = new LogRecord();
150                }
151                rec.set(Process.myTid(), level, System.currentTimeMillis(), tag, msg);
152                mLogList.add(rec);
153            }
154        }
155
156        @Override
157        public boolean isCurrent() {
158            final boolean persistent = BugleGservices.get().getBoolean(
159                    BugleGservicesKeys.PERSISTENT_LOGSAVER,
160                    BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT);
161            if (persistent) {
162                return false;
163            }
164            final int size = BugleGservices.get().getInt(
165                    BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT,
166                    BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT_DEFAULT);
167            return size == mSize;
168        }
169    }
170
171    /**
172     * A persistent, on-disk log saver. It uses the standard Java util logger along with
173     * a rotation log file set to store the logs in app's local file directory "app_logs".
174     */
175    private static final class DiskLogSaver extends LogSaver {
176        private static final String DISK_LOG_DIR_NAME = "logs";
177
178        private final int mSetSize;
179        private final int mFileLimitBytes;
180        private Logger mDiskLogger;
181
182        public DiskLogSaver(final int setSize, final int fileLimitBytes) {
183            Assert.isTrue(setSize > 0);
184            Assert.isTrue(fileLimitBytes > 0);
185            mSetSize = setSize;
186            mFileLimitBytes = fileLimitBytes;
187            initDiskLog();
188        }
189
190        private static void clearDefaultHandlers(Logger logger) {
191            Assert.notNull(logger);
192            for (Handler handler : logger.getHandlers()) {
193                logger.removeHandler(handler);
194            }
195        }
196
197        private void initDiskLog() {
198            mDiskLogger = Logger.getLogger(LogUtil.BUGLE_TAG);
199            // We don't want the default console handler
200            clearDefaultHandlers(mDiskLogger);
201            // Don't want duplicate print in system log
202            mDiskLogger.setUseParentHandlers(false);
203            // FileHandler manages the log files in a fixed rotation set
204            final File logDir = Factory.get().getApplicationContext().getDir(
205                    DISK_LOG_DIR_NAME, 0/*mode*/);
206            FileHandler handler = null;
207            try {
208                handler = new FileHandler(
209                        logDir + "/%g.log", mFileLimitBytes, mSetSize, true/*append*/);
210            } catch (Exception e) {
211                Log.e(LogUtil.BUGLE_TAG, "LogSaver: fail to init disk logger", e);
212                return;
213            }
214            final Formatter formatter = new Formatter() {
215                @Override
216                public String format(java.util.logging.LogRecord r) {
217                    return r.getMessage();
218                }
219            };
220            handler.setFormatter(formatter);
221            handler.setLevel(Level.ALL);
222            mDiskLogger.addHandler(handler);
223        }
224
225        @Override
226        public void dump(PrintWriter writer) {
227            for (int i = mSetSize - 1; i >= 0; i--) {
228                final File logDir = Factory.get().getApplicationContext().getDir(
229                        DISK_LOG_DIR_NAME, 0/*mode*/);
230                final String logFilePath = logDir + "/" + i + ".log";
231                try {
232                    final File logFile = new File(logFilePath);
233                    if (!logFile.exists()) {
234                        continue;
235                    }
236                    final BufferedReader reader = new BufferedReader(new FileReader(logFile));
237                    for (String line; (line = reader.readLine()) != null;) {
238                        line = line.trim();
239                        writer.println(line);
240                    }
241                } catch (FileNotFoundException e) {
242                    Log.w(LogUtil.BUGLE_TAG, "LogSaver: can not find log file " + logFilePath);
243                } catch (IOException e) {
244                    Log.w(LogUtil.BUGLE_TAG, "LogSaver: can not read log file", e);
245                }
246            }
247        }
248
249        @Override
250        public void log(int level, String tag, String msg) {
251            final SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
252            mDiskLogger.info(String.format("%s %5d %5d %s %s: %s\n",
253                    sdf.format(System.currentTimeMillis()),
254                    Process.myPid(), Process.myTid(), getLevelString(level), tag, msg));
255        }
256
257        @Override
258        public boolean isCurrent() {
259            final boolean persistent = BugleGservices.get().getBoolean(
260                    BugleGservicesKeys.PERSISTENT_LOGSAVER,
261                    BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT);
262            if (!persistent) {
263                return false;
264            }
265            final int setSize = BugleGservices.get().getInt(
266                    BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE,
267                    BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE_DEFAULT);
268            final int fileLimitBytes = BugleGservices.get().getInt(
269                    BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES,
270                    BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES_DEFAULT);
271            return setSize == mSetSize && fileLimitBytes == mFileLimitBytes;
272        }
273    }
274
275    private static String getLevelString(final int level) {
276        switch (level) {
277            case android.util.Log.DEBUG:
278                return "D";
279            case android.util.Log.WARN:
280                return "W";
281            case android.util.Log.INFO:
282                return "I";
283            case android.util.Log.VERBOSE:
284                return "V";
285            case android.util.Log.ERROR:
286                return "E";
287            case android.util.Log.ASSERT:
288                return "A";
289            default:
290                return "?";
291        }
292    }
293}
294