/*
* This source is part of the
* _____ ___ ____
* __ / / _ \/ _ | / __/___ _______ _
* / // / , _/ __ |/ _/_/ _ \/ __/ _ `/
* \___/_/|_/_/ |_/_/ (_)___/_/ \_, /
* /___/
* repository.
*
* Copyright (C) 2015 Benoit 'BoD' Lubek (BoD@JRAF.org)
*
* 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 org.jraf.android.util.log.timber;
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.text.SimpleDateFormat;
import java.util.Date;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import org.jraf.android.util.io.IoUtil;
public class FileTree extends TagAndMethodNameTree {
private static final String FILE = "log_%s.html";
private static final String FILE_0 = "log0.txt";
private static final String FILE_1 = "log1.txt";
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_THREAD_NAME = "KEY_THREAD_NAME";
private static final String KEY_THROWABLE = "KEY_THROWABLE";
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss''SSS");
private final Context mContext;
private final int mMaxLogSize;
private File mFile;
private final File mFile0;
private final File mFile1;
private Handler mHandler;
private boolean mErrorLogged;
private File mCurrentFile;
private BufferedWriter mWriter;
@SuppressWarnings("HandlerLeak")
public FileTree(Context context, String applicationTag, int maxLogSize) {
super(applicationTag);
mContext = context;
mMaxLogSize = maxLogSize;
mFile0 = new File(context.getFilesDir(), FILE_0);
mFile1 = 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(FileTree.class.getName(), android.os.Process.THREAD_PRIORITY_LOWEST);
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
if (mCurrentFile.length() >= mMaxLogSize / 2) {
android.util.Log.d("Log", "File is " + mCurrentFile.length() + " bytes: switch");
// Switch files
mCurrentFile = mCurrentFile == mFile0 ? mFile1 : mFile0;
try {
IoUtil.closeSilently(mWriter);
mWriter = new BufferedWriter(new FileWriter(mCurrentFile, false));
} catch (IOException e) {
logError("Fatal error! Could not open log file.", e);
}
}
try {
mWriter.write(getCurrentDateTime());
mWriter.write('\t');
switch (msg.what) {
case android.util.Log.VERBOSE:
mWriter.write("V");
break;
case android.util.Log.DEBUG:
mWriter.write("D");
break;
case android.util.Log.INFO:
mWriter.write("I");
break;
case android.util.Log.WARN:
mWriter.write("W");
break;
case android.util.Log.ERROR:
mWriter.write("E");
break;
}
mWriter.write('\t');
Bundle data = msg.getData();
mWriter.write(data.getString(KEY_THREAD_NAME));
mWriter.write('\t');
mWriter.write(data.getString(KEY_TAG));
mWriter.write('\t');
mWriter.write(data.getString(KEY_MESSAGE));
mWriter.write('\n');
Throwable throwable = (Throwable) data.getSerializable(KEY_THROWABLE);
if (throwable != null) {
throwable.printStackTrace(new PrintWriter(mWriter));
mWriter.write('\n');
}
mWriter.flush();
} catch (IOException e) {
logError("Fatal error! Could not write to log file.", e);
}
}
};
// Install an exception handler
final Thread.UncaughtExceptionHandler previousExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {
prepareLogFile();
previousExceptionHandler.uncaughtException(thread, ex);
}
});
}
private String getCurrentDateTime() {
return DATE_FORMAT.format(new Date());
}
private void initFile() throws IOException {
if (!mFile0.exists() || !mFile1.exists()) {
// Use file 0 by default (the first time)
mCurrentFile = mFile0;
} else {
// Keep using the file we used last time (the most recently modified one)
if (mFile0.lastModified() > mFile1.lastModified()) {
android.util.Log.d("Log", "Using log0");
mCurrentFile = mFile0;
} else {
android.util.Log.d("Log", "Using log1");
mCurrentFile = mFile1;
}
}
mWriter = new BufferedWriter(new FileWriter(mCurrentFile, 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 boolean prepareLogFile() {
String fileName = String.format(FILE, new SimpleDateFormat("yyMMddHHmm").format(new Date()));
mFile = new File(mContext.getExternalFilesDir(null), fileName);
android.util.Log.d("Log", "Preparing log file...");
BufferedInputStream in0 = null;
BufferedInputStream in1 = null;
BufferedOutputStream out = null;
try {
if (mFile0.exists()) in0 = new BufferedInputStream(new FileInputStream(mFile0));
if (mFile1.exists()) in1 = new BufferedInputStream(new FileInputStream(mFile1));
out = new BufferedOutputStream(new FileOutputStream(mFile, false));
out.write(getHeader().getBytes("utf-8"));
if (mFile0.exists() && mFile1.exists()) {
if (mFile0.lastModified() < mFile1.lastModified()) {
IoUtil.copy(in0, out);
IoUtil.copy(in1, out);
} else {
IoUtil.copy(in1, out);
IoUtil.copy(in0, out);
}
} else if (mFile0.exists()) {
IoUtil.copy(in0, out);
} else if (mFile1.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;
}
private String getHeader() {
int versionCode;
try {
final PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
versionCode = packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
// Should never happen
throw new AssertionError(e);
}
String res = "<html><body><pre>\n";
res += "===================================================================\n";
res += "Logs collected on: " + getCurrentDateTime() + "\n";
res += "Version code: " + versionCode + "\n";
res += "Android API level: " + Build.VERSION.SDK_INT + "\n";
res += "Device: " + Build.MANUFACTURER + " " + Build.DEVICE + "\n";
res += "===================================================================\n";
return res;
}
public File getFile() {
return mFile;
}
/*
* Internal errors.
*/
private void logError(String message, Throwable throwable) {
// This will be logged only once
if (!mErrorLogged) {
android.util.Log.e("Log", message, throwable);
mErrorLogged = true;
}
}
/*
* TagAndMethodNameTree implementation.
*/
@Override
protected void doLog(int priority, String tag, String methodName, String message, Throwable t) {
Message msg = Message.obtain(mHandler, priority, message);
Bundle data = msg.getData();
data.putString(KEY_TAG, tag);
data.putString(KEY_MESSAGE, methodName + " " + message);
data.putLong(KEY_DATE, System.currentTimeMillis());
data.putString(KEY_THREAD_NAME, String.valueOf(Thread.currentThread().getName()));
if (t != null) data.putSerializable(KEY_THROWABLE, t);
mHandler.sendMessage(msg);
}
}