/*
* Copyright (C) 2015 AChep@xda <artemchep@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.achep.base.timber;
import android.Manifest;
import android.content.Context;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.util.Log;
import com.achep.base.AppHeap;
import com.achep.base.Build;
import com.achep.base.permissions.RuntimePermissions;
import com.achep.base.tests.Check;
import com.achep.base.utils.EncryptionUtils;
import com.achep.base.utils.FileUtils;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import timber.log.Timber;
import static com.achep.base.Build.ENCRYPT_LOGS;
/**
* Async-ly writes all logs to a file.
*
* @author Artem Chepurnoy
*/
public class WritingTree extends Timber.DebugTree {
public static final String FILENAME = "acdisplay_logs.txt";
@NonNull
private final SimpleDateFormat mFormat = new SimpleDateFormat("dd HH:mm:ss", Locale.ENGLISH);
private T mThread;
/**
* @author Artem Chepurnoy
*/
private static class T extends Thread {
private static final String TAG = "WritingTree.Thread";
@SuppressWarnings("PointlessBooleanExpression")
private final boolean DEBUG = false && Build.DEBUG;
/**
* How long should the thread sleep after getting a pending
* log line.
*/
private static final long SLEEP = 10000; // 10s
/**
* Max number of symbols in a log buffer.
*/
private static final int MAX_LENGTH = 30000; // 30k symbols
private final Object mMonitor = new Object();
private final StringBuilder mBuilder = new StringBuilder();
private final File mFile;
private volatile boolean mLocked = false;
private volatile boolean mRunning = true;
private volatile boolean mSleepy;
public T() {
mFile = new File(Environment.getExternalStorageDirectory() + "/" + FILENAME);
if (!FileUtils.deleteRecursive(mFile)) Log.w(TAG, "Failed to remove the ");
setPriority(Thread.MIN_PRIORITY);
}
@Override
public void run() {
while (mRunning) {
synchronized (mMonitor) {
final int length = mBuilder.length();
if (length > 0) {
//noinspection ConstantConditions
if (DEBUG) {
Log.d(TAG, "Writing " + length + "-symbols log to a file.");
}
final Context context = AppHeap.getContext();
final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
if (RuntimePermissions.has(context, permission)) {
//noinspection ConstantConditions
final CharSequence log;
if (ENCRYPT_LOGS) {
log = EncryptionUtils.x(mBuilder.toString(), Build.LOG_KEY_SALT) + "\n";
if (DEBUG) {
// Check if we can decrypt it
String encrypted = log.toString().substring(0, log.length() - 2);
Check.getInstance().isTrue(EncryptionUtils
.fromX(encrypted, Build.LOG_KEY_SALT)
.equals(mBuilder.toString()));
}
} else log = mBuilder;
final boolean succeed = FileUtils.writeToFileAppend(mFile, log);
if (succeed) mBuilder.delete(0, length - 1);
} else if (RuntimePermissions.allowed(context, permission)) {
RuntimePermissions.ask(context, permission);
} else {
// We can not archive it, so lets just fall back
// sit and cry.
mBuilder.delete(0, length - 1);
}
}
try {
mLocked = true;
mMonitor.wait();
} catch (InterruptedException e) { /* unused */ } finally {
mLocked = false;
}
}
if (mSleepy) try {
Thread.sleep(SLEEP);
} catch (InterruptedException e) { /* unused */ }
mSleepy = true;
}
}
public void requestWrite(int priority, @NonNull CharSequence cs) {
synchronized (mMonitor) {
mBuilder.append(cs);
// Cut the log
final int length = mBuilder.length();
if (length > MAX_LENGTH) {
Log.w(TAG, "Cutting down the log: length=" + length);
mBuilder.delete(0, length - MAX_LENGTH);
}
// Write the important logs immediately
mSleepy &= priority < Log.WARN;
if (mLocked) mMonitor.notifyAll();
}
}
}
public WritingTree() {
super();
}
@Override
protected void log(int priority, String tag, String message, Throwable t) {
synchronized (this) {
Log.println(priority, tag, message);
// Append a log to the pool.
if (mThread == null) {
mThread = new T();
mThread.start();
}
String log = mFormat.format(Calendar.getInstance().getTime())
+ "/" + priority
+ "/" + tag
+ ": " + message + "\n";
mThread.requestWrite(priority, log);
}
}
}