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 = 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() + } +}