diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 5b4dd48..3ea549e 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -76,7 +76,7 @@ dependencies { implementation(platform(compose.bom)) implementation(compose.material3) implementation(compose.activity) - implementation(compose.material.extendedicons) + implementation(compose.icons.material.extendedicons) implementation(compose.drawablepainter) // ------------------------ diff --git a/demo/src/main/java/com/michaelflisar/lumberjack/demo/DemoLogging.kt b/demo/src/main/java/com/michaelflisar/lumberjack/demo/DemoLogging.kt index 61a8f3c..9fe52b5 100644 --- a/demo/src/main/java/com/michaelflisar/lumberjack/demo/DemoLogging.kt +++ b/demo/src/main/java/com/michaelflisar/lumberjack/demo/DemoLogging.kt @@ -22,6 +22,7 @@ import timber.log.Timber object DemoLogging { lateinit var FILE_LOGGING_SETUP: IFileLoggingSetup + var FILE_LOGGING_SETUP2: IFileLoggingSetup? = null fun init(context: Context) { @@ -39,11 +40,16 @@ object DemoLogging { Timber.plant(ConsoleTree()) Timber.plant(FileLoggingTree(setup)) } else { - val setup = FileLoggerSetup.Daily(context).also { + val setup = FileLoggerSetup.Daily.create(context, fileBaseName = "log_daily").also { FILE_LOGGING_SETUP = it } L.plant(ConsoleLogger()) L.plant(FileLogger(setup)) + + val setup2 = FileLoggerSetup.FileSize.create(context, maxFileSizeInBytes = 1000 * 10 /* 10 kB */, fileBaseName = "log_size", filesToKeep = 2).also { + FILE_LOGGING_SETUP2 = it + } + L.plant(FileLogger(setup2)) } // EXAMPLE on how you could use lumberjack inside a library with the minimal dependency on the core module diff --git a/demo/src/main/java/com/michaelflisar/lumberjack/demo/ui/MainActivity.kt b/demo/src/main/java/com/michaelflisar/lumberjack/demo/ui/MainActivity.kt index cacb293..4f09e35 100644 --- a/demo/src/main/java/com/michaelflisar/lumberjack/demo/ui/MainActivity.kt +++ b/demo/src/main/java/com/michaelflisar/lumberjack/demo/ui/MainActivity.kt @@ -31,6 +31,7 @@ import com.michaelflisar.lumberjack.demo.DemoLogging import com.michaelflisar.lumberjack.extensions.composeviewer.LumberjackDialog import com.michaelflisar.lumberjack.extensions.feedback.sendFeedback import com.michaelflisar.lumberjack.extensions.viewer.showLog +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class MainActivity : DemoBaseActivity() { @@ -50,9 +51,8 @@ class MainActivity : DemoBaseActivity() { var mail by rememberSaveable { mutableStateOf("") } - val showComposeLogView = rememberSaveable { - mutableStateOf(false) - } + val showComposeLogView = rememberSaveable { mutableStateOf(false) } + val showComposeLogView2 = rememberSaveable { mutableStateOf(false) } Column( modifier = modifier @@ -85,6 +85,15 @@ class MainActivity : DemoBaseActivity() { }) { Text("Log Viewer (Compose)") } + if (DemoLogging.FILE_LOGGING_SETUP2 != null) { + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + onClick = { + showComposeLogView2.value = true + }) { + Text("Log Viewer (Compose) - Setup2") + } + } } DemoCollapsibleRegion( title = "Actions", @@ -131,6 +140,17 @@ class MainActivity : DemoBaseActivity() { }) { Text("Log something") } + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + onClick = { + scope.launch(Dispatchers.IO) { + (1..100).forEach { + L.d { "Logging a lot $it..." } + } + } + }) { + Text("Log a lot") + } } } @@ -141,5 +161,15 @@ class MainActivity : DemoBaseActivity() { darkTheme = theme.isDark(), //mail = "...@gmail.com" ) + + if (DemoLogging.FILE_LOGGING_SETUP2 != null) { + LumberjackDialog( + visible = showComposeLogView2, + title = "Logs", + setup = DemoLogging.FILE_LOGGING_SETUP2!!, + darkTheme = theme.isDark(), + //mail = "...@gmail.com" + ) + } } } \ No newline at end of file diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index c788d81..e8b266c 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,12 +1,10 @@ [versions] core = "1.12.0" -lifecycle = "2.6.2" -constraintlayout = "2.1.4" +lifecycle = "2.7.0" recyclerview = "1.3.2" [libraries] core = { module = "androidx.core:core-ktx", version.ref = "core" } lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } -constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } \ No newline at end of file diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 285a1a2..a19d37c 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,25 +1,15 @@ [versions] -composeBom = "2023.10.01" -compiler = "1.5.3" +composeBom = "2024.03.00" +compiler = "1.5.10" -lifecycle = "2.6.2" -activity = "1.8.0" +activity = "1.8.2" accompanist = "0.32.0" [libraries] -bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -animation = { group = "androidx.compose.animation", name = "animation" } -foundation = { group = "androidx.compose.foundation", name = "foundation" } -material = { group = "androidx.compose.material", name = "material" } -runtime = { group = "androidx.compose.runtime", name = "runtime" } -ui = { group = "androidx.compose.ui", name = "ui" } -ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -material-extendedicons = { group = "androidx.compose.material", name = "material-icons-extended" } -material3 = { group = "androidx.compose.material3", name = "material3" } +bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +icons-material-extendedicons = { group = "androidx.compose.material", name = "material-icons-extended" } +material3 = { group = "androidx.compose.material3", name = "material3" } -lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } -activity = { module = "androidx.activity:activity-compose", version.ref = "activity" } +activity = { module = "androidx.activity:activity-compose", version.ref = "activity" } -systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } -drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" } \ No newline at end of file +drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" } \ No newline at end of file diff --git a/gradle/dependencies.versions.toml b/gradle/dependencies.versions.toml index 57efc68..1688f61 100644 --- a/gradle/dependencies.versions.toml +++ b/gradle/dependencies.versions.toml @@ -7,10 +7,10 @@ fastscroller = "1.0.0" # mflisar feedback = "2.0.4" -composedemobaseactivity = "0.4" +composedemobaseactivity = "0.6" # google -material = "1.10.0" +material = "1.11.0" # --------- # libraries diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6b4cb3f..4abaa8d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip diff --git a/library/extensions/composeviewer/build.gradle.kts b/library/extensions/composeviewer/build.gradle.kts index fda00a7..dd3580b 100644 --- a/library/extensions/composeviewer/build.gradle.kts +++ b/library/extensions/composeviewer/build.gradle.kts @@ -50,7 +50,7 @@ dependencies { implementation(platform(compose.bom)) implementation(compose.material3) implementation(compose.activity) - implementation(compose.material.extendedicons) + implementation(compose.icons.material.extendedicons) implementation(compose.drawablepainter) // ------------------------ diff --git a/library/extensions/composeviewer/src/main/java/com/michaelflisar/lumberjack/extensions/composeviewer/LumberjackView.kt b/library/extensions/composeviewer/src/main/java/com/michaelflisar/lumberjack/extensions/composeviewer/LumberjackView.kt index 2755151..c7f68f0 100644 --- a/library/extensions/composeviewer/src/main/java/com/michaelflisar/lumberjack/extensions/composeviewer/LumberjackView.kt +++ b/library/extensions/composeviewer/src/main/java/com/michaelflisar/lumberjack/extensions/composeviewer/LumberjackView.kt @@ -32,6 +32,7 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -190,7 +191,7 @@ fun LumberjackDialog( } ) if (mail != null) { - Divider() + HorizontalDivider() DropdownMenuItem( text = { Text("Send Mail") }, leadingIcon = { @@ -463,10 +464,11 @@ private fun Info(file: File?, filteredCount: Int, totalCount: Int) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) { + val info = "%.2fkB".format((file?.length()?.toDouble() ?: 0.0) / 1000.0) Text( modifier = Modifier.weight(1f), maxLines = 1, - text = file?.name ?: "", + text = file?.let { "${it.name} ($info)" } ?: "", style = MaterialTheme.typography.bodySmall ) Text( diff --git a/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/BaseFileLoggerSetup.kt b/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/BaseFileLoggerSetup.kt new file mode 100644 index 0000000..3bb8f5f --- /dev/null +++ b/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/BaseFileLoggerSetup.kt @@ -0,0 +1,82 @@ +package com.michaelflisar.lumberjack.loggers.file + +import com.michaelflisar.lumberjack.implementation.LumberjackLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.parcelize.IgnoredOnParcel +import java.io.File + +abstract class BaseFileLoggerSetup : FileLoggerSetup() { + + abstract val folder: File + abstract val fileBaseName: String + abstract val fileExtension: String + abstract var lastFileKey: String + abstract var lastFileKeyChanged: Boolean + + @IgnoredOnParcel + override val fileConverter = FileConverter + + override fun filePath(data: FileLogger.Event.Data): String { + val lastPath = "${folder.path}/${fileBaseName}_${lastFileKey}.$fileExtension" + val key = getFileKey(data, lastPath) + val path = "${folder.path}/${fileBaseName}_${key}.$fileExtension" + if (key != lastFileKey) { + lastFileKey = key + lastFileKeyChanged = true + } + return path + } + + abstract fun getFileKey(data: FileLogger.Event.Data, lastPath: String): String + abstract fun filterLogFilesToDelete(files: List): List + + protected fun getKeyFromFile(file: File) : String { + return file.nameWithoutExtension.replace(fileBaseName, "").substring(1) + } + + override fun onLogged(scope: CoroutineScope) { + if (lastFileKeyChanged) { + lastFileKeyChanged = false + scope.launch { + clearLogFiles(false) + } + } + } + + override suspend fun clearLogFiles() { + clearLogFiles(true) + } + + override fun getAllExistingLogFiles(): List { + return folder.listFiles()?.filter { + it.name.startsWith(fileBaseName) + }?.sortedByDescending { it.name } ?: emptyList() + } + + override fun getLatestLogFiles(): File? { + return getAllExistingLogFiles().firstOrNull() + } + + private suspend fun clearLogFiles(all: Boolean) { + withContext(Dispatchers.IO) { + val files = getAllExistingLogFiles() + val filesToDelete = if (all) files else filterLogFilesToDelete(files) + if (filesToDelete.isNotEmpty()) { + LumberjackLogger.loggers() + .filterIsInstance() + .filter { + it.setup == this@BaseFileLoggerSetup + } + .forEach { + it.onLogFilesWillBeDeleted(filesToDelete) + } + filesToDelete.forEach { + it.delete() + } + } + } + } +} \ No newline at end of file diff --git a/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/FileLogger.kt b/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/FileLogger.kt index 453cbaa..d10e168 100644 --- a/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/FileLogger.kt +++ b/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/FileLogger.kt @@ -90,7 +90,7 @@ class FileLogger( withContext(Dispatchers.IO) { // try/catch - in no circumstance we want that any problem crashes the app because of the logger try { - val path = setup.filePath(data.time) + val path = setup.filePath(data) if (path != file?.path || bufferWriter == null) { bufferWriter?.close() file = File(path) diff --git a/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/FileLoggerSetup.kt b/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/FileLoggerSetup.kt index 1ac19bb..7fdc302 100644 --- a/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/FileLoggerSetup.kt +++ b/library/loggers/lumberjack/file/src/main/java/com/michaelflisar/lumberjack/loggers/file/FileLoggerSetup.kt @@ -1,121 +1,130 @@ package com.michaelflisar.lumberjack.loggers.file +import android.annotation.SuppressLint import android.content.Context import com.michaelflisar.lumberjack.core.interfaces.IFileLoggingSetup -import com.michaelflisar.lumberjack.implementation.LumberjackLogger import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import java.io.File import java.text.SimpleDateFormat import java.util.Date +import kotlin.math.min sealed class FileLoggerSetup : IFileLoggingSetup { - abstract fun filePath(time: Long): String + abstract fun filePath(data: FileLogger.Event.Data): String abstract fun onLogged(scope: CoroutineScope) @Parcelize - data class Daily private constructor( - val folder: File, - val fileBaseName: String = "log", - val fileExtension: String = "log", - val filesToKeep: Int = 1, - var lastFileNameKey: String = "", - var keyChanged: Boolean = false - ) : FileLoggerSetup() { - - constructor( - context: Context, - fileBaseName: String = "log", - fileExtension: String = "log", - filesToKeep: Int = 1 - ) : this( - File(context.filesDir, "lumberjack"), - fileBaseName, - fileExtension, - filesToKeep, - "", - false - ) - - constructor( - folder: File, - fileBaseName: String = "log", - fileExtension: String = "log", - filesToKeep: Int = 1 - ) : this(folder, fileBaseName, fileExtension, filesToKeep, "", false) + class Daily private constructor( + override val folder: File, + override val fileBaseName: String, + override val fileExtension: String, + private val filesToKeep: Int, + override var lastFileKey: String = "", + override var lastFileKeyChanged: Boolean = false + ) : BaseFileLoggerSetup() { - @IgnoredOnParcel - override val fileConverter = FileConverter + companion object { + fun create( + context: Context, + fileBaseName: String = "log", + fileExtension: String = "log", + filesToKeep: Int = 1 + ) = Daily( + File(context.filesDir, "lumberjack"), + fileBaseName, + fileExtension, + filesToKeep + ) + + fun create( + folder: File, + fileBaseName: String = "log", + fileExtension: String = "log", + filesToKeep: Int = 1 + ) = Daily(folder, fileBaseName, fileExtension, filesToKeep) + } + @SuppressLint("SimpleDateFormat") @IgnoredOnParcel private val timeFormatter = SimpleDateFormat("yyyy_MM_dd") @IgnoredOnParcel private val date = Date() - override fun filePath(time: Long): String { - date.time = time + override fun getFileKey(data: FileLogger.Event.Data, lastPath: String): String { + date.time = data.time val key = timeFormatter.format(date) - val path = "${folder.path}/${fileBaseName}_${key}.$fileExtension" - if (key != lastFileNameKey) { - lastFileNameKey = key - keyChanged = true - } - return path + return key } - override fun onLogged(scope: CoroutineScope) { - if (keyChanged) { - keyChanged = false - scope.launch { - clearLogFiles(false) - } - } + override fun filterLogFilesToDelete(files: List): List { + return files.dropLast((files.size - filesToKeep).coerceAtLeast(0)) } + } - override suspend fun clearLogFiles() { - clearLogFiles(true) - } + @Parcelize + class FileSize private constructor( + override val folder: File, + override val fileBaseName: String, + override val fileExtension: String, + private val filesToKeep: Int, + private val maxFileSizeInBytes: Int, + override var lastFileKey: String = "", + override var lastFileKeyChanged: Boolean = false + ) : BaseFileLoggerSetup() { - override fun getAllExistingLogFiles(): List { - return folder.listFiles()?.filter { - it.name.startsWith(fileBaseName) - }?.sortedByDescending { it.name } ?: emptyList() - } + companion object { - override fun getLatestLogFiles(): File? { - return getAllExistingLogFiles().firstOrNull() + const val DEFAULT_SIZE = 5 * 1000 * 1000 // 5MB + + fun create( + context: Context, + fileBaseName: String = "log", + fileExtension: String = "log", + filesToKeep: Int = 1, + maxFileSizeInBytes: Int = DEFAULT_SIZE + ) = FileSize( + File(context.filesDir, "lumberjack"), + fileBaseName, + fileExtension, + filesToKeep, + maxFileSizeInBytes + ) + + fun create( + folder: File, + fileBaseName: String = "log", + fileExtension: String = "log", + filesToKeep: Int = 1, + maxFileSizeInBytes: Int = DEFAULT_SIZE + ) = FileSize(folder, fileBaseName, fileExtension, filesToKeep, maxFileSizeInBytes) } - private suspend fun clearLogFiles(all: Boolean) { - withContext(Dispatchers.IO) { - val files = getAllExistingLogFiles() - val filesToDelete = files.drop(if (all) 0 else filesToKeep) - LumberjackLogger.loggers() - .filterIsInstance() - .filter { - it.setup == this@Daily - } - .forEach { - it.onLogFilesWillBeDeleted(filesToDelete) - } - filesToDelete.forEach { - it.delete() - } + @IgnoredOnParcel + private var fileIndex: Int? = null + + override fun getFileKey(data: FileLogger.Event.Data, lastPath: String): String { + if (fileIndex == null) { + // we must find out what the highest existing log file index currently is + fileIndex = + getAllExistingLogFiles().lastOrNull()?.let { getKeyFromFile(it).toIntOrNull() } + ?: 1 } + if (lastPath.isEmpty()) { + return fileIndex.toString() + } + val bytes = File(lastPath).takeIf { it.exists() }?.length() + if ((bytes ?: 0L) >= maxFileSizeInBytes) { + fileIndex = fileIndex!! + 1 + } + return fileIndex.toString() + } + + override fun filterLogFilesToDelete(files: List): List { + return files.drop(filesToKeep) } } - /* - class FileSize( - val fileName: String, - val folder: File, - val maxFileSizeInBytes: Int = 5 * 1000 * 1000 // 5MB - ) : FileLoggerSetup() { - override fun provideFile() = File(folder, fileName) - }*/ } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2cc4f7f..fa0185a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,10 +7,10 @@ dependencyResolutionManagement { } versionCatalogs { - val kotlin = "1.9.10" - val ksp = "1.9.10-1.0.13" + val kotlin = "1.9.22" + val ksp = "1.9.10-1.0.17" val coroutines = "1.7.3" - val gradle = "8.1.2" + val gradle = "8.3.1" // TOML Files create("androidx") {