From a35e7871e8e6fc60fe1447d8d2023ffdb1a4de74 Mon Sep 17 00:00:00 2001
From: Jobobby04 <jobobby04@users.noreply.github.com>
Date: Wed, 11 Nov 2020 17:28:09 -0500
Subject: [PATCH] Enhance file logging

---
 app/src/main/java/eu/kanade/tachiyomi/App.kt  |  16 +-
 .../main/java/exh/log/EnhancedFilePrinter.kt  | 408 ++++++++++++++++++
 2 files changed, 421 insertions(+), 3 deletions(-)
 create mode 100644 app/src/main/java/exh/log/EnhancedFilePrinter.kt

diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt
index 2568b719e..197059ff6 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/App.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt
@@ -16,7 +16,6 @@ import com.elvishew.xlog.LogLevel
 import com.elvishew.xlog.XLog
 import com.elvishew.xlog.printer.AndroidPrinter
 import com.elvishew.xlog.printer.Printer
-import com.elvishew.xlog.printer.file.FilePrinter
 import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
 import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
 import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
@@ -36,6 +35,7 @@ import exh.debug.DebugToggles
 import exh.log.CrashlyticsPrinter
 import exh.log.EHDebugModeOverlay
 import exh.log.EHLogLevel
+import exh.log.EnhancedFilePrinter
 import exh.syDebugVersion
 import io.realm.Realm
 import io.realm.RealmConfiguration
@@ -50,6 +50,8 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
 import java.io.File
 import java.security.NoSuchAlgorithmException
 import java.security.Security
+import java.text.SimpleDateFormat
+import java.util.Locale
 import javax.net.ssl.SSLContext
 import kotlin.concurrent.thread
 import kotlin.time.ExperimentalTime
@@ -193,16 +195,24 @@ open class App : Application(), LifecycleObserver {
             "logs"
         )
 
+        val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
+
         @OptIn(ExperimentalTime::class)
-        printers += FilePrinter
+        printers += EnhancedFilePrinter
             .Builder(logFolder.absolutePath)
             .fileNameGenerator(
                 object : DateFileNameGenerator() {
                     override fun generateFileName(logLevel: Int, timestamp: Long): String {
-                        return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}.log"
+                        return super.generateFileName(
+                            logLevel,
+                            timestamp
+                        ) + "-${BuildConfig.BUILD_TYPE}.log"
                     }
                 }
             )
+            .flattener { timeMillis, level, tag, message ->
+                "${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
+            }
             .cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
             .backupStrategy(NeverBackupStrategy())
             .build()
diff --git a/app/src/main/java/exh/log/EnhancedFilePrinter.kt b/app/src/main/java/exh/log/EnhancedFilePrinter.kt
new file mode 100644
index 000000000..f6bbe49e4
--- /dev/null
+++ b/app/src/main/java/exh/log/EnhancedFilePrinter.kt
@@ -0,0 +1,408 @@
+package exh.log
+
+import com.elvishew.xlog.flattener.Flattener2
+import com.elvishew.xlog.internal.DefaultsFactory
+import com.elvishew.xlog.printer.Printer
+import com.elvishew.xlog.printer.file.backup.BackupStrategy
+import com.elvishew.xlog.printer.file.clean.CleanStrategy
+import com.elvishew.xlog.printer.file.naming.FileNameGenerator
+import java.io.BufferedWriter
+import java.io.File
+import java.io.FileWriter
+import java.io.IOException
+import java.util.concurrent.BlockingQueue
+import java.util.concurrent.LinkedBlockingQueue
+
+/**
+ * Log [Printer] using file system. When print a log, it will print it to the specified file.
+ *
+ *
+ * Use the [Builder] to construct a [EnhancedFilePrinter] object.
+ */
+@Suppress("unused")
+class EnhancedFilePrinter internal constructor(builder: Builder) : Printer {
+    /**
+     * The folder path of log file.
+     */
+    private val folderPath: String
+
+    /**
+     * The file name generator for log file.
+     */
+    private val fileNameGenerator: FileNameGenerator
+
+    /**
+     * The backup strategy for log file.
+     */
+    private val backupStrategy: BackupStrategy
+
+    /**
+     * The clean strategy for log file.
+     */
+    private val cleanStrategy: CleanStrategy
+
+    /**
+     * The flattener when print a log.
+     */
+    private val flattener: Flattener2
+
+    /**
+     * Log writer.
+     */
+    private val writer: Writer
+
+    @Volatile
+    private var worker: Worker? = null
+
+    /**
+     * Make sure the folder of log file exists.
+     */
+    private fun checkLogFolder() {
+        val folder = File(folderPath)
+        if (!folder.exists()) {
+            folder.mkdirs()
+        }
+    }
+
+    override fun println(logLevel: Int, tag: String, msg: String) {
+        val timeMillis = System.currentTimeMillis()
+        if (USE_WORKER) {
+            val worker = worker ?: return
+            if (!worker.isStarted()) {
+                worker.start()
+            }
+            worker.enqueue(LogItem(timeMillis, logLevel, tag, msg))
+        } else {
+            doPrintln(timeMillis, logLevel, tag, msg)
+        }
+    }
+
+    /**
+     * Do the real job of writing log to file.
+     */
+    private fun doPrintln(timeMillis: Long, logLevel: Int, tag: String, msg: String) {
+        var lastFileName = writer.lastFileName
+        if (lastFileName == null || fileNameGenerator.isFileNameChangeable) {
+            val newFileName = fileNameGenerator.generateFileName(logLevel, System.currentTimeMillis())
+            require(!(newFileName == null || newFileName.trim { it <= ' ' }.isEmpty())) { "File name should not be empty." }
+            if (newFileName != lastFileName) {
+                if (writer.isOpened) {
+                    writer.close()
+                }
+                cleanLogFilesIfNecessary()
+                if (writer.open(newFileName).not()) {
+                    return
+                }
+                lastFileName = newFileName
+            }
+        }
+        val lastFile = writer.file ?: return
+        if (backupStrategy.shouldBackup(lastFile)) {
+            // Backup the log file, and create a new log file.
+            writer.close()
+            val backupFile = File(folderPath, "$lastFileName.bak")
+            if (backupFile.exists()) {
+                backupFile.delete()
+            }
+            lastFile.renameTo(backupFile)
+            if (writer.open(lastFileName).not()) {
+                return
+            }
+        }
+        val flattenedLog = flattener.flatten(timeMillis, logLevel, tag, msg).toString()
+        writer.appendLog(flattenedLog)
+    }
+
+    /**
+     * Clean log files if should clean follow strategy
+     */
+    private fun cleanLogFilesIfNecessary() {
+        val logDir = File(folderPath)
+        logDir.listFiles().orEmpty()
+            .asSequence()
+            .filter { cleanStrategy.shouldClean(it) }
+            .forEach { it.delete() }
+    }
+
+    /**
+     * Builder for [EnhancedFilePrinter].
+     */
+    class Builder
+    /**
+     * Construct a builder.
+     *
+     * @param folderPath the folder path of log file
+     */(
+        /**
+         * The folder path of log file.
+         */
+        val folderPath: String,
+    ) {
+        /**
+         * The file name generator for log file.
+         */
+        var fileNameGenerator: FileNameGenerator? = null
+
+        /**
+         * The backup strategy for log file.
+         */
+        var backupStrategy: BackupStrategy? = null
+
+        /**
+         * The clean strategy for log file.
+         */
+        var cleanStrategy: CleanStrategy? = null
+
+        /**
+         * The flattener when print a log.
+         */
+        var flattener: Flattener2? = null
+
+        /**
+         * Set the file name generator for log file.
+         *
+         * @param fileNameGenerator the file name generator for log file
+         * @return the builder
+         */
+        fun fileNameGenerator(fileNameGenerator: FileNameGenerator): Builder {
+            this.fileNameGenerator = fileNameGenerator
+            return this
+        }
+
+        /**
+         * Set the backup strategy for log file.
+         *
+         * @param backupStrategy the backup strategy for log file
+         * @return the builder
+         */
+        fun backupStrategy(backupStrategy: BackupStrategy): Builder {
+            this.backupStrategy = backupStrategy
+            return this
+        }
+
+        /**
+         * Set the clean strategy for log file.
+         *
+         * @param cleanStrategy the clean strategy for log file
+         * @return the builder
+         */
+        fun cleanStrategy(cleanStrategy: CleanStrategy): Builder {
+            this.cleanStrategy = cleanStrategy
+            return this
+        }
+
+        /**
+         * Set the flattener when print a log.
+         *
+         * @param flattener the flattener when print a log
+         * @return the builder
+         * @since 1.6.0
+         */
+        fun flattener(flattener: Flattener2): Builder {
+            this.flattener = flattener
+            return this
+        }
+
+        /**
+         * Build configured [EnhancedFilePrinter] object.
+         *
+         * @return the built configured [EnhancedFilePrinter] object
+         */
+        fun build(): EnhancedFilePrinter {
+            fillEmptyFields()
+            return EnhancedFilePrinter(this)
+        }
+
+        private fun fillEmptyFields() {
+            if (fileNameGenerator == null) {
+                fileNameGenerator = DefaultsFactory.createFileNameGenerator()
+            }
+            if (backupStrategy == null) {
+                backupStrategy = DefaultsFactory.createBackupStrategy()
+            }
+            if (cleanStrategy == null) {
+                cleanStrategy = DefaultsFactory.createCleanStrategy()
+            }
+            if (flattener == null) {
+                flattener = DefaultsFactory.createFlattener2()
+            }
+        }
+    }
+
+    private class LogItem(
+        var timeMillis: Long,
+        var level: Int,
+        var tag: String,
+        var msg: String,
+    )
+
+    /**
+     * Work in background, we can enqueue the logs, and the worker will dispatch them.
+     */
+    private inner class Worker : Runnable {
+        private val logs: BlockingQueue<LogItem> = LinkedBlockingQueue()
+
+        @Volatile
+        private var started = false
+
+        /**
+         * Enqueue the log.
+         *
+         * @param log the log to be written to file
+         */
+        fun enqueue(log: LogItem) {
+            try {
+                logs.put(log)
+            } catch (e: InterruptedException) {
+                e.printStackTrace()
+            }
+        }
+
+        /**
+         * Whether the worker is started.
+         *
+         * @return true if started, false otherwise
+         */
+        fun isStarted(): Boolean {
+            synchronized(this) { return started }
+        }
+
+        /**
+         * Start the worker.
+         */
+        fun start() {
+            synchronized(this) {
+                Thread(this).start()
+                started = true
+            }
+        }
+
+        override fun run() {
+            var log: LogItem
+            try {
+                while (logs.take().also { log = it } != null) {
+                    doPrintln(log.timeMillis, log.level, log.tag, log.msg)
+                }
+            } catch (e: InterruptedException) {
+                e.printStackTrace()
+                synchronized(this) { started = false }
+            }
+        }
+    }
+
+    /**
+     * Used to write the flattened logs to the log file.
+     */
+    private inner class Writer {
+        /**
+         * Get the name of last used log file.
+         * @return the name of last used log file, maybe null
+         */
+        /**
+         * The file name of last used log file.
+         */
+        var lastFileName: String? = null
+            private set
+        /**
+         * Get the current log file.
+         *
+         * @return the current log file, maybe null
+         */
+        /**
+         * The current log file.
+         */
+        var file: File? = null
+            private set
+
+        private var bufferedWriter: BufferedWriter? = null
+
+        /**
+         * Whether the log file is opened.
+         *
+         * @return true if opened, false otherwise
+         */
+        val isOpened: Boolean
+            get() = bufferedWriter != null
+
+        /**
+         * Open the file of specific name to be written into.
+         *
+         * @param newFileName the specific file name
+         * @return true if opened successfully, false otherwise
+         */
+        fun open(newFileName: String): Boolean {
+            return try {
+                val file = File(folderPath, newFileName)
+                if (file.exists().not()) {
+                    (file.parentFile ?: File(file.absolutePath.substringBeforeLast(File.separatorChar))).mkdirs()
+                    file.createNewFile()
+                }
+                bufferedWriter = FileWriter(file, true).buffered()
+                lastFileName = newFileName
+                this.file = file
+                true
+            } catch (e: Exception) {
+                e.printStackTrace()
+                false
+            }
+        }
+
+        /**
+         * Close the current log file if it is opened.
+         *
+         * @return true if closed successfully, false otherwise
+         */
+        fun close(): Boolean {
+            if (bufferedWriter != null) {
+                try {
+                    bufferedWriter?.close()
+                } catch (e: IOException) {
+                    e.printStackTrace()
+                    return false
+                } finally {
+                    bufferedWriter = null
+                    lastFileName = null
+                    file = null
+                }
+            }
+            return true
+        }
+
+        /**
+         * Append the flattened log to the end of current opened log file.
+         *
+         * @param flattenedLog the flattened log
+         */
+        fun appendLog(flattenedLog: String) {
+            val bufferedWriter = bufferedWriter
+            require(bufferedWriter != null)
+            try {
+                bufferedWriter.write(flattenedLog)
+                bufferedWriter.newLine()
+                bufferedWriter.flush()
+            } catch (e: IOException) {
+            }
+        }
+    }
+
+    companion object {
+        /**
+         * Use worker, write logs asynchronously.
+         */
+        private const val USE_WORKER = true
+    }
+
+    /*package*/
+    init {
+        folderPath = builder.folderPath
+        fileNameGenerator = builder.fileNameGenerator!!
+        backupStrategy = builder.backupStrategy!!
+        cleanStrategy = builder.cleanStrategy!!
+        flattener = builder.flattener!!
+        writer = Writer()
+        if (USE_WORKER) {
+            worker = Worker()
+        }
+        checkLogFolder()
+    }
+}