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 
17 package com.android.messaging.util;
18 
19 import android.os.Process;
20 import android.util.Log;
21 
22 import com.android.messaging.Factory;
23 
24 import java.io.BufferedReader;
25 import java.io.File;
26 import java.io.FileNotFoundException;
27 import java.io.FileReader;
28 import java.io.IOException;
29 import java.io.PrintWriter;
30 import java.text.SimpleDateFormat;
31 import java.util.logging.FileHandler;
32 import java.util.logging.Formatter;
33 import java.util.logging.Handler;
34 import java.util.logging.Level;
35 import java.util.logging.Logger;
36 
37 /**
38  * Save the app's own log to dump along with adb bugreport
39  */
40 public 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      */
dump(PrintWriter writer)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      */
log(int level, String tag, String msg)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      */
isCurrent()64     public abstract boolean isCurrent();
65 
LogSaver()66     private LogSaver() {
67     }
68 
newInstance()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 
LogRecord()107             LogRecord() {
108             }
109 
set(int tid, int level, long time, String tag, String message)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 
MemoryLogSaver(final int size)125         public MemoryLogSaver(final int size) {
126             mSize = size;
127             mLogList = new CircularArray<LogRecord>(size);
128             mLock = new Object();
129         }
130 
131         @Override
dump(PrintWriter writer)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
log(int level, String tag, String msg)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
isCurrent()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 
DiskLogSaver(final int setSize, final int fileLimitBytes)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 
clearDefaultHandlers(Logger logger)190         private static void clearDefaultHandlers(Logger logger) {
191             Assert.notNull(logger);
192             for (Handler handler : logger.getHandlers()) {
193                 logger.removeHandler(handler);
194             }
195         }
196 
initDiskLog()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
dump(PrintWriter writer)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
log(int level, String tag, String msg)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
isCurrent()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 
getLevelString(final int level)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