package com.door43.tools.reporting;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.channels.FileChannel;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Logs messages using the android Log class and also records logs to a file if configured.
* Requires permission WRITE_EXTERNAL_STORAGE in AndroidManifest.xml.
*/
public class Logger {
/**
* The pattern to match the leading log line
*/
public final static String PATTERN = "(\\d+\\/\\d+\\/\\d+\\s+\\d+:\\d+\\s+[A|P]M)\\s+([A-Z|])\\/(((?!:).)*):(.*)";
private final File logFile;
private final Level mMinLoggingLevel;
private final long mMaxLogFileSize;
private static Logger sInstance;
private static final long DEFAULT_MAX_LOG_FILE_SIZE = 1024 * 200;
static {
sInstance = new Logger(null, Level.Info);
}
public File getLogFile() {
if(!logFile.exists()){
logFile.mkdirs();
}
return logFile;
}
/**
* @param logFile
* @param minLoggingLevel
*/
private Logger(File logFile, Level minLoggingLevel) {
this.logFile = logFile;
if (minLoggingLevel == null) {
mMinLoggingLevel = Level.Info;
} else {
mMinLoggingLevel = minLoggingLevel;
}
mMaxLogFileSize = DEFAULT_MAX_LOG_FILE_SIZE;
}
/**
* @param logFile
* @param minLoggingLevel
* @param maxLogFileSize
*/
private Logger(File logFile, Level minLoggingLevel, long maxLogFileSize) {
this.logFile = logFile;
if (minLoggingLevel == null) {
mMinLoggingLevel = Level.Info;
} else {
mMinLoggingLevel = minLoggingLevel;
}
mMaxLogFileSize = maxLogFileSize;
}
/**
* Configures the logger to write log messages to a file
*
* @param logFile the file where logs will be written
* @param minLogingLevel the minimum level a log must be before it is recorded to the log file
*/
public static void configure(File logFile, Level minLogingLevel) {
sInstance = new Logger(logFile, minLogingLevel);
}
/**
* Configures the logger to write log messages to a file
*
* @param logFile the file where logs will be written
* @param minLogingLevel the minimum level a log must be before it is recorded to the log file
* @param maxLogFileSize the maximum size the log file may become before old logs are truncated
*/
public static void configure(File logFile, Level minLogingLevel, long maxLogFileSize) {
sInstance = new Logger(logFile, minLogingLevel, maxLogFileSize);
}
public enum Level {
Info(0, "I"),
Warning(1, "W"),
Error(2, "E");
Level(int i, String label) {
this.level = i;
this.label = label;
}
private int level;
private String label;
public int getIndex() {
return level;
}
public String getLabel() {
return label;
}
/**
* Returns a level by it's label
*
* @param label the case insensitive label of the level
* @return null if the level does not exist
*/
public static Level getLevel(String label) {
for (Level l : Level.values()) {
if (l.getLabel().toLowerCase().equals(label.toLowerCase())) {
return l;
}
}
return null;
}
/**
* Returns a level by it's index
*
* @param index the level index
* @return null if the level does not exist
*/
public static Level getLevel(int index) {
for (Level l : Level.values()) {
if (l.getIndex() == index) {
return l;
}
}
return null;
}
}
/**
* Sends an error message to LogCat and to a log file.
*
* @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
* class calling the logger.
* @param logMessage The message to add to the log.
*/
public static void e(String logMessageTag, String logMessage) {
try {
int logResult = Log.e(logMessageTag, logMessage);
if (logResult > 0) {
sInstance.logToFile(Level.Error, logMessageTag, logMessage);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Sends a warning message to LogCat and to a log file.
*
* @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
* class calling the logger.
* @param logMessage The message to add to the log.
*/
public static void w(String logMessageTag, String logMessage) {
try {
int logResult = Log.w(logMessageTag, logMessage);
if (logResult > 0) {
sInstance.logToFile(Level.Warning, logMessageTag, logMessage);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Sends an info message to LogCat and to a log file.
*
* @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
* class calling the logger.
* @param logMessage The message to add to the log.
*/
public static void i(String logMessageTag, String logMessage) {
try {
int logResult = Log.i(logMessageTag, logMessage);
if (logResult > 0) {
sInstance.logToFile(Level.Info, logMessageTag, logMessage);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Sends an error message and the exception to LogCat and to a log file.
*
* @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
* class calling the logger.
* @param logMessage The message to add to the log.
* @param throwableException An exception to log
*/
public static void e(String logMessageTag, String logMessage, Throwable throwableException) {
try {
int logResult = Log.e(logMessageTag, logMessage, throwableException);
if (logResult > 0) {
sInstance.logToFile(Level.Error, logMessageTag, logMessage + "\r\n" + Log.getStackTraceString(throwableException));
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Sends a message and the exception to LogCat and to a log file.
*
* @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
* class calling the logger.
* @param logMessage The message to add to the log.
* @param throwableException An exception to log
*/
public static void w(String logMessageTag, String logMessage, Throwable throwableException) {
try {
int logResult = Log.w(logMessageTag, logMessage, throwableException);
if (logResult > 0) {
sInstance.logToFile(Level.Warning, logMessageTag, logMessage + "\r\n" + Log.getStackTraceString(throwableException));
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Empties the log file
*/
public static void flush() {
if (sInstance.logFile != null) {
sInstance.logFile.delete();
} else {
Log.w(Logger.class.getName(), "The log file has not been configured and cannot be deleted");
}
}
/**
* Gets a stamp containing the current date and time to write to the log.
*
* @return The stamp for the current date and time.
*/
private static String getDateTimeStamp() {
Date dateNow = Calendar.getInstance().getTime();
return (DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.ENGLISH).format(dateNow));
}
/**
* Writes a message to the log file on the device.
*
* @param logMessageTag A tag identifying a group of log messages.
* @param logMessage The message to add to the log.
*/
private void logToFile(Level level, String logMessageTag, String logMessage) {
// filter out logging levels
if (level.getIndex() >= mMinLoggingLevel.getIndex() && logFile != null) {
try {
if (!logFile.exists()) {
logFile.getParentFile().mkdirs();
logFile.createNewFile();
}
// append log message
String log = FileUtil.getStringFromFile(logFile);
log = String.format("%1s %2s/%3s: %4s\r\n%5s", getDateTimeStamp(), level.getLabel(), logMessageTag, logMessage, log);
logFile.delete();
FileUtil.saveFile(logFile, log.getBytes());
// truncate the log if it gets too big.
if (logFile.length() > mMaxLogFileSize) {
FileChannel outChan = new FileOutputStream(logFile, true).getChannel();
outChan.truncate(mMaxLogFileSize * (long) 0.8);
outChan.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Returns a list of log entries
* @return
*/
public static List<Entry> getLogEntries() {
List<Entry> logs = new ArrayList<>();
if (sInstance.logFile != null) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(sInstance.getLogFile())));
StringBuilder sb = new StringBuilder();
String line;
Pattern pattern = Pattern.compile(Logger.PATTERN);
Entry log = null;
while ((line = br.readLine()) != null) {
if (Thread.interrupted()) break;
Matcher match = pattern.matcher(line);
if (match.find()) {
// save log
if (log != null) {
log.setDetails(sb.toString().trim());
logs.add(log);
sb.setLength(0);
}
// start new log
SimpleDateFormat format = new SimpleDateFormat("MM/dd/yy hh:mm a");
log = new Entry(format.parse(match.group(1)), Level.getLevel(match.group(2)), match.group(3), match.group(5));
} else {
// build log details
sb.append(line);
}
}
// save the last log
if (log != null) {
log.setDetails(sb.toString().trim());
logs.add(log);
sb.setLength(0);
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
Log.w(Logger.class.getName(), "The log file has not been configured and cannot be read");
}
return logs;
}
public static class Entry {
public final Date date;
public final Level level;
public final String classPath;
public final String message;
private String mDetails;
/**
* Creates a new error object
*
* @param date
* @param level
* @param classPath
* @param message
*/
public Entry(Date date, Level level, String classPath, String message) {
this.date = date;
this.level = level;
this.classPath = classPath;
this.message = message;
}
/**
* Creates a new error object
*
* @param date
* @param level
* @param classPath
* @param message
* @param details
*/
public Entry(Date date, Level level, String classPath, String message, String details) {
this.date = date;
this.level = level;
this.classPath = classPath;
this.message = message;
mDetails = details;
}
/**
* Sets the error log details
*
* @param details
*/
public void setDetails(String details) {
mDetails = details;
}
/**
* Returns the error log details
*
* @return
*/
public String getDetails() {
return mDetails;
}
}
}