diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 25cd960782..813bff1822 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -384,7 +384,6 @@ dependencies { implementation(deps.googlePlay.services) implementation(deps.googlePlay.advertising) coreLibraryDesugaring(deps.desugar) - implementation(deps.kermit) implementation(deps.zxing.qrCore) implementation(deps.coil) implementation(deps.coil.gif) @@ -405,7 +404,6 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.reownCore) implementation(deps.reownWeb3) - implementation(deps.prettyLogger) implementation(deps.decompose.ext.compose) implementation(deps.moshi.adapters) implementation(deps.moshi.kotlin) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 61c9e07c43..d66468de37 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -164,7 +164,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. appScope.launch { launch(Dispatchers.IO) { loadNativeLibraries() - updateLogFiles() } } @@ -196,22 +195,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. ) } - private fun updateLogFiles() { - appLogsStore.deleteOldLogsFile() - - if (!BuildConfig.TESTER_MENU_ENABLED) { - appLogsStore.deleteLastLogFile() - } - - // Temporarily logs are not saved - // scope.launch { - // if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) { - // appLogsStore.deleteLastLogFile() - // appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true) - // } - // } - } - override fun newImageLoader(): ImageLoader { return createCoilImageLoader( context = this, diff --git a/app/src/main/java/com/tangem/tap/common/log/FileLogWriter.kt b/app/src/main/java/com/tangem/tap/common/log/FileLogWriter.kt new file mode 100644 index 0000000000..600f325928 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/log/FileLogWriter.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.common.log + +import com.tangem.datasource.local.logs.AppLogsStore +import com.tangem.utils.logging.Severity +import com.tangem.utils.logging.TangemLogger + +/** + * [TangemLogger.LogWriter] that persists log entries to [AppLogsStore]. + * + * Only [Severity.Error] and [Severity.Info] are written. The `shouldSanitize` flag is forwarded to + * [AppLogsStore.saveLogMessage], so callers that deliberately log unsanitized content + * (`shouldSanitize = false`) bypass the sanitizer. + */ +internal class FileLogWriter( + private val appLogsStore: AppLogsStore, +) : TangemLogger.LogWriter { + + override fun isLoggable(severity: Severity, tag: String): Boolean { + return severity == Severity.Error || severity == Severity.Info + } + + override fun write( + severity: Severity, + tag: String, + message: String, + throwable: Throwable?, + shouldSanitize: Boolean, + ) { + appLogsStore.saveLogMessage( + tag = tag, + message = message, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/log/LogcatLogWriter.kt b/app/src/main/java/com/tangem/tap/common/log/LogcatLogWriter.kt new file mode 100644 index 0000000000..76ac68af8b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/log/LogcatLogWriter.kt @@ -0,0 +1,86 @@ +package com.tangem.tap.common.log + +import android.os.Build +import android.util.Log +import com.tangem.utils.logging.Severity +import com.tangem.utils.logging.TangemLogger + +/** + * [TangemLogger.LogWriter] that pretty-prints log entries to Logcat. + * + * Wraps each entry in unicode borders and chunks long messages so that they fit + * Android's per-entry byte limit (~4076 bytes). + */ +internal class LogcatLogWriter : TangemLogger.LogWriter { + + override fun write( + severity: Severity, + tag: String, + message: String, + throwable: Throwable?, + shouldSanitize: Boolean, + ) { + val priority = severity.toAndroidPriority() + val truncatedTag = tag.truncateForLogcat() + val finalMessage = if (throwable != null) { + "$message\n${Log.getStackTraceString(throwable)}" + } else { + message + } + printBoxed(priority, truncatedTag, finalMessage) + } + + private fun printBoxed(priority: Int, tag: String, message: String) { + Log.println(priority, tag, TOP_BORDER) + val bytes = message.toByteArray() + val length = bytes.size + if (length <= CHUNK_SIZE) { + printContent(priority, tag, message) + } else { + var i = 0 + while (i < length) { + val count = (length - i).coerceAtMost(CHUNK_SIZE) + printContent(priority, tag, String(bytes, i, count)) + i += CHUNK_SIZE + } + } + Log.println(priority, tag, BOTTOM_BORDER) + } + + private fun printContent(priority: Int, tag: String, chunk: String) { + chunk.split(System.lineSeparator()).forEach { line -> + Log.println(priority, tag, "$HORIZONTAL_LINE $line") + } + } + + @Suppress("MagicNumber") + private fun String.truncateForLogcat(): String { + // Tag length limit was removed in API 26. + return if (length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) { + this + } else { + substring(0, MAX_TAG_LENGTH) + } + } + + private fun Severity.toAndroidPriority(): Int = when (this) { + Severity.Verbose -> Log.VERBOSE + Severity.Debug -> Log.DEBUG + Severity.Info -> Log.INFO + Severity.Warn -> Log.WARN + Severity.Error -> Log.ERROR + Severity.Assert -> Log.ASSERT + } + + private companion object { + // Android's max per-entry byte limit is ~4076; leave headroom for borders. + const val CHUNK_SIZE = 4000 + + const val MAX_TAG_LENGTH = 23 + + const val HORIZONTAL_LINE = "│" + const val DIVIDER = "────────────────────────────────────────────────────────" + const val TOP_BORDER = "┌$DIVIDER$DIVIDER" + const val BOTTOM_BORDER = "└$DIVIDER$DIVIDER" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt index 0231c5fb23..b71a74125c 100644 --- a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt +++ b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt @@ -1,17 +1,8 @@ package com.tangem.tap.common.log -import android.os.Build -import android.util.Log -import co.touchlab.kermit.BaseLogger -import co.touchlab.kermit.LogWriter -import co.touchlab.kermit.Logger -import co.touchlab.kermit.Severity -import com.orhanobut.logger.AndroidLogAdapter import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.BuildConfig -import java.util.regex.Pattern -import com.orhanobut.logger.Logger as PrettyLogger /** * Tangem app logger @@ -24,98 +15,14 @@ class TangemAppLoggerInitializer( private val appLogsStore: AppLogsStore, ) { - /** Initialize */ fun initialize() { - if (IS_LOG_ENABLED) { - PrettyLogger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy())) - } - - Logger.setLogWriters(KermitLogWriter(::finalLogOutput)) - } - - private fun finalLogOutput(priority: Int, tag: String?, message: String, t: Throwable?) { - if (IS_LOG_ENABLED) { - PrettyLogger.log(priority, tag, message, t) - } - - if (PERMITTED_PRIORITY.contains(priority)) { - appLogsStore.saveLogMessage( - tag = tag ?: "TangemAppLogger", - message = message, - ) - } - } - - @Suppress("BooleanPropertyNaming") - private companion object { - val IS_LOG_ENABLED: Boolean = BuildConfig.LOG_ENABLED - val PERMITTED_PRIORITY = listOf(Log.ERROR, Log.INFO) - } -} - -private class KermitLogWriter( - private val finalLogOutput: (priority: Int, tag: String?, message: String, t: Throwable?) -> Unit, -) : LogWriter() { - - private val fqcnIgnore = setOf( - LogWriter::class.java.name, - KermitLogWriter::class.java.name, - BaseLogger::class.java.name, - Logger::class.java.name, - TangemLogger::class.java.name, - TangemLogger.TaggedLogger::class.java.name, - ) - - override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { - val priority = when (severity) { - Severity.Verbose -> PrettyLogger.VERBOSE - Severity.Debug -> PrettyLogger.DEBUG - Severity.Info -> PrettyLogger.INFO - Severity.Warn -> PrettyLogger.WARN - Severity.Error -> PrettyLogger.ERROR - Severity.Assert -> PrettyLogger.ASSERT - } - - val finalTag = if (tag != KERMIT_LOGGER_DEFAULT_TAG) { - tag - } else { - /** - * like in [Logger.debugTree.tag] - */ - @Suppress("UnnecessaryLet", "ThrowingExceptionsWithoutMessageOrCause") - Throwable().stackTrace - .first { it.className !in fqcnIgnore } - .let(::createStackElementTag) - } - - finalLogOutput(priority, finalTag, message, throwable) - } - - /** - * copy from [Logger.debugTree.createStackElementTag] - */ - @Suppress("MagicNumber") - private fun createStackElementTag(element: StackTraceElement): String? { - var tag = element.className.substringAfterLast('.') - val m = ANONYMOUS_CLASS.matcher(tag) - if (m.find()) { - tag = m.replaceAll("") - } - // Tag length limit was removed in API 26. - return if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) { - tag - } else { - tag.substring(0, MAX_TAG_LENGTH) - } - } - - private companion object { - private const val KERMIT_LOGGER_DEFAULT_TAG = "" - - /** - * copy from [Logger.debugTree.Companion] - */ - private const val MAX_TAG_LENGTH = 23 - private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$") + TangemLogger.setLogWriters( + buildList { + if (BuildConfig.LOG_ENABLED) { + add(LogcatLogWriter()) + } + add(FileLogWriter(appLogsStore)) + }, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt b/app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt deleted file mode 100644 index 0a43f525d4..0000000000 --- a/app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.tap.common.log - -import com.orhanobut.logger.FormatStrategy -import com.orhanobut.logger.LogStrategy -import com.orhanobut.logger.LogcatLogStrategy - -class TimberFormatStrategy : FormatStrategy { - - private val logStrategy: LogStrategy = LogcatLogStrategy() - - override fun log(priority: Int, tag: String?, message: String) { - logTopBorder(priority, tag) - val bytes = message.toByteArray() - val length = bytes.size - if (length <= CHUNK_SIZE) { - logContent(priority, tag, message) - logBottomBorder(priority, tag) - return - } - var i = 0 - while (i < length) { - val count = (length - i).coerceAtMost(CHUNK_SIZE) - // create a new String with system's default charset (which is UTF-8 for Android) - logContent(priority, tag, String(bytes, i, count)) - i += CHUNK_SIZE - } - logBottomBorder(priority, tag) - } - - private fun logTopBorder(logType: Int, tag: String?) { - logChunk(logType, tag, TOP_BORDER) - } - - private fun logBottomBorder(logType: Int, tag: String?) { - logChunk(logType, tag, BOTTOM_BORDER) - } - - private fun logContent(logType: Int, tag: String?, chunk: String) { - chunk.split(System.lineSeparator()).forEach { line -> - logChunk(logType, tag, "$HORIZONTAL_LINE $line") - } - } - - private fun logChunk(priority: Int, tag: String?, chunk: String) { - logStrategy.log(priority, tag, chunk) - } - - private companion object { - /** - * Android's max limit for a log entry is ~4076 bytes, - * so 4000 bytes is used as chunk size since default charset - * is UTF-8 - */ - private const val CHUNK_SIZE = 4000 - - const val TOP_LEFT_CORNER = "┌" - const val BOTTOM_LEFT_CORNER = "└" - const val HORIZONTAL_LINE = "│" - const val DOUBLE_DIVIDER = "────────────────────────────────────────────────────────" - const val TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER - const val BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER - } -} \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/common/log/FileLogWriterTest.kt b/app/src/test/kotlin/com/tangem/tap/common/log/FileLogWriterTest.kt new file mode 100644 index 0000000000..ea9d224b42 --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/common/log/FileLogWriterTest.kt @@ -0,0 +1,157 @@ +package com.tangem.tap.common.log + +import com.google.common.truth.Truth +import com.tangem.datasource.local.logs.AppLogsStore +import com.tangem.utils.logging.Severity +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class FileLogWriterTest { + + private val appLogsStore: AppLogsStore = mockk(relaxUnitFun = true) + private val writer = FileLogWriter(appLogsStore) + + // region isLoggable filter + + @Test + fun `isLoggable returns true for Error severity`() { + Truth.assertThat(writer.isLoggable(Severity.Error, "tag")).isTrue() + } + + @Test + fun `isLoggable returns true for Info severity`() { + Truth.assertThat(writer.isLoggable(Severity.Info, "tag")).isTrue() + } + + @Test + fun `isLoggable returns false for Verbose severity`() { + Truth.assertThat(writer.isLoggable(Severity.Verbose, "tag")).isFalse() + } + + @Test + fun `isLoggable returns false for Debug severity`() { + Truth.assertThat(writer.isLoggable(Severity.Debug, "tag")).isFalse() + } + + @Test + fun `isLoggable returns false for Warn severity`() { + Truth.assertThat(writer.isLoggable(Severity.Warn, "tag")).isFalse() + } + + @Test + fun `isLoggable returns false for Assert severity`() { + Truth.assertThat(writer.isLoggable(Severity.Assert, "tag")).isFalse() + } + + @Test + fun `isLoggable result is independent of the tag value`() { + Truth.assertThat(writer.isLoggable(Severity.Info, "")).isTrue() + Truth.assertThat(writer.isLoggable(Severity.Info, "anything")).isTrue() + Truth.assertThat(writer.isLoggable(Severity.Debug, "")).isFalse() + Truth.assertThat(writer.isLoggable(Severity.Debug, "anything")).isFalse() + } + + // endregion + + // region write delegation + + @Test + fun `write forwards tag, message, throwable and shouldSanitize to AppLogsStore`() { + // Arrange + val throwable = IllegalStateException("boom") + + // Act + writer.write( + severity = Severity.Error, + tag = "MyTag", + message = "error happened", + throwable = throwable, + shouldSanitize = true, + ) + + // Assert + verify(exactly = 1) { + appLogsStore.saveLogMessage( + tag = "MyTag", + message = "error happened", + throwable = throwable, + shouldSanitize = true, + ) + } + } + + @Test + fun `write forwards null throwable as null`() { + // Act + writer.write( + severity = Severity.Info, + tag = "Tag", + message = "info", + throwable = null, + shouldSanitize = true, + ) + + // Assert + verify(exactly = 1) { + appLogsStore.saveLogMessage( + tag = "Tag", + message = "info", + throwable = null, + shouldSanitize = true, + ) + } + } + + @Test + fun `write forwards shouldSanitize false to AppLogsStore so sanitizer is bypassed`() { + // Act + writer.write( + severity = Severity.Info, + tag = "Tag", + message = "raw payload", + throwable = null, + shouldSanitize = false, + ) + + // Assert + verify(exactly = 1) { + appLogsStore.saveLogMessage( + tag = "Tag", + message = "raw payload", + throwable = null, + shouldSanitize = false, + ) + } + } + + @Test + fun `write delegates regardless of severity (filtering is the caller's job)`() { + // The contract: TangemLogger asks isLoggable first; if a caller bypasses that and + // invokes write directly, the writer should still delegate to the store. + Severity.entries.forEach { severity -> + // Act + writer.write( + severity = severity, + tag = "Tag", + message = "msg-$severity", + throwable = null, + shouldSanitize = true, + ) + + // Assert + verify(exactly = 1) { + appLogsStore.saveLogMessage( + tag = "Tag", + message = "msg-$severity", + throwable = null, + shouldSanitize = true, + ) + } + } + } + + // endregion +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/common/log/LogcatLogWriterTest.kt b/app/src/test/kotlin/com/tangem/tap/common/log/LogcatLogWriterTest.kt new file mode 100644 index 0000000000..57a2b8fb7d --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/common/log/LogcatLogWriterTest.kt @@ -0,0 +1,213 @@ +package com.tangem.tap.common.log + +import android.util.Log +import com.tangem.utils.logging.Severity +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class LogcatLogWriterTest { + + private val writer = LogcatLogWriter() + + @BeforeEach + fun setUp() { + mockkStatic(Log::class) + every { Log.println(any(), any(), any()) } returns 0 + every { Log.getStackTraceString(any()) } returns "STACK" + } + + @AfterEach + fun tearDown() { + unmockkStatic(Log::class) + } + + // region Severity → Android priority mapping + + @Test + fun `Verbose severity maps to Log VERBOSE priority`() { + // Act + writer.write(Severity.Verbose, "Tag", "msg", throwable = null, shouldSanitize = true) + + // Assert + verify { Log.println(Log.VERBOSE, "Tag", any()) } + } + + @Test + fun `Debug severity maps to Log DEBUG priority`() { + // Act + writer.write(Severity.Debug, "Tag", "msg", throwable = null, shouldSanitize = true) + + // Assert + verify { Log.println(Log.DEBUG, "Tag", any()) } + } + + @Test + fun `Info severity maps to Log INFO priority`() { + // Act + writer.write(Severity.Info, "Tag", "msg", throwable = null, shouldSanitize = true) + + // Assert + verify { Log.println(Log.INFO, "Tag", any()) } + } + + @Test + fun `Warn severity maps to Log WARN priority`() { + // Act + writer.write(Severity.Warn, "Tag", "msg", throwable = null, shouldSanitize = true) + + // Assert + verify { Log.println(Log.WARN, "Tag", any()) } + } + + @Test + fun `Error severity maps to Log ERROR priority`() { + // Act + writer.write(Severity.Error, "Tag", "msg", throwable = null, shouldSanitize = true) + + // Assert + verify { Log.println(Log.ERROR, "Tag", any()) } + } + + @Test + fun `Assert severity maps to Log ASSERT priority`() { + // Act + writer.write(Severity.Assert, "Tag", "msg", throwable = null, shouldSanitize = true) + + // Assert + verify { Log.println(Log.ASSERT, "Tag", any()) } + } + + // endregion + + // region Box layout + + @Test + fun `single-line message is wrapped between top and bottom borders`() { + // Act + writer.write(Severity.Info, "Tag", "hello", throwable = null, shouldSanitize = true) + + // Assert + verifySequence { + Log.println(Log.INFO, "Tag", match { it.startsWith("┌") }) + Log.println(Log.INFO, "Tag", "│ hello") + Log.println(Log.INFO, "Tag", match { it.startsWith("└") }) + } + } + + @Test + fun `each line of a multi-line message is printed as a separate logcat entry`() { + // Arrange + val sep = System.lineSeparator() + val message = "first${sep}second${sep}third" + + // Act + writer.write(Severity.Debug, "Tag", message, throwable = null, shouldSanitize = true) + + // Assert + verifySequence { + Log.println(Log.DEBUG, "Tag", match { it.startsWith("┌") }) + Log.println(Log.DEBUG, "Tag", "│ first") + Log.println(Log.DEBUG, "Tag", "│ second") + Log.println(Log.DEBUG, "Tag", "│ third") + Log.println(Log.DEBUG, "Tag", match { it.startsWith("└") }) + } + } + + // endregion + + // region Throwable handling + + @Test + fun `throwable is rendered via Log getStackTraceString`() { + // Arrange + val throwable = RuntimeException("boom") + every { Log.getStackTraceString(throwable) } returns "STACK" + + // Act + writer.write(Severity.Error, "Tag", "fail", throwable = throwable, shouldSanitize = true) + + // Assert + verify(exactly = 1) { Log.getStackTraceString(throwable) } + } + + @Test + fun `null throwable does not invoke getStackTraceString`() { + // Act + writer.write(Severity.Info, "Tag", "no throwable", throwable = null, shouldSanitize = true) + + // Assert + verify(exactly = 0) { Log.getStackTraceString(any()) } + } + + // endregion + + // region Chunking of long messages + + @Test + fun `message under CHUNK_SIZE bytes produces a single content line`() { + // Arrange + val message = "a".repeat(3999) + + // Act + writer.write(Severity.Info, "Tag", message, throwable = null, shouldSanitize = true) + + // Assert — top border + 1 content line + bottom border + verify(exactly = 3) { Log.println(Log.INFO, "Tag", any()) } + } + + @Test + fun `message exceeding CHUNK_SIZE bytes is split into multiple chunks`() { + // Arrange — 9000 ASCII bytes → chunks of 4000 + 4000 + 1000 = 3 chunks + val message = "a".repeat(9000) + + // Act + writer.write(Severity.Info, "Tag", message, throwable = null, shouldSanitize = true) + + // Assert — top border + 3 content lines + bottom border + verify(exactly = 5) { Log.println(Log.INFO, "Tag", any()) } + } + + // endregion + + // region Tag truncation + + @Test + fun `tag longer than 23 chars is truncated on legacy Android API stub`() { + // Arrange — in the unit-test Android stub, Build.VERSION.SDK_INT == 0, + // triggering the legacy truncation path. + val longTag = "a".repeat(50) + + // Act + writer.write(Severity.Info, longTag, "msg", throwable = null, shouldSanitize = true) + + // Assert + verify { Log.println(Log.INFO, "a".repeat(23), any()) } + } + + @Test + fun `tag of MAX_TAG_LENGTH chars is not truncated`() { + // Arrange + val tag = "a".repeat(23) + + // Act + writer.write(Severity.Info, tag, "msg", throwable = null, shouldSanitize = true) + + // Assert + verify { Log.println(Log.INFO, "a".repeat(23), any()) } + } + + @Test + fun `short tag is forwarded verbatim`() { + // Act + writer.write(Severity.Info, "Short", "msg", throwable = null, shouldSanitize = true) + + // Assert + verify { Log.println(Log.INFO, "Short", any()) } + } + + // endregion +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt index 830cc43c53..507dd085c9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt @@ -4,34 +4,6 @@ import android.util.Log import com.ihsanbal.logging.Level import com.ihsanbal.logging.LoggingInterceptor import okhttp3.Interceptor -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import java.util.concurrent.TimeUnit - -@Suppress("MagicNumber") -@Deprecated("Create and provide by DI") -fun createRetrofitInstance( - baseUrl: String, - okHttpBuilder: OkHttpClient.Builder = OkHttpClient.Builder(), - interceptors: List = emptyList(), - logEnabled: Boolean, -): Retrofit { - okHttpBuilder.apply { - callTimeout(10, TimeUnit.SECONDS) - connectTimeout(20, TimeUnit.SECONDS) - readTimeout(20, TimeUnit.SECONDS) - writeTimeout(20, TimeUnit.SECONDS) - } - interceptors.forEach { okHttpBuilder.addInterceptor(it) } - - if (logEnabled) okHttpBuilder.addInterceptor(createNetworkLoggingInterceptor()) - - return Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(MoshiConverter.networkMoshiConverter) - .client(okHttpBuilder.build()) - .build() -} fun createNetworkLoggingInterceptor(): Interceptor { return LoggingInterceptor.Builder() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index 65dd45cf46..1704e959a9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -70,12 +70,17 @@ class AppLogsStore @Inject constructor( } } - /** Save log [message] */ - fun saveLogMessage(tag: String, message: String) { + /** + * Save log [message]. Pass [shouldSanitize] = false to bypass [LogsSanitizer]. + * The optional [throwable]'s stack trace is appended verbatim (never sanitized), + * since stack traces routinely contain hex-like sequences that the sanitizer would + * otherwise destroy. + */ + fun saveLogMessage(tag: String, message: String, throwable: Throwable? = null, shouldSanitize: Boolean = true) { launchWithLock { createFileIfNotExist() - writeMessage(tag = tag, message) + writeMessage(tag = tag, shouldSanitize = shouldSanitize, throwable = throwable, message) } } @@ -84,7 +89,7 @@ class AppLogsStore @Inject constructor( launchWithLock { createFileIfNotExist() - writeMessage(tag = tag, *messages) + writeMessage(tag = tag, shouldSanitize = true, throwable = null, messages = messages) } } @@ -97,24 +102,16 @@ class AppLogsStore @Inject constructor( } } - fun deleteOldLogsFile() { - val file = File(applicationContext.filesDir, LOG_FILE_NAME) - - if (file.exists()) file.delete() - } - - fun deleteLastLogFile() { - val file = File(applicationContext.filesDir, NEW_LOG_FILE_NAME) - - if (file.exists()) file.delete() - } - - private fun writeMessage(tag: String, vararg messages: String) { + private fun writeMessage(tag: String, shouldSanitize: Boolean, throwable: Throwable?, vararg messages: String) { BufferedWriter(FileWriter(logFile, true)).use { writer -> writer.append(formatter.print(DateTime.now())) writer.append(": $tag ") - messages.map(LogsSanitizer::sanitize) - .forEach(writer::append) + val processed = if (shouldSanitize) messages.map(LogsSanitizer::sanitize) else messages.toList() + processed.forEach(writer::append) + if (throwable != null) { + writer.newLine() + writer.append(throwable.stackTraceToString().trimEnd()) + } writer.newLine() } } @@ -166,8 +163,6 @@ class AppLogsStore @Inject constructor( private companion object { const val BUFFER_SIZE = 1024 - const val LOG_FILE_NAME = "logs.txt" - const val NEW_LOG_FILE_NAME = "app_logs.txt" // the only name that we allow to send as email to company addresses const val PERMITTED_FILE_NAME = "log.txt" const val PERMITTED_FILE_NAME_ZIP = "log.zip" diff --git a/core/utils/build.gradle.kts b/core/utils/build.gradle.kts index 0d59823349..48e632eff4 100644 --- a/core/utils/build.gradle.kts +++ b/core/utils/build.gradle.kts @@ -23,10 +23,6 @@ dependencies { implementation(deps.jodatime) // endregion - // region Logging - implementation(deps.kermit) - // endregion - testImplementation(deps.test.coroutine) testImplementation(deps.test.junit5) testRuntimeOnly(deps.test.junit5.engine) diff --git a/core/utils/src/main/java/com/tangem/utils/logging/BaseLogger.kt b/core/utils/src/main/java/com/tangem/utils/logging/BaseLogger.kt new file mode 100644 index 0000000000..dca1140ca6 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/logging/BaseLogger.kt @@ -0,0 +1,13 @@ +package com.tangem.utils.logging + +/** + * Common contract for application loggers. + */ +internal interface BaseLogger { + fun v(messageString: String, throwable: Throwable? = null, shouldSanitize: Boolean = true) + fun d(messageString: String, throwable: Throwable? = null, shouldSanitize: Boolean = true) + fun i(messageString: String, throwable: Throwable? = null, shouldSanitize: Boolean = true) + fun w(messageString: String, throwable: Throwable? = null, shouldSanitize: Boolean = true) + fun e(messageString: String, throwable: Throwable? = null, shouldSanitize: Boolean = true) + fun a(messageString: String, throwable: Throwable? = null, shouldSanitize: Boolean = true) +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/logging/LogTagResolver.kt b/core/utils/src/main/java/com/tangem/utils/logging/LogTagResolver.kt new file mode 100644 index 0000000000..0e0228c472 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/logging/LogTagResolver.kt @@ -0,0 +1,35 @@ +package com.tangem.utils.logging + +import java.util.regex.Pattern + +/** + * Resolves a log tag from the call site's class name when the caller didn't supply one + * via [TangemLogger.withTag]. Used by [TangemLogger.write] before dispatching to writers. + */ +internal object LogTagResolver { + + private const val FALLBACK_TAG = "TangemAppLogger" + + private val ANONYMOUS_CLASS_REGEX: Pattern = Pattern.compile("(\\$\\d+)+$") + + private val FQCN_IGNORE = setOf( + LogTagResolver::class.java.name, + TangemLogger::class.java.name, + TangemLogger.TaggedLogger::class.java.name, + // Synthetic class generated for BaseLogger's default-arg trampolines (d$default, etc.). + // Without this, every call that omits default args resolves to BaseLogger.DefaultImpls. + "${BaseLogger::class.java.name}\$DefaultImpls", + ) + + @Suppress("ThrowingExceptionsWithoutMessageOrCause") + fun resolveTag(): String { + val element = Throwable().stackTrace.firstOrNull { it.className !in FQCN_IGNORE } + ?: return FALLBACK_TAG + var tag = element.className.substringAfterLast('.') + val matcher = ANONYMOUS_CLASS_REGEX.matcher(tag) + if (matcher.find()) { + tag = matcher.replaceAll("") + } + return tag + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/logging/Severity.kt b/core/utils/src/main/java/com/tangem/utils/logging/Severity.kt new file mode 100644 index 0000000000..52f9e62a20 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/logging/Severity.kt @@ -0,0 +1,10 @@ +package com.tangem.utils.logging + +enum class Severity { + Verbose, + Debug, + Info, + Warn, + Error, + Assert, +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt b/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt index 5bd1fab06f..c7a8aa10ef 100644 --- a/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt +++ b/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt @@ -1,63 +1,173 @@ package com.tangem.utils.logging -import co.touchlab.kermit.Logger +import java.util.concurrent.CopyOnWriteArrayList /** - * Application-level logger that wraps Kermit [Logger] with the same API. - * All modules should use [TangemLogger] instead of importing Kermit directly. + * Application-level logger */ -object TangemLogger { +object TangemLogger : BaseLogger { - fun v(messageString: String, throwable: Throwable? = null) { - Logger.v(messageString, throwable) + private val logWriters = CopyOnWriteArrayList() + + fun setLogWriters(writers: List) { + logWriters.clear() + logWriters.addAll(writers) } - fun d(messageString: String, throwable: Throwable? = null) { - Logger.d(messageString, throwable) + fun addLogWriter(writer: LogWriter) { + logWriters.add(writer) } - fun i(messageString: String, throwable: Throwable? = null) { - Logger.i(messageString, throwable) + override fun v(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Verbose, + tag = null, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) } - fun w(messageString: String, throwable: Throwable? = null) { - Logger.w(messageString, throwable) + override fun d(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Debug, + tag = null, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) } - fun e(messageString: String, throwable: Throwable? = null) { - Logger.e(messageString, throwable) + override fun i(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Info, + tag = null, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) } - fun a(messageString: String, throwable: Throwable? = null) { - Logger.a(messageString, throwable) + override fun w(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Warn, + tag = null, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } + + override fun e(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Error, + tag = null, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } + + override fun a(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Assert, + tag = null, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) } fun withTag(tag: String): TaggedLogger = TaggedLogger(tag) - class TaggedLogger internal constructor(private val tag: String) { - - fun v(messageString: String, throwable: Throwable? = null) { - Logger.withTag(tag).v(messageString, throwable) - } - - fun d(messageString: String, throwable: Throwable? = null) { - Logger.withTag(tag).d(messageString, throwable) - } - - fun i(messageString: String, throwable: Throwable? = null) { - Logger.withTag(tag).i(messageString, throwable) - } - - fun w(messageString: String, throwable: Throwable? = null) { - Logger.withTag(tag).w(messageString, throwable) - } - - fun e(messageString: String, throwable: Throwable? = null) { - Logger.withTag(tag).e(messageString, throwable) - } - - fun a(messageString: String, throwable: Throwable? = null) { - Logger.withTag(tag).a(messageString, throwable) + private fun write( + severity: Severity, + tag: String?, + message: String, + throwable: Throwable?, + shouldSanitize: Boolean, + ) { + val resolvedTag = tag ?: LogTagResolver.resolveTag() + logWriters.forEach { writer -> + if (writer.isLoggable(severity, resolvedTag)) { + writer.write( + severity = severity, + tag = resolvedTag, + message = message, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } } } + + class TaggedLogger internal constructor(private val tag: String) : BaseLogger { + + override fun v(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Verbose, + tag = tag, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } + + override fun d(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Debug, + tag = tag, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } + + override fun i(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Info, + tag = tag, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } + + override fun w(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Warn, + tag = tag, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } + + override fun e(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Error, + tag = tag, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } + + override fun a(messageString: String, throwable: Throwable?, shouldSanitize: Boolean) { + write( + severity = Severity.Assert, + tag = tag, + message = messageString, + throwable = throwable, + shouldSanitize = shouldSanitize, + ) + } + } + + interface LogWriter { + + fun isLoggable(severity: Severity, tag: String): Boolean = true + + fun write(severity: Severity, tag: String, message: String, throwable: Throwable?, shouldSanitize: Boolean) + } } \ No newline at end of file diff --git a/core/utils/src/test/kotlin/com/tangem/utils/logging/LogTagResolverTest.kt b/core/utils/src/test/kotlin/com/tangem/utils/logging/LogTagResolverTest.kt new file mode 100644 index 0000000000..194944c929 --- /dev/null +++ b/core/utils/src/test/kotlin/com/tangem/utils/logging/LogTagResolverTest.kt @@ -0,0 +1,100 @@ +package com.tangem.utils.logging + +import com.google.common.truth.Truth +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class LogTagResolverTest { + + @AfterEach + fun tearDown() { + // Clean up shared TangemLogger state used in some cases + TangemLogger.setLogWriters(emptyList()) + } + + @Test + fun `resolveTag returns the simple class name of the direct caller`() { + // Act + val tag = LogTagResolver.resolveTag() + + // Assert + Truth.assertThat(tag).isEqualTo("LogTagResolverTest") + } + + @Test + fun `resolveTag does not include package qualifier`() { + // Act + val tag = LogTagResolver.resolveTag() + + // Assert + Truth.assertThat(tag).doesNotContain(".") + } + + @Test + fun `resolveTag never returns its own class name`() { + // Act + val tag = LogTagResolver.resolveTag() + + // Assert + Truth.assertThat(tag).isNotEqualTo("LogTagResolver") + } + + @Test + fun `resolveTag skips TangemLogger frames when invoked through it`() { + // Arrange + var capturedTag: String? = null + val writer = object : TangemLogger.LogWriter { + override fun write( + severity: Severity, + tag: String, + message: String, + throwable: Throwable?, + shouldSanitize: Boolean, + ) { + capturedTag = tag + } + } + TangemLogger.setLogWriters(listOf(writer)) + + // Act + TangemLogger.d("via TangemLogger") + + // Assert — TangemLogger and LogTagResolver are filtered, leaving the test class + Truth.assertThat(capturedTag).isEqualTo("LogTagResolverTest") + } + + @Test + fun `resolveTag is bypassed by TaggedLogger when an explicit tag is supplied`() { + // Arrange + var capturedTag: String? = null + val writer = object : TangemLogger.LogWriter { + override fun write( + severity: Severity, + tag: String, + message: String, + throwable: Throwable?, + shouldSanitize: Boolean, + ) { + capturedTag = tag + } + } + TangemLogger.setLogWriters(listOf(writer)) + + // Act + TangemLogger.withTag("ExplicitTag").d("hi") + + // Assert + Truth.assertThat(capturedTag).isEqualTo("ExplicitTag") + } + + @Test + fun `resolveTag returns a non-empty string`() { + // Act + val tag = LogTagResolver.resolveTag() + + // Assert + Truth.assertThat(tag).isNotEmpty() + } +} \ No newline at end of file diff --git a/core/utils/src/test/kotlin/com/tangem/utils/logging/TangemLoggerTest.kt b/core/utils/src/test/kotlin/com/tangem/utils/logging/TangemLoggerTest.kt new file mode 100644 index 0000000000..a1c42eae34 --- /dev/null +++ b/core/utils/src/test/kotlin/com/tangem/utils/logging/TangemLoggerTest.kt @@ -0,0 +1,311 @@ +package com.tangem.utils.logging + +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import io.mockk.verifyOrder +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TangemLoggerTest { + + private lateinit var writer: TangemLogger.LogWriter + + @BeforeEach + fun setUp() { + writer = mockk(relaxed = true) + every { writer.isLoggable(any(), any()) } returns true + TangemLogger.setLogWriters(listOf(writer)) + } + + @AfterEach + fun tearDown() { + // Reset singleton state to avoid cross-test pollution + TangemLogger.setLogWriters(emptyList()) + } + + // region Severity dispatch + + @Test + fun `v dispatches Verbose severity to writer`() { + // Act + TangemLogger.v("verbose message") + + // Assert + verify(exactly = 1) { + writer.write(Severity.Verbose, any(), "verbose message", null, true) + } + } + + @Test + fun `d dispatches Debug severity to writer`() { + // Act + TangemLogger.d("debug message") + + // Assert + verify(exactly = 1) { + writer.write(Severity.Debug, any(), "debug message", null, true) + } + } + + @Test + fun `i dispatches Info severity to writer`() { + // Act + TangemLogger.i("info message") + + // Assert + verify(exactly = 1) { + writer.write(Severity.Info, any(), "info message", null, true) + } + } + + @Test + fun `w dispatches Warn severity to writer`() { + // Act + TangemLogger.w("warn message") + + // Assert + verify(exactly = 1) { + writer.write(Severity.Warn, any(), "warn message", null, true) + } + } + + @Test + fun `e dispatches Error severity to writer`() { + // Act + TangemLogger.e("error message") + + // Assert + verify(exactly = 1) { + writer.write(Severity.Error, any(), "error message", null, true) + } + } + + @Test + fun `a dispatches Assert severity to writer`() { + // Act + TangemLogger.a("assert message") + + // Assert + verify(exactly = 1) { + writer.write(Severity.Assert, any(), "assert message", null, true) + } + } + + // endregion + + // region Throwable & shouldSanitize propagation + + @Test + fun `throwable parameter is forwarded to writer`() { + // Arrange + val throwable = IllegalStateException("boom") + + // Act + TangemLogger.e("error", throwable) + + // Assert + verify(exactly = 1) { + writer.write(Severity.Error, any(), "error", throwable, true) + } + } + + @Test + fun `shouldSanitize flag is forwarded to writer`() { + // Act + TangemLogger.w("not checked", shouldSanitize = false) + + // Assert + verify(exactly = 1) { + writer.write(Severity.Warn, any(), "not checked", null, false) + } + } + + // endregion + + // region setLogWriters / addLogWriter + + @Test + fun `setLogWriters replaces previously registered writers`() { + // Arrange + val previous: TangemLogger.LogWriter = mockk(relaxed = true) + every { previous.isLoggable(any(), any()) } returns true + val replacement: TangemLogger.LogWriter = mockk(relaxed = true) + every { replacement.isLoggable(any(), any()) } returns true + + TangemLogger.setLogWriters(listOf(previous)) + TangemLogger.setLogWriters(listOf(replacement)) + + // Act + TangemLogger.i("after replace") + + // Assert + verify(exactly = 0) { previous.write(any(), any(), any(), any(), any()) } + verify(exactly = 1) { + replacement.write(Severity.Info, any(), "after replace", null, true) + } + } + + @Test + fun `addLogWriter appends without removing existing writers`() { + // Arrange + val first: TangemLogger.LogWriter = mockk(relaxed = true) + every { first.isLoggable(any(), any()) } returns true + val second: TangemLogger.LogWriter = mockk(relaxed = true) + every { second.isLoggable(any(), any()) } returns true + + TangemLogger.setLogWriters(listOf(first)) + TangemLogger.addLogWriter(second) + + // Act + TangemLogger.d("broadcast") + + // Assert + verify(exactly = 1) { first.write(Severity.Debug, any(), "broadcast", null, true) } + verify(exactly = 1) { second.write(Severity.Debug, any(), "broadcast", null, true) } + } + + @Test + fun `setLogWriters with empty list silences all output`() { + // Arrange + TangemLogger.setLogWriters(emptyList()) + + // Act + TangemLogger.i("nobody listening") + + // Assert + verify(exactly = 0) { writer.write(any(), any(), any(), any(), any()) } + } + + // endregion + + // region isLoggable filtering + + @Test + fun `write is skipped when isLoggable returns false`() { + // Arrange + every { writer.isLoggable(any(), any()) } returns false + + // Act + TangemLogger.w("filtered out") + + // Assert + verify(exactly = 1) { writer.isLoggable(Severity.Warn, any()) } + verify(exactly = 0) { writer.write(any(), any(), any(), any(), any()) } + } + + @Test + fun `each writer is filtered independently by its own isLoggable`() { + // Arrange + val accepting: TangemLogger.LogWriter = mockk(relaxed = true) + every { accepting.isLoggable(any(), any()) } returns true + val rejecting: TangemLogger.LogWriter = mockk(relaxed = true) + every { rejecting.isLoggable(any(), any()) } returns false + + TangemLogger.setLogWriters(listOf(accepting, rejecting)) + + // Act + TangemLogger.i("partial") + + // Assert + verify(exactly = 1) { accepting.write(Severity.Info, any(), "partial", null, true) } + verify(exactly = 0) { rejecting.write(any(), any(), any(), any(), any()) } + } + + @Test + fun `LogWriter isLoggable defaults to true`() { + // Arrange + val realWriter = object : TangemLogger.LogWriter { + override fun write( + severity: Severity, + tag: String, + message: String, + throwable: Throwable?, + shouldSanitize: Boolean, + ) = Unit + } + + // Act + Assert + Severity.entries.forEach { severity -> + Truth.assertThat(realWriter.isLoggable(severity, "anyTag")).isTrue() + } + } + + // endregion + + // region Tag resolution + + @Test + fun `resolved tag falls back to caller class name when no tag is provided`() { + // Act + TangemLogger.d("no tag") + + // Assert + verify(exactly = 1) { + writer.write(Severity.Debug, "TangemLoggerTest", "no tag", null, true) + } + } + + @Test + fun `withTag returns a TaggedLogger that uses the supplied tag`() { + // Arrange + val tagged = TangemLogger.withTag("MyFeature") + + // Act + tagged.i("hello") + + // Assert + verify(exactly = 1) { + writer.write(Severity.Info, "MyFeature", "hello", null, true) + } + } + + // endregion + + // region TaggedLogger + + @Test + fun `TaggedLogger dispatches each severity with its tag, throwable and shouldSanitize flag`() { + // Arrange + val tagged = TangemLogger.withTag("Tag") + val throwable = RuntimeException("oops") + + // Act + tagged.v("v") + tagged.d("d") + tagged.i("i") + tagged.w("w") + tagged.e("e", throwable) + tagged.a("a", shouldSanitize = false) + + // Assert + verifyOrder { + writer.write(Severity.Verbose, "Tag", "v", null, true) + writer.write(Severity.Debug, "Tag", "d", null, true) + writer.write(Severity.Info, "Tag", "i", null, true) + writer.write(Severity.Warn, "Tag", "w", null, true) + writer.write(Severity.Error, "Tag", "e", throwable, true) + writer.write(Severity.Assert, "Tag", "a", null, false) + } + } + + @Test + fun `TaggedLogger respects writer isLoggable filtering`() { + // Arrange + every { writer.isLoggable(any(), any()) } returns false + val tagged = TangemLogger.withTag("Filtered") + + // Act + tagged.e("ignored") + + // Assert + verify(exactly = 1) { writer.isLoggable(Severity.Error, "Filtered") } + verify(exactly = 0) { writer.write(any(), any(), any(), any(), any()) } + } + + // endregion +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5bf1853472..5c04c70efa 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -76,7 +76,6 @@ okhttp = "4.9.3" retrofit = "2.11.0" retrofitMoshiConverter = "2.9.0" spongycastleCryptoCore = "1.58.0.0" -kermit = "2.1.0" viewBindingDelegate = "1.5.9" xmlShimmer = "1.1.3" zxingQrCode = "3.5.1" @@ -85,7 +84,6 @@ kotlinDatetime = "0.6.2" arrow = "1.2.4" # 2.0.1 breaks the build reownCore = "1.4.11" reownWeb3 = "1.4.11" -prettyLogger = "2.2.0" okHttp-prettyLogging = "3.1.0" chucker = "4.2.0" mlKit-barcodeScanning = "17.3.0" @@ -282,7 +280,6 @@ spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "sp retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-response-type-keeper = { module = "com.squareup.retrofit2:response-type-keeper", version.ref = "retrofit" } retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofitMoshiConverter" } -kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version.ref = "viewBindingDelegate" } xmlShimmer = { module = "com.github.skydoves:androidveil", version.ref = "xmlShimmer" } zxing-qrCore = { module = "com.google.zxing:core", version.ref = "zxingQrCode" } @@ -292,7 +289,6 @@ arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } arrow-fx = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } reownCore = { module = "com.reown:android-core", version.ref = "reownCore" } reownWeb3 = { module = "com.reown:walletkit", version.ref = "reownWeb3" } -prettyLogger = { module = "com.orhanobut:logger", version.ref = "prettyLogger" } chucker = { module = "com.github.chuckerteam.chucker:library", version.ref = "chucker" } chuckerStub = { module = "com.github.chuckerteam.chucker:library-no-op", version.ref = "chucker" } mlKit-barcodeScanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlKit-barcodeScanning" }