/*
* This source is part of the
* _____ ___ ____
* __ / / _ \/ _ | / __/___ _______ _
* / // / , _/ __ |/ _/_/ _ \/ __/ _ `/
* \___/_/|_/_/ |_/_/ (_)___/_/ \_, /
* /___/
* repository.
*
* Copyright (C) 2013 Benoit 'BoD' Lubek (BoD@JRAF.org)
* Copyright (C) 2014-2017 Carmen Alvarez (c@rmen.ca)
*
* 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 ca.rmen.android.networkmonitor.util;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import ca.rmen.android.networkmonitor.app.dbops.ui.Share;
/**
* A logger that appends messages to a file on the disk.<br/> {@link #init(Context, int, boolean)} must be called prior to using the other methods of this class
* (typically this should be done in {@link Application#onCreate()}).<br/>
* Before using the log file (for instance to send it to server), {@link #prepareLogFile(Context)} must be called.
* However, this is automatically called in case of an uncaught Exception.
*/
public class Log {
public static final String FILE = "log.txt";
private static final String FILE_0 = "log0.txt";
private static final String FILE_1 = "log1.txt";
private static final int MSG_V = 0;
private static final int MSG_D = 1;
private static final int MSG_I = 2;
private static final int MSG_W = 3;
private static final int MSG_E = 4;
private static final String KEY_TAG = "KEY_TAG";
private static final String KEY_MESSAGE = "KEY_MESSAGE";
private static final String KEY_DATE = "KEY_DATE";
private static final String KEY_THREADID = "KEY_THREADID";
private static final String KEY_THROWABLE = "KEY_THROWABLE";
private static int sMaxLogSize;
private static BufferedWriter sWriter;
private static File sFile0;
private static File sFile1;
private static File sCurrentFile;
private static Handler sHandler;
private static boolean sError = true;
private static boolean sErrorLogged;
private static boolean sAndroidLogD;
/**
* If you are using ACRA, this method must be called <em>after</em> calling {@code ACRA.init()}.
*
* @param maxLogSize Max log size in bytes.
* @param androidLogD If {@code true}, then {@link #d(String, String)} and {@link #v(String, String)} calls will also log
* to the standard Android Logcat facility.
*/
public static void init(final Context context, int maxLogSize, boolean androidLogD) {
if (!sError) logError("Fatal error! Init must be called only once.");
sMaxLogSize = maxLogSize;
sAndroidLogD = androidLogD;
sFile0 = new File(context.getFilesDir(), FILE_0);
sFile1 = new File(context.getFilesDir(), FILE_1);
try {
initFile();
} catch (IOException e) {
logError("Fatal error! Could not open log file.", e);
return;
}
HandlerThread handlerThread = new HandlerThread(Log.class.getName(), android.os.Process.THREAD_PRIORITY_LOWEST);
handlerThread.start();
sHandler = new LogHandler(handlerThread.getLooper());
// Install an exception handler
final UncaughtExceptionHandler previousExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
Log.prepareLogFile(context);
previousExceptionHandler.uncaughtException(thread, ex);
});
sError = false;
}
private static void initFile() throws IOException {
if (!sFile0.exists() || !sFile1.exists()) {
// Use file 0 by default (the first time)
sCurrentFile = sFile0;
} else {
// Keep using the file we used last time (the most recently modified one)
if (sFile0.lastModified() > sFile1.lastModified()) {
android.util.Log.d("Log", "Using log0");
sCurrentFile = sFile0;
} else {
android.util.Log.d("Log", "Using log1");
sCurrentFile = sFile1;
}
}
sWriter = new BufferedWriter(new FileWriter(sCurrentFile, true));
}
private static void log(int messageId, String tag, String message, Throwable throwable) {
if (sError) {
logError("Fatal error! You must call Log.init() prior to calling any logging methods.");
return;
}
Message msg = Message.obtain(sHandler, messageId, message);
Bundle data = msg.getData();
data.putString(KEY_TAG, tag);
data.putString(KEY_MESSAGE, message);
data.putLong(KEY_DATE, System.currentTimeMillis());
data.putString(KEY_THREADID, String.valueOf(Thread.currentThread().getId()));
if (throwable != null) data.putSerializable(KEY_THROWABLE, throwable);
sHandler.sendMessage(msg);
}
/*
* Verbose.
*/
public static void v(String tag, String message) {
v(tag, message, null);
}
public static void v(String tag, String message, Throwable throwable) {
if (sAndroidLogD) {
if (throwable != null) {
android.util.Log.v(tag, message, throwable);
} else {
android.util.Log.v(tag, message);
}
}
log(MSG_V, tag, message, throwable);
}
/*
* Debug.
*/
public static void d(String tag, String message) {
d(tag, message, null);
}
public static void d(String tag, String message, Throwable throwable) {
if (sAndroidLogD) {
if (throwable != null) {
android.util.Log.d(tag, message, throwable);
} else {
android.util.Log.d(tag, message);
}
}
log(MSG_D, tag, message, throwable);
}
/*
* Info.
*/
@SuppressWarnings("unused")
public static void i(String tag, String message) { // NO_UCD (unused code)
i(tag, message, null);
}
private static void i(String tag, String message, Throwable throwable) {
if (throwable != null) {
android.util.Log.i(tag, message, throwable);
} else {
android.util.Log.i(tag, message);
}
log(MSG_I, tag, message, throwable);
}
/*
* Warning.
*/
public static void w(String tag, String message) {
w(tag, message, null);
}
public static void w(String tag, String message, Throwable throwable) {
if (throwable != null) {
android.util.Log.w(tag, message, throwable);
} else {
android.util.Log.w(tag, message);
}
log(MSG_W, tag, message, throwable);
}
/*
* Error.
*/
@SuppressWarnings("unused")
public static void e(String tag, String message) { // NO_UCD (unused code)
e(tag, message, null);
}
public static void e(String tag, String message, Throwable throwable) {
if (throwable != null) {
android.util.Log.e(tag, message, throwable);
} else {
android.util.Log.e(tag, message);
}
log(MSG_E, tag, message, throwable);
}
/*
* Internal errors.
*/
private static void logError(String message) {
logError(message, new Exception(message));
}
private static void logError(String message, Throwable throwable) {
// This will be logged only once
if (!sErrorLogged) {
android.util.Log.e("Log", message, throwable);
sErrorLogged = true;
}
}
/**
* Prepares the log file by retrieving contents from the temporary files.<br/>
* This must not be called from the UI thread since it accesses the disk.
*
* @return true if we were able to prepare the log file, false if some error occurred.
*/
public static boolean prepareLogFile(Context context) {
android.util.Log.d("Log", "Preparing log file...");
BufferedInputStream in0 = null;
BufferedInputStream in1 = null;
BufferedOutputStream out = null;
try {
if (sFile0.exists()) in0 = new BufferedInputStream(new FileInputStream(sFile0));
if (sFile1.exists()) in1 = new BufferedInputStream(new FileInputStream(sFile1));
File outputFile = Share.getExportFile(context, FILE);
if (outputFile == null) return false;
out = new BufferedOutputStream(new FileOutputStream(outputFile, false));
if (sFile0.exists() && sFile1.exists()) {
if (sFile0.lastModified() < sFile1.lastModified()) {
IoUtil.copy(in0, out);
IoUtil.copy(in1, out);
} else {
IoUtil.copy(in1, out);
IoUtil.copy(in0, out);
}
} else if (sFile0.exists()) {
IoUtil.copy(in0, out);
} else if (sFile1.exists()) {
IoUtil.copy(in1, out);
}
out.flush();
} catch (IOException e) {
android.util.Log.e("Log", "Could not prepare log file.", e);
return false;
} finally {
IoUtil.closeSilently(in0, in1, out);
}
android.util.Log.d("Log", "Done.");
return true;
}
final static class LogHandler extends Handler {
LogHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (sCurrentFile.length() >= sMaxLogSize / 2) {
android.util.Log.d("Log", "File is " + sCurrentFile.length() + " bytes: switch");
// Switch files
sCurrentFile = sCurrentFile == sFile0 ? sFile1 : sFile0;
try {
IoUtil.closeSilently(sWriter);
sWriter = new BufferedWriter(new FileWriter(sCurrentFile, false));
} catch (IOException e) {
logError("Fatal error! Could not open log file.", e);
sError = true;
}
}
try {
sWriter.write(String.valueOf(System.currentTimeMillis()));
switch (msg.what) {
case MSG_V:
sWriter.write(" V ");
break;
case MSG_D:
sWriter.write(" D ");
break;
case MSG_I:
sWriter.write(" I ");
break;
case MSG_W:
sWriter.write(" W ");
break;
case MSG_E:
sWriter.write(" E ");
break;
}
Bundle data = msg.getData();
String threadId = data.getString(KEY_THREADID);
String tag = data.getString(KEY_TAG);
String message = data.getString(KEY_MESSAGE);
if (threadId != null) {
sWriter.write(threadId);
sWriter.write(" ");
}
if (tag != null) {
sWriter.write(tag);
sWriter.write(" ");
}
if (message != null) {
sWriter.write(message);
}
sWriter.write('\n');
Throwable throwable = (Throwable) data.getSerializable(KEY_THROWABLE);
if (throwable != null) {
throwable.printStackTrace(new PrintWriter(sWriter));
}
sWriter.flush();
} catch (IOException e) {
logError("Fatal error! Could not write to log file.", e);
sError = true;
}
}
}
}