/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.messaging.util; import android.os.Process; import android.util.Log; import com.android.messaging.Factory; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.logging.FileHandler; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; /** * Save the app's own log to dump along with adb bugreport */ public abstract class LogSaver { /** * Writes the accumulated log entries, from oldest to newest, to the specified PrintWriter. * Log lines are emitted in much the same form as logcat -v threadtime -- specifically, * lines will include a timestamp, pid, tid, level, and tag. * * @param writer The PrintWriter to output */ public abstract void dump(PrintWriter writer); /** * Log a line * * @param level The log level to use * @param tag The log tag * @param msg The message of the log line */ public abstract void log(int level, String tag, String msg); /** * Check if the LogSaver still matches the current Gservices settings * * @return true if matches, false otherwise */ public abstract boolean isCurrent(); private LogSaver() { } public static LogSaver newInstance() { final boolean persistent = BugleGservices.get().getBoolean( BugleGservicesKeys.PERSISTENT_LOGSAVER, BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT); if (persistent) { final int setSize = BugleGservices.get().getInt( BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE, BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE_DEFAULT); final int fileLimitBytes = BugleGservices.get().getInt( BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES, BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES_DEFAULT); return new DiskLogSaver(setSize, fileLimitBytes); } else { final int size = BugleGservices.get().getInt( BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT, BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT_DEFAULT); return new MemoryLogSaver(size); } } /** * A circular in-memory log to be used to log potentially verbose logs. The logs will be * persisted in memory in the application and can be dumped by various dump() methods. * For example, adb shell dumpsys activity provider com.android.messaging. * The dump will also show up in bugreports. */ private static final class MemoryLogSaver extends LogSaver { /** * Record to store a single log entry. Stores timestamp, tid, level, tag, and message. * It can be reused when the circular log rolls over. This avoids creating new objects. */ private static class LogRecord { int mTid; String mLevelString; long mTimeMillis; // from System.currentTimeMillis String mTag; String mMessage; LogRecord() { } void set(int tid, int level, long time, String tag, String message) { this.mTid = tid; this.mTimeMillis = time; this.mTag = tag; this.mMessage = message; this.mLevelString = getLevelString(level); } } private final int mSize; private final CircularArray mLogList; private final Object mLock; private final SimpleDateFormat mSdf = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); public MemoryLogSaver(final int size) { mSize = size; mLogList = new CircularArray(size); mLock = new Object(); } @Override public void dump(PrintWriter writer) { int pid = Process.myPid(); synchronized (mLock) { for (int i = 0; i < mLogList.count(); i++) { LogRecord rec = mLogList.get(i); writer.println(String.format("%s %5d %5d %s %s: %s", mSdf.format(rec.mTimeMillis), pid, rec.mTid, rec.mLevelString, rec.mTag, rec.mMessage)); } } } @Override public void log(int level, String tag, String msg) { synchronized (mLock) { LogRecord rec = mLogList.getFree(); if (rec == null) { rec = new LogRecord(); } rec.set(Process.myTid(), level, System.currentTimeMillis(), tag, msg); mLogList.add(rec); } } @Override public boolean isCurrent() { final boolean persistent = BugleGservices.get().getBoolean( BugleGservicesKeys.PERSISTENT_LOGSAVER, BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT); if (persistent) { return false; } final int size = BugleGservices.get().getInt( BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT, BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT_DEFAULT); return size == mSize; } } /** * A persistent, on-disk log saver. It uses the standard Java util logger along with * a rotation log file set to store the logs in app's local file directory "app_logs". */ private static final class DiskLogSaver extends LogSaver { private static final String DISK_LOG_DIR_NAME = "logs"; private final int mSetSize; private final int mFileLimitBytes; private Logger mDiskLogger; public DiskLogSaver(final int setSize, final int fileLimitBytes) { Assert.isTrue(setSize > 0); Assert.isTrue(fileLimitBytes > 0); mSetSize = setSize; mFileLimitBytes = fileLimitBytes; initDiskLog(); } private static void clearDefaultHandlers(Logger logger) { Assert.notNull(logger); for (Handler handler : logger.getHandlers()) { logger.removeHandler(handler); } } private void initDiskLog() { mDiskLogger = Logger.getLogger(LogUtil.BUGLE_TAG); // We don't want the default console handler clearDefaultHandlers(mDiskLogger); // Don't want duplicate print in system log mDiskLogger.setUseParentHandlers(false); // FileHandler manages the log files in a fixed rotation set final File logDir = Factory.get().getApplicationContext().getDir( DISK_LOG_DIR_NAME, 0/*mode*/); FileHandler handler = null; try { handler = new FileHandler( logDir + "/%g.log", mFileLimitBytes, mSetSize, true/*append*/); } catch (Exception e) { Log.e(LogUtil.BUGLE_TAG, "LogSaver: fail to init disk logger", e); return; } final Formatter formatter = new Formatter() { @Override public String format(java.util.logging.LogRecord r) { return r.getMessage(); } }; handler.setFormatter(formatter); handler.setLevel(Level.ALL); mDiskLogger.addHandler(handler); } @Override public void dump(PrintWriter writer) { for (int i = mSetSize - 1; i >= 0; i--) { final File logDir = Factory.get().getApplicationContext().getDir( DISK_LOG_DIR_NAME, 0/*mode*/); final String logFilePath = logDir + "/" + i + ".log"; try { final File logFile = new File(logFilePath); if (!logFile.exists()) { continue; } final BufferedReader reader = new BufferedReader(new FileReader(logFile)); for (String line; (line = reader.readLine()) != null;) { line = line.trim(); writer.println(line); } } catch (FileNotFoundException e) { Log.w(LogUtil.BUGLE_TAG, "LogSaver: can not find log file " + logFilePath); } catch (IOException e) { Log.w(LogUtil.BUGLE_TAG, "LogSaver: can not read log file", e); } } } @Override public void log(int level, String tag, String msg) { final SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); mDiskLogger.info(String.format("%s %5d %5d %s %s: %s\n", sdf.format(System.currentTimeMillis()), Process.myPid(), Process.myTid(), getLevelString(level), tag, msg)); } @Override public boolean isCurrent() { final boolean persistent = BugleGservices.get().getBoolean( BugleGservicesKeys.PERSISTENT_LOGSAVER, BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT); if (!persistent) { return false; } final int setSize = BugleGservices.get().getInt( BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE, BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE_DEFAULT); final int fileLimitBytes = BugleGservices.get().getInt( BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES, BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES_DEFAULT); return setSize == mSetSize && fileLimitBytes == mFileLimitBytes; } } private static String getLevelString(final int level) { switch (level) { case android.util.Log.DEBUG: return "D"; case android.util.Log.WARN: return "W"; case android.util.Log.INFO: return "I"; case android.util.Log.VERBOSE: return "V"; case android.util.Log.ERROR: return "E"; case android.util.Log.ASSERT: return "A"; default: return "?"; } } }