content: /*
   * 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 androidx.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);
          }
      }
  }