| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- /* _____ _
- * |_ _| |_ _ _ ___ ___ _ __ __ _
- * | | | ' \| '_/ -_) -_) ' \/ _` |_
- * |_| |_||_|_| \___\___|_|_|_\__,_(_)
- *
- * Threema for Android
- * Copyright (c) 2018-2022 Threema GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
- package ch.threema.logging.backend;
- import android.os.Handler;
- import android.os.HandlerThread;
- import android.os.Looper;
- import android.util.Log;
- import net.lingala.zip4j.io.outputstream.ZipOutputStream;
- import net.lingala.zip4j.model.ZipParameters;
- import net.lingala.zip4j.model.enums.CompressionLevel;
- import net.lingala.zip4j.model.enums.CompressionMethod;
- import org.slf4j.helpers.MessageFormatter;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.FileWriter;
- import java.io.PrintWriter;
- import java.util.Date;
- import androidx.annotation.NonNull;
- import androidx.annotation.Nullable;
- import ch.threema.app.ThreemaApplication;
- import ch.threema.app.services.FileService;
- import ch.threema.app.utils.ZipUtil;
- import ch.threema.app.utils.executor.HandlerExecutor;
- import ch.threema.logging.LogLevel;
- import ch.threema.base.utils.LoggingUtil;
- import java8.util.concurrent.CompletableFuture;
- /**
- * A logging backend that logs to the debug log file.
- *
- * This backend is only enabled if the user enabled the debug log.
- *
- * The log file is deleted when calling `setEnabled(false)`.
- *
- * A zipped log file can be requested with `getZipFile()`.
- */
- public class DebugLogFileBackend implements LogBackend {
- // Constants
- private static final String TAG = "3ma";
- private static final String LOGFILE_NAME = "debug_log.txt";
- // Static variables
- private static boolean enabled = false;
- private static File logFile = null;
- private final @LogLevel int minLogLevel;
- // For tags starting with these prefixes, the package path is stripped
- private final static String[] STRIP_PREFIXES = {
- "ch.threema.app.",
- "ch.threema.domain.",
- "ch.threema.storage.",
- "ch.threema.",
- };
- // Worker thread
- private static @Nullable HandlerExecutor handler;
- /**
- * Create and start worker thread.
- */
- private static synchronized HandlerExecutor createHandler() {
- final HandlerThread handlerThread = new HandlerThread("DebugLogWorker");
- handlerThread.start();
- final Looper looper = handlerThread.getLooper();
- final Handler parent;
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
- parent = Handler.createAsync(looper);
- } else {
- parent = new Handler(looper);
- }
- return new HandlerExecutor(parent);
- }
- /**
- * Return the handler for the worker thread. Start it first if necessary.
- */
- private static synchronized @NonNull HandlerExecutor getHandler() {
- if (handler == null) {
- handler = createHandler();
- }
- return handler;
- }
- public DebugLogFileBackend(@LogLevel int minLogLevel) {
- this.minLogLevel = minLogLevel;
- }
- @Override
- public boolean isEnabled(int level) {
- return enabled && level >= this.minLogLevel;
- }
- /**
- * Enable or disable logging to the debug log file.
- *
- * By default, it is disabled.
- *
- * When disabling the logging, then the file is deleted if it already exists.
- */
- public synchronized static void setEnabled(boolean enabled) {
- DebugLogFileBackend.enabled = enabled;
- if (!enabled) {
- final File file = getLogFile();
- if (file == null) {
- Log.e(TAG,"DebugLogFileBackend: Could not get debug log file path");
- return;
- }
- if (file.exists() && !file.delete()) {
- Log.e(TAG,"DebugLogFileBackend: Could not delete debug log file");
- }
- }
- }
- /**
- * Return whether debug log file logging is enabled.
- */
- public static boolean isEnabled() {
- return enabled;
- }
- /**
- * @hidden Used only for tests.
- */
- static File getLogFilePath() {
- final File threemaDir = new File(ThreemaApplication.getAppContext().getExternalFilesDir(null), "log");
- return new File(threemaDir, LOGFILE_NAME);
- }
- /**
- * Return a `File` instance pointing to the debug log file.
- *
- * Returns `null` if the log file directory could not be created.
- */
- @Nullable
- private static File getLogFile() {
- if (logFile == null || !logFile.exists()) {
- final File threemaDir = new File(ThreemaApplication.getAppContext().getExternalFilesDir(null), "log");
- if (!threemaDir.exists()) {
- if (!threemaDir.mkdirs()) {
- Log.e(TAG, "DebugLogFileBackend: Could not create threema directory");
- return null;
- }
- }
- logFile = new File(threemaDir, LOGFILE_NAME);
- }
- return logFile;
- }
- /**
- * If the logger is enabled, write the log asynchronously to the log file.
- *
- * I/O is dispatched to the handler thread.
- *
- * A CompletableFuture is returned, which resolves once processing is finished.
- * The returned value is TRUE if the log was written successfully,
- * FALSE if writing the log failed, and null if the logger was not enabled.
- *
- * @see #print(int, String, Throwable, String)
- */
- synchronized CompletableFuture<Boolean> printAsync(
- @LogLevel int level,
- @NonNull String tag,
- @Nullable Throwable throwable,
- @Nullable String message
- ) {
- final CompletableFuture<Boolean> future = new CompletableFuture<>();
- if (!this.isEnabled(level)) {
- future.complete(null);
- return future;
- }
- // Dispatch I/O to worker thread.
- getHandler().post(() -> {
- // Get log file
- final File logFile = getLogFile();
- if (logFile == null) {
- Log.w(TAG, "DebugLogFileBackend: Could not get log file path");
- future.complete(false);
- return;
- }
- // Get log level string
- String levelString;
- switch (level) {
- case Log.VERBOSE:
- levelString = "TRACE";
- break;
- case Log.DEBUG:
- levelString = "DEBUG";
- break;
- case Log.INFO:
- levelString = "INFO ";
- break;
- case Log.WARN:
- levelString = "WARN ";
- break;
- case Log.ERROR:
- levelString = "ERROR";
- break;
- default:
- levelString = "? ";
- }
- // Prepare log text
- final Date now = new Date();
- String logLine = now.toString()
- + '\t' + levelString
- + " " + LoggingUtil.cleanTag(tag, STRIP_PREFIXES) + ": ";
- if (message == null) {
- if (throwable != null) {
- logLine += Log.getStackTraceString(throwable);
- }
- } else {
- if (throwable == null) {
- logLine += message;
- } else {
- logLine += message + '\n' + Log.getStackTraceString(throwable);
- }
- }
- // Write to logfile
- try (
- final FileWriter fw = new FileWriter(logFile, true);
- final PrintWriter pw = new PrintWriter(fw)
- ) {
- pw.println(logLine);
- future.complete(true);
- } catch (Exception e) {
- // Write failed...
- future.complete(false);
- }
- });
- return future;
- }
- /**
- * If the logger is enabled, write the log to the log file.
- *
- * Note: I/O is done asynchronously, so the log may not yet be fully written
- * to storage when this method returns!
- *
- * @param level The log level
- * @param tag The log tag
- * @param throwable A throwable (may be null)
- * @param message A message (may be null)
- */
- @Override
- public synchronized void print(
- @LogLevel int level,
- @NonNull String tag,
- @Nullable Throwable throwable,
- @Nullable String message
- ) {
- this.printAsync(level, tag, throwable, message);
- }
- @Override
- public synchronized void print(@LogLevel int level, @NonNull String tag, @Nullable Throwable throwable, @NonNull String messageFormat, Object... args) {
- if (!this.isEnabled(level)) {
- return;
- }
- try {
- this.print(level, tag, throwable, MessageFormatter.arrayFormat(messageFormat, args).getMessage());
- } catch (Exception e) { // Never crash
- this.print(level, tag, throwable, messageFormat);
- }
- }
- @Nullable
- public static File getZipFile(FileService fileService) {
- // Open log file
- final File logFile = getLogFile();
- if (logFile == null) {
- Log.w(TAG, "DebugLogFileBackend: getLogFile returned null");
- return null;
- }
- // Delete old debug log archive
- final File tempDebugLogArchive = new File(fileService.getExtTmpPath(), "debug_log.zip");
- if (tempDebugLogArchive.exists() && !tempDebugLogArchive.delete()) {
- Log.w(TAG, "DebugLogFileBackend: Could not delete tempDebugLogArchive");
- }
- // Create and return ZIP
- try (
- final FileInputStream inputStream = new FileInputStream(logFile);
- final ZipOutputStream zipOutputStream = ZipUtil.initializeZipOutputStream(tempDebugLogArchive, null)
- ) {
- final ZipParameters parameters = createZipParameters(logFile.getName());
- zipOutputStream.putNextEntry(parameters);
- final byte[] buf = new byte[16384];
- int nread;
- while ((nread = inputStream.read(buf)) > 0) {
- zipOutputStream.write(buf, 0, nread);
- }
- zipOutputStream.closeEntry();
- return tempDebugLogArchive;
- } catch (Exception e) {
- return null;
- }
- }
- private static ZipParameters createZipParameters(String filenameInZip) {
- ZipParameters parameters = new ZipParameters();
- parameters.setCompressionMethod(CompressionMethod.DEFLATE);
- parameters.setCompressionLevel(CompressionLevel.NORMAL);
- parameters.setFileNameInZip(filenameInZip);
- return parameters;
- }
- }
|