From 842639bcb43bf57a213f2506e7d0a22c496f6a23 Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy Date: Sun, 16 Jun 2024 19:44:11 +0300 Subject: [PATCH] feature: Record / export logs --- .../keyguard/copy/LogRepositoryAndroid.kt | 26 +- .../session/FingerprintRepositoryModule.kt | 2 +- .../composeResources/values/strings.xml | 7 + .../artemchep/keyguard/common/model/Log.kt | 11 + .../keyguard/common/model/SyncProgress.kt | 18 ++ .../common/service/logging/LogLevel.kt | 12 +- .../common/service/logging/LogRepository.kt | 55 +++- .../service/logging/LogRepositoryBase.kt | 24 ++ .../service/logging/LogRepositoryChild.kt | 3 + .../logging/inmemory/InMemoryLogRepository.kt | 17 ++ .../inmemory/InMemoryLogRepositoryImpl.kt | 62 ++++ .../logging/kotlin/LogRepositoryKotlin.kt | 20 +- .../service/permission/PermissionState.kt | 4 + .../keyguard/common/usecase/ExportLogs.kt | 6 + .../common/usecase/GetInMemoryLogs.kt | 7 + .../common/usecase/GetInMemoryLogsEnabled.kt | 8 + .../common/usecase/PutInMemoryLogsEnabled.kt | 5 + .../keyguard/common/usecase/Watchdog.kt | 34 ++- .../impl/GetInMemoryLogsEnabledImpl.kt | 21 ++ .../usecase/impl/GetInMemoryLogsImpl.kt | 17 ++ .../impl/PutInMemoryLogsEnabledImpl.kt | 18 ++ .../common/util/withLogTimeOfFirstEvent.kt | 2 +- .../keyguard/core/session/usecase/SubDI.kt | 7 +- .../keyguard/core/store/DatabaseManager.kt | 23 +- .../home/settings/SettingPaneContent.kt | 3 + .../home/settings/component/SettingLogs.kt | 66 ++++ .../settings/other/OtherSettingsScreen.kt | 1 + .../keyguard/feature/logs/LogsItem.kt | 34 +++ .../keyguard/feature/logs/LogsRoute.kt | 11 + .../keyguard/feature/logs/LogsScreen.kt | 289 ++++++++++++++++++ .../keyguard/feature/logs/LogsState.kt | 32 ++ .../feature/logs/LogsStateProducer.kt | 169 ++++++++++ .../provider/bitwarden/api/SyncEngine.kt | 43 ++- .../keyguard/provider/bitwarden/api/fff.kt | 36 ++- .../usecase/AddCipherOpenedHistoryImpl.kt | 6 +- .../AddCipherUsedAutofillHistoryImpl.kt | 6 +- .../AddCipherUsedPasskeyHistoryImpl.kt | 7 +- .../bitwarden/usecase/ExportLogsImpl.kt | 74 +++++ .../bitwarden/usecase/RemoveAccountById.kt | 3 +- .../bitwarden/usecase/RemoveAccounts.kt | 3 +- .../usecase/internal/AddAccountImpl.kt | 2 +- .../usecase/internal/SyncByTokenImpl.kt | 18 +- .../bitwarden/usecase/util/ModifyDatabase.kt | 2 +- .../bitwarden/usecase/util/RefreshToken.kt | 4 +- .../session/FingerprintRepositoryModule.kt | 2 +- .../artemchep/keyguard/di/GlobalModuleJvm.kt | 40 ++- 46 files changed, 1173 insertions(+), 87 deletions(-) create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/Log.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/SyncProgress.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepositoryBase.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepositoryChild.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/inmemory/InMemoryLogRepository.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/inmemory/InMemoryLogRepositoryImpl.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/ExportLogs.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetInMemoryLogs.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetInMemoryLogsEnabled.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutInMemoryLogsEnabled.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetInMemoryLogsEnabledImpl.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetInMemoryLogsImpl.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutInMemoryLogsEnabledImpl.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/component/SettingLogs.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsItem.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsRoute.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsScreen.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsState.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsStateProducer.kt create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/ExportLogsImpl.kt diff --git a/common/src/androidMain/kotlin/com/artemchep/keyguard/copy/LogRepositoryAndroid.kt b/common/src/androidMain/kotlin/com/artemchep/keyguard/copy/LogRepositoryAndroid.kt index 516375fc..0e0fd412 100644 --- a/common/src/androidMain/kotlin/com/artemchep/keyguard/copy/LogRepositoryAndroid.kt +++ b/common/src/androidMain/kotlin/com/artemchep/keyguard/copy/LogRepositoryAndroid.kt @@ -1,27 +1,21 @@ package com.artemchep.keyguard.copy import android.util.Log -import com.artemchep.keyguard.common.io.attempt -import com.artemchep.keyguard.common.io.ioEffect -import com.artemchep.keyguard.common.io.launchIn import com.artemchep.keyguard.common.service.logging.LogLevel -import com.artemchep.keyguard.common.service.logging.LogRepository -import kotlinx.coroutines.GlobalScope +import com.artemchep.keyguard.common.service.logging.LogRepositoryChild -class LogRepositoryAndroid() : LogRepository { - override fun post( +class LogRepositoryAndroid( +) : LogRepositoryChild { + override suspend fun add( tag: String, message: String, level: LogLevel, ) { - add(tag, message, level).attempt().launchIn(GlobalScope) - } - - override fun add( - tag: String, - message: String, - level: LogLevel, - ) = ioEffect { - Log.d(tag, message) + when (level) { + LogLevel.DEBUG -> Log.d(tag, message) + LogLevel.INFO -> Log.i(tag, message) + LogLevel.WARNING -> Log.w(tag, message) + LogLevel.ERROR -> Log.e(tag, message) + } } } diff --git a/common/src/androidMain/kotlin/com/artemchep/keyguard/core/session/FingerprintRepositoryModule.kt b/common/src/androidMain/kotlin/com/artemchep/keyguard/core/session/FingerprintRepositoryModule.kt index 8c01c339..6b54acf3 100644 --- a/common/src/androidMain/kotlin/com/artemchep/keyguard/core/session/FingerprintRepositoryModule.kt +++ b/common/src/androidMain/kotlin/com/artemchep/keyguard/core/session/FingerprintRepositoryModule.kt @@ -236,7 +236,7 @@ fun diFingerprintRepositoryModule() = DI.Module( deviceEncryptionKeyUseCase = instance(), ) } - bindSingleton { + bindSingleton { LogRepositoryAndroid() } } diff --git a/common/src/commonMain/composeResources/values/strings.xml b/common/src/commonMain/composeResources/values/strings.xml index e897154f..471d74fd 100644 --- a/common/src/commonMain/composeResources/values/strings.xml +++ b/common/src/commonMain/composeResources/values/strings.xml @@ -1023,6 +1023,7 @@ Show the content of the app when switching between recent apps and allow capturing a screen Hide the content of the app when switching between recent apps and forbid capturing a screen Data safety + Logs Lock vault Lock after a reboot Lock the vault after a device reboot @@ -1150,4 +1151,10 @@ Remote data Remote data is stored on Bitwarden servers with zero-knowledge encryption. + Logs + Start recording + Stop recording + Export + Export complete + diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/Log.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/Log.kt new file mode 100644 index 00000000..144c5d4a --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/Log.kt @@ -0,0 +1,11 @@ +package com.artemchep.keyguard.common.model + +import com.artemchep.keyguard.common.service.logging.LogLevel +import kotlinx.datetime.Instant + +data class Log( + val tag: String, + val message: String, + val level: LogLevel, + val createdAt: Instant, +) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/SyncProgress.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/SyncProgress.kt new file mode 100644 index 00000000..6bf0216c --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/SyncProgress.kt @@ -0,0 +1,18 @@ +package com.artemchep.keyguard.common.model + +data class SyncProgress( + val title: String, + val progress: Progress? = null, +) { + data class Progress( + val at: Int, + val total: Int, + ) +} + +interface SyncScope { + suspend fun post( + title: String, + progress: SyncProgress.Progress? = null, + ) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogLevel.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogLevel.kt index aeccfe53..f97426ae 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogLevel.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogLevel.kt @@ -1,8 +1,10 @@ package com.artemchep.keyguard.common.service.logging -enum class LogLevel { - DEBUG, - INFO, - WARNING, - ERROR, +enum class LogLevel( + val letter: String, +) { + DEBUG("D"), + INFO("I"), + WARNING("W"), + ERROR("E"), } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepository.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepository.kt index 9adfea87..79edf8ae 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepository.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepository.kt @@ -1,21 +1,10 @@ package com.artemchep.keyguard.common.service.logging -import com.artemchep.keyguard.common.io.IO import com.artemchep.keyguard.platform.util.isRelease +import org.kodein.di.DirectDI +import org.kodein.di.allInstances -interface LogRepository { - fun post( - tag: String, - message: String, - level: LogLevel = LogLevel.DEBUG, - ) - - fun add( - tag: String, - message: String, - level: LogLevel = LogLevel.DEBUG, - ): IO -} +interface LogRepository : LogRepositoryBase /** * A version that only exists in debug and gets completely @@ -30,3 +19,41 @@ inline fun LogRepository.postDebug( post(tag, msg, level = LogLevel.DEBUG) } } + +class LogRepositoryBridge( + private val logRepositoryList: List, +) : LogRepository { + constructor( + directDI: DirectDI, + ) : this( + logRepositoryList = directDI.allInstances(), + ) + + override fun post( + tag: String, + message: String, + level: LogLevel, + ) { + logRepositoryList.forEach { repo -> + repo.post( + tag = tag, + message = message, + level = level, + ) + } + } + + override suspend fun add( + tag: String, + message: String, + level: LogLevel, + ) { + logRepositoryList.forEach { repo -> + repo.add( + tag = tag, + message = message, + level = level, + ) + } + } +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepositoryBase.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepositoryBase.kt new file mode 100644 index 00000000..2f3dbea0 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepositoryBase.kt @@ -0,0 +1,24 @@ +package com.artemchep.keyguard.common.service.logging + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +interface LogRepositoryBase { + fun post( + tag: String, + message: String, + level: LogLevel = LogLevel.DEBUG, + ) { + GlobalScope.launch { + runCatching { + add(tag, message, level) + } + } + } + + suspend fun add( + tag: String, + message: String, + level: LogLevel = LogLevel.DEBUG, + ) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepositoryChild.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepositoryChild.kt new file mode 100644 index 00000000..9ef6e966 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/LogRepositoryChild.kt @@ -0,0 +1,3 @@ +package com.artemchep.keyguard.common.service.logging + +interface LogRepositoryChild : LogRepositoryBase diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/inmemory/InMemoryLogRepository.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/inmemory/InMemoryLogRepository.kt new file mode 100644 index 00000000..4c4e0d31 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/inmemory/InMemoryLogRepository.kt @@ -0,0 +1,17 @@ +package com.artemchep.keyguard.common.service.logging.inmemory + +import com.artemchep.keyguard.common.io.IO +import com.artemchep.keyguard.common.model.Log +import com.artemchep.keyguard.common.service.logging.LogRepositoryChild +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.Flow + +interface InMemoryLogRepository : LogRepositoryChild { + val isEnabled: Boolean + + fun setEnabled(enabled: Boolean): IO + + fun getEnabled(): Flow + + fun get(): Flow> +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/inmemory/InMemoryLogRepositoryImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/inmemory/InMemoryLogRepositoryImpl.kt new file mode 100644 index 00000000..c608b582 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/inmemory/InMemoryLogRepositoryImpl.kt @@ -0,0 +1,62 @@ +package com.artemchep.keyguard.common.service.logging.inmemory + +import com.artemchep.keyguard.common.io.IO +import com.artemchep.keyguard.common.io.ioEffect +import com.artemchep.keyguard.common.model.Log +import com.artemchep.keyguard.common.service.logging.LogLevel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.datetime.Clock +import org.kodein.di.DirectDI + +class InMemoryLogRepositoryImpl( +) : InMemoryLogRepository { + companion object { + private const val DEFAULT_ENABLED = false + } + + private val switchSink = MutableStateFlow(DEFAULT_ENABLED) + + private val logsSink = MutableStateFlow(persistentListOf()) + + override val isEnabled: Boolean get() = switchSink.value + + constructor( + directDI: DirectDI, + ) : this() + + override fun setEnabled(enabled: Boolean): IO = ioEffect { + switchSink.value = enabled + if (!enabled) { + logsSink.value = persistentListOf() + } + } + + override fun getEnabled(): Flow = switchSink + + override fun get(): Flow> = logsSink + + override suspend fun add( + tag: String, + message: String, + level: LogLevel, + ) { + if (!isEnabled) { + return + } + + val now = Clock.System.now() + val entity = Log( + tag = tag, + message = message, + level = level, + createdAt = now, + ) + logsSink.update { logs -> + logs.add(entity) + } + } +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/kotlin/LogRepositoryKotlin.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/kotlin/LogRepositoryKotlin.kt index 49c11f0d..f44006d3 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/kotlin/LogRepositoryKotlin.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/logging/kotlin/LogRepositoryKotlin.kt @@ -1,26 +1,14 @@ package com.artemchep.keyguard.common.service.logging.kotlin -import com.artemchep.keyguard.common.io.attempt -import com.artemchep.keyguard.common.io.ioEffect -import com.artemchep.keyguard.common.io.launchIn import com.artemchep.keyguard.common.service.logging.LogLevel -import com.artemchep.keyguard.common.service.logging.LogRepository -import kotlinx.coroutines.GlobalScope +import com.artemchep.keyguard.common.service.logging.LogRepositoryChild -class LogRepositoryKotlin : LogRepository { - override fun post( +class LogRepositoryKotlin : LogRepositoryChild { + override suspend fun add( tag: String, message: String, level: LogLevel, ) { - add(tag, message, level).attempt().launchIn(GlobalScope) - } - - override fun add( - tag: String, - message: String, - level: LogLevel, - ) = ioEffect { - println("$tag: $message") + println("[${level.letter}]/$tag: $message") } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/permission/PermissionState.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/permission/PermissionState.kt index 82a25189..39376dcf 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/permission/PermissionState.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/permission/PermissionState.kt @@ -1,10 +1,14 @@ package com.artemchep.keyguard.common.service.permission +import androidx.compose.runtime.Immutable import com.artemchep.keyguard.platform.LeContext +@Immutable sealed interface PermissionState { + @Immutable data object Granted : PermissionState + @Immutable data class Declined( val ask: (LeContext) -> Unit, ) : PermissionState diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/ExportLogs.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/ExportLogs.kt new file mode 100644 index 00000000..a0ff2345 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/ExportLogs.kt @@ -0,0 +1,6 @@ +package com.artemchep.keyguard.common.usecase + +import com.artemchep.keyguard.common.io.IO + +interface ExportLogs : ( +) -> IO diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetInMemoryLogs.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetInMemoryLogs.kt new file mode 100644 index 00000000..b7cba2a0 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetInMemoryLogs.kt @@ -0,0 +1,7 @@ +package com.artemchep.keyguard.common.usecase + +import com.artemchep.keyguard.common.model.Log +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.Flow + +interface GetInMemoryLogs : () -> Flow> diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetInMemoryLogsEnabled.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetInMemoryLogsEnabled.kt new file mode 100644 index 00000000..ab26f6be --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetInMemoryLogsEnabled.kt @@ -0,0 +1,8 @@ +package com.artemchep.keyguard.common.usecase + +import kotlinx.coroutines.flow.Flow + +interface GetInMemoryLogsEnabled : () -> Flow { + /** Latest value */ + val value: Boolean +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutInMemoryLogsEnabled.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutInMemoryLogsEnabled.kt new file mode 100644 index 00000000..cc484076 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutInMemoryLogsEnabled.kt @@ -0,0 +1,5 @@ +package com.artemchep.keyguard.common.usecase + +import com.artemchep.keyguard.common.io.IO + +interface PutInMemoryLogsEnabled : (Boolean) -> IO diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/Watchdog.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/Watchdog.kt index cf3bd4a3..45f14075 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/Watchdog.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/Watchdog.kt @@ -5,13 +5,18 @@ import com.artemchep.keyguard.common.io.bind import com.artemchep.keyguard.common.io.ioEffect import com.artemchep.keyguard.common.model.AccountId import com.artemchep.keyguard.common.model.AccountTask +import com.artemchep.keyguard.common.service.logging.LogRepository import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.persistentMapOf +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext +import org.kodein.di.DirectDI +import org.kodein.di.instance interface Watchdog { fun track( @@ -37,21 +42,46 @@ interface SupervisorRead { fun get(accountTask: AccountTask): Flow> } -class WatchdogImpl() : Watchdog, SupervisorRead { +class WatchdogImpl( + private val logRepository: LogRepository, +) : Watchdog, SupervisorRead { + companion object { + private const val TAG = "Watchdog" + } + private val sink = MutableStateFlow( value = persistentMapOf>(), ) + constructor(directDI: DirectDI) : this( + logRepository = directDI.instance(), + ) + override fun track( accountIdSet: Set, accountTask: AccountTask, io: IO, ): IO = ioEffect { + val ids = accountIdSet + .joinToString { it.id } + logRepository.add( + tag = TAG, + message = "Adding '$accountTask' marker to accounts: $ids", + ) try { updateState(accountIdSet, accountTask, Int::inc) io.bind() } finally { - updateState(accountIdSet, accountTask, Int::dec) + try { + withContext(NonCancellable) { + logRepository.add( + tag = TAG, + message = "Removing '$accountTask' marker from accounts: $ids", + ) + } + } finally { + updateState(accountIdSet, accountTask, Int::dec) + } } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetInMemoryLogsEnabledImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetInMemoryLogsEnabledImpl.kt new file mode 100644 index 00000000..3d58b5e4 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetInMemoryLogsEnabledImpl.kt @@ -0,0 +1,21 @@ +package com.artemchep.keyguard.common.usecase.impl + +import com.artemchep.keyguard.common.service.logging.inmemory.InMemoryLogRepository +import com.artemchep.keyguard.common.usecase.GetInMemoryLogsEnabled +import kotlinx.coroutines.flow.Flow +import org.kodein.di.DirectDI +import org.kodein.di.instance + +class GetInMemoryLogsEnabledImpl( + private val inMemoryLogRepository: InMemoryLogRepository, +) : GetInMemoryLogsEnabled { + constructor(directDI: DirectDI) : this( + inMemoryLogRepository = directDI.instance(), + ) + + override val value: Boolean + get() = inMemoryLogRepository.isEnabled + + override fun invoke(): Flow = inMemoryLogRepository + .getEnabled() +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetInMemoryLogsImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetInMemoryLogsImpl.kt new file mode 100644 index 00000000..f83dd244 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetInMemoryLogsImpl.kt @@ -0,0 +1,17 @@ +package com.artemchep.keyguard.common.usecase.impl + +import com.artemchep.keyguard.common.service.logging.inmemory.InMemoryLogRepository +import com.artemchep.keyguard.common.usecase.GetInMemoryLogs +import org.kodein.di.DirectDI +import org.kodein.di.instance + +class GetInMemoryLogsImpl( + private val inMemoryLogRepository: InMemoryLogRepository, +) : GetInMemoryLogs { + constructor(directDI: DirectDI) : this( + inMemoryLogRepository = directDI.instance(), + ) + + override fun invoke() = inMemoryLogRepository + .get() +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutInMemoryLogsEnabledImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutInMemoryLogsEnabledImpl.kt new file mode 100644 index 00000000..27c0ce44 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutInMemoryLogsEnabledImpl.kt @@ -0,0 +1,18 @@ +package com.artemchep.keyguard.common.usecase.impl + +import com.artemchep.keyguard.common.io.IO +import com.artemchep.keyguard.common.service.logging.inmemory.InMemoryLogRepository +import com.artemchep.keyguard.common.usecase.PutInMemoryLogsEnabled +import org.kodein.di.DirectDI +import org.kodein.di.instance + +class PutInMemoryLogsEnabledImpl( + private val inMemoryLogRepository: InMemoryLogRepository, +) : PutInMemoryLogsEnabled { + constructor(directDI: DirectDI) : this( + inMemoryLogRepository = directDI.instance(), + ) + + override fun invoke(inMemoryLogsEnabled: Boolean): IO = inMemoryLogRepository + .setEnabled(inMemoryLogsEnabled) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/util/withLogTimeOfFirstEvent.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/util/withLogTimeOfFirstEvent.kt index 5c7c14f3..368d9875 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/util/withLogTimeOfFirstEvent.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/util/withLogTimeOfFirstEvent.kt @@ -28,7 +28,7 @@ inline fun Flow.withLogTimeOfFirstEvent( } else { "" } - "It took ${dt}ms. to load first portion of data. $suffix" + "It took ${dt}. to load first portion of data. $suffix" } } }, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/session/usecase/SubDI.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/session/usecase/SubDI.kt index 153328cd..b7572ecb 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/session/usecase/SubDI.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/session/usecase/SubDI.kt @@ -68,6 +68,7 @@ import com.artemchep.keyguard.common.usecase.CopyCipherById import com.artemchep.keyguard.common.usecase.DownloadAttachment import com.artemchep.keyguard.common.usecase.EditWordlist import com.artemchep.keyguard.common.usecase.ExportAccount +import com.artemchep.keyguard.common.usecase.ExportLogs import com.artemchep.keyguard.common.usecase.FavouriteCipherById import com.artemchep.keyguard.common.usecase.GetAccountHasError import com.artemchep.keyguard.common.usecase.GetAccountStatus @@ -198,6 +199,7 @@ import com.artemchep.keyguard.provider.bitwarden.usecase.CipherUnsecureUrlAutoFi import com.artemchep.keyguard.provider.bitwarden.usecase.CipherUnsecureUrlCheckImpl import com.artemchep.keyguard.provider.bitwarden.usecase.CopyCipherByIdImpl import com.artemchep.keyguard.provider.bitwarden.usecase.ExportAccountImpl +import com.artemchep.keyguard.provider.bitwarden.usecase.ExportLogsImpl import com.artemchep.keyguard.provider.bitwarden.usecase.FavouriteCipherByIdImpl import com.artemchep.keyguard.provider.bitwarden.usecase.GetAccountHasErrorImpl import com.artemchep.keyguard.provider.bitwarden.usecase.GetAccountsHasErrorImpl @@ -534,6 +536,9 @@ fun DI.Builder.createSubDi2( bindSingleton { ExportAccountImpl(this) } + bindSingleton { + ExportLogsImpl(this) + } bindSingleton { PutAccountColorByIdImpl(this) } @@ -683,7 +688,7 @@ fun DI.Builder.createSubDi2( SyncByTokenImpl(this) } bindSingleton { - WatchdogImpl() + WatchdogImpl(this) } bindSingleton { instance() diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/DatabaseManager.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/DatabaseManager.kt index 413af1c9..5322aea6 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/DatabaseManager.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/DatabaseManager.kt @@ -13,6 +13,7 @@ import com.artemchep.keyguard.common.io.retry import com.artemchep.keyguard.common.io.shared import com.artemchep.keyguard.common.model.MasterKey import com.artemchep.keyguard.common.service.logging.LogRepository +import com.artemchep.keyguard.common.usecase.WatchdogImpl import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher import com.artemchep.keyguard.core.store.bitwarden.BitwardenCollection import com.artemchep.keyguard.core.store.bitwarden.BitwardenFolder @@ -41,8 +42,10 @@ import com.artemchep.keyguard.data.pwnage.AccountBreach import com.artemchep.keyguard.data.pwnage.PasswordBreach import com.artemchep.keyguard.provider.bitwarden.entity.HibpBreachGroup import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import kotlinx.datetime.Instant import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer @@ -53,6 +56,7 @@ interface DatabaseManager { fun get(): IO fun mutate( + tag: String, block: suspend (Database) -> T, ): IO @@ -193,11 +197,28 @@ class DatabaseManagerImpl( override fun get() = dbIo.map { it.database } override fun mutate( + tag: String, block: suspend (Database) -> T, ) = dbIo .effectMap(Dispatchers.IO) { db -> - mutex.withLock { + logRepository.add( + tag = TAG, + message = "Adding '$tag' database lock.", + ) + mutex.lock() + try { block(db.database) + } finally { + try { + withContext(NonCancellable) { + logRepository.add( + tag = TAG, + message = "Removing '$tag' database lock.", + ) + } + } finally { + mutex.unlock() + } } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingPaneContent.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingPaneContent.kt index c3a05cd8..2e874147 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingPaneContent.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingPaneContent.kt @@ -63,6 +63,7 @@ import com.artemchep.keyguard.feature.home.settings.component.settingKeepScreenO import com.artemchep.keyguard.feature.home.settings.component.settingLaunchAppPicker import com.artemchep.keyguard.feature.home.settings.component.settingLaunchYubiKey import com.artemchep.keyguard.feature.home.settings.component.settingLocalizationProvider +import com.artemchep.keyguard.feature.home.settings.component.settingLogsProvider import com.artemchep.keyguard.feature.home.settings.component.settingMarkdownProvider import com.artemchep.keyguard.feature.home.settings.component.settingMasterPasswordProvider import com.artemchep.keyguard.feature.home.settings.component.settingNavAnimationProvider @@ -167,6 +168,7 @@ object Setting { const val LAUNCH_APP_PICKER = "launch_app_picker" const val LAUNCH_YUBIKEY = "launch_yubikey" const val DATA_SAFETY = "data_safety" + const val LOGS = "logs" const val FEATURES_OVERVIEW = "features_overview" const val URL_OVERRIDE = "url_override" const val RATE_APP = "rate_app" @@ -253,6 +255,7 @@ val hub = mapOf SettingComponent>( Setting.LAUNCH_YUBIKEY to ::settingLaunchYubiKey, Setting.LAUNCH_APP_PICKER to ::settingLaunchAppPicker, Setting.DATA_SAFETY to ::settingDataSafetyProvider, + Setting.LOGS to ::settingLogsProvider, Setting.FEATURES_OVERVIEW to ::settingFeaturesOverviewProvider, Setting.URL_OVERRIDE to ::settingUrlOverrideProvider, Setting.RATE_APP to ::settingRateAppProvider, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/component/SettingLogs.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/component/SettingLogs.kt new file mode 100644 index 00000000..c788ce37 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/component/SettingLogs.kt @@ -0,0 +1,66 @@ +package com.artemchep.keyguard.feature.home.settings.component + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import com.artemchep.keyguard.feature.logs.LogsRoute +import com.artemchep.keyguard.feature.navigation.LocalNavigationController +import com.artemchep.keyguard.feature.navigation.NavigationIntent +import com.artemchep.keyguard.res.Res +import com.artemchep.keyguard.res.* +import com.artemchep.keyguard.ui.FlatItem +import com.artemchep.keyguard.ui.icons.ChevronIcon +import com.artemchep.keyguard.ui.icons.Stub +import com.artemchep.keyguard.ui.icons.icon +import org.jetbrains.compose.resources.stringResource +import kotlinx.coroutines.flow.flowOf +import org.kodein.di.DirectDI + +fun settingLogsProvider( + directDI: DirectDI, +) = settingLogsProvider() + +fun settingLogsProvider(): SettingComponent = kotlin.run { + val item = SettingIi( + search = SettingIi.Search( + group = "about", + tokens = listOf( + "logs", + "debug", + "crash", + ), + ), + ) { + val navigationController by rememberUpdatedState(LocalNavigationController.current) + SettingLogs( + onClick = { + val intent = NavigationIntent.NavigateToRoute( + route = LogsRoute, + ) + navigationController.queue(intent) + }, + ) + } + flowOf(item) +} + +@Composable +private fun SettingLogs( + onClick: (() -> Unit)?, +) { + FlatItem( + leading = icon(Icons.Stub), + trailing = { + ChevronIcon() + }, + title = { + Text( + text = stringResource(Res.string.pref_item_logs_title), + ) + }, + onClick = onClick, + ) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/other/OtherSettingsScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/other/OtherSettingsScreen.kt index 3b9bd0c0..3da65faf 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/other/OtherSettingsScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/other/OtherSettingsScreen.kt @@ -32,6 +32,7 @@ fun OtherSettingsScreen() { SettingPaneItem.Item(Setting.CRASHLYTICS), SettingPaneItem.Item(Setting.DATA_SAFETY), SettingPaneItem.Item(Setting.PERMISSION_DETAILS), + SettingPaneItem.Item(Setting.LOGS), ), ), SettingPaneItem.Group( diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsItem.kt new file mode 100644 index 00000000..9c87dacc --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsItem.kt @@ -0,0 +1,34 @@ +package com.artemchep.keyguard.feature.logs + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.AnnotatedString +import arrow.optics.optics +import com.artemchep.keyguard.common.service.logging.LogLevel +import java.util.UUID + +@Immutable +@optics +sealed interface LogsItem { + companion object + + val id: String + + @Immutable + data class Section( + override val id: String = UUID.randomUUID().toString(), + val text: String? = null, + val caps: Boolean = true, + ) : LogsItem { + companion object + } + + @Immutable + data class Value( + override val id: String, + val text: AnnotatedString, + val level: LogLevel, + val time: String, + ) : LogsItem { + companion object; + } +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsRoute.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsRoute.kt new file mode 100644 index 00000000..1d4c3f9f --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsRoute.kt @@ -0,0 +1,11 @@ +package com.artemchep.keyguard.feature.logs + +import androidx.compose.runtime.Composable +import com.artemchep.keyguard.feature.navigation.Route + +object LogsRoute : Route { + @Composable + override fun Content() { + LogsScreen() + } +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsScreen.kt new file mode 100644 index 00000000..6b2c1ae8 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsScreen.kt @@ -0,0 +1,289 @@ +package com.artemchep.keyguard.feature.logs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.PlayArrow +import androidx.compose.material.icons.outlined.SaveAlt +import androidx.compose.material.icons.outlined.Stop +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import com.artemchep.keyguard.common.model.fold +import com.artemchep.keyguard.common.service.logging.LogLevel +import com.artemchep.keyguard.feature.EmptyView +import com.artemchep.keyguard.feature.home.vault.component.Section +import com.artemchep.keyguard.feature.navigation.NavigationIcon +import com.artemchep.keyguard.res.Res +import com.artemchep.keyguard.res.* +import com.artemchep.keyguard.ui.DefaultFab +import com.artemchep.keyguard.ui.FabState +import com.artemchep.keyguard.ui.FlatItemLayout +import com.artemchep.keyguard.ui.MediumEmphasisAlpha +import com.artemchep.keyguard.ui.ScaffoldLazyColumn +import com.artemchep.keyguard.ui.skeleton.SkeletonItem +import com.artemchep.keyguard.ui.theme.Dimens +import com.artemchep.keyguard.ui.theme.combineAlpha +import com.artemchep.keyguard.ui.theme.infoContainer +import com.artemchep.keyguard.ui.theme.onInfoContainer +import com.artemchep.keyguard.ui.theme.onWarningContainer +import com.artemchep.keyguard.ui.theme.warningContainer +import com.artemchep.keyguard.ui.toolbar.LargeToolbar +import com.artemchep.keyguard.ui.toolbar.util.ToolbarBehavior +import org.jetbrains.compose.resources.stringResource + +@Composable +fun LogsScreen() { + val modifier = Modifier + val scrollBehavior = ToolbarBehavior.behavior() + + val loadableState = produceLogsState() + loadableState.fold( + ifLoading = { + LogsScreenSkeleton( + modifier = modifier, + scrollBehavior = scrollBehavior, + ) + }, + ifOk = { state -> + LogsScreenContent( + modifier = modifier, + scrollBehavior = scrollBehavior, + state = state, + ) + }, + ) +} + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalMaterialApi::class, +) +@Composable +private fun LogsScreenSkeleton( + modifier: Modifier, + scrollBehavior: TopAppBarScrollBehavior, +) { + ScaffoldLazyColumn( + modifier = modifier + .nestedScroll(scrollBehavior.nestedScrollConnection), + topAppBarScrollBehavior = scrollBehavior, + topBar = { + LargeToolbar( + title = { + Text(stringResource(Res.string.logs_header_title)) + }, + navigationIcon = { + NavigationIcon() + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { + item("skeleton") { + SkeletonItem() + } + } +} + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalMaterialApi::class, +) +@Composable +private fun LogsScreenContent( + modifier: Modifier, + scrollBehavior: TopAppBarScrollBehavior, + state: LogsState, +) { + val contentState = state.contentFlow.collectAsState() + ScaffoldLazyColumn( + modifier = modifier + .nestedScroll(scrollBehavior.nestedScrollConnection), + topAppBarScrollBehavior = scrollBehavior, + topBar = { + LargeToolbar( + title = { + Text(stringResource(Res.string.logs_header_title)) + }, + navigationIcon = { + NavigationIcon() + }, + actions = { + val exportState = state.exportFlow.collectAsState() + TextButton( + enabled = exportState.value.onExportClick != null, + onClick = { + exportState.value.onExportClick?.invoke() + }, + ) { + Icon( + imageVector = Icons.Outlined.SaveAlt, + contentDescription = null, + ) + Spacer( + modifier = Modifier + .width(Dimens.buttonIconPadding), + ) + Text( + text = stringResource(Res.string.logs_export_button), + ) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + floatingActionState = run { + val switchState = state.switchFlow.collectAsState() + val fabState = FabState( + onClick = switchState.value.onToggle, + model = switchState.value.checked, + ) + rememberUpdatedState(fabState) + }, + floatingActionButton = { + val checked = this.state.value?.model == true + DefaultFab( + icon = { + Icon( + imageVector = if (!checked) { + Icons.Outlined.PlayArrow + } else { + Icons.Outlined.Stop + }, + contentDescription = null, + ) + }, + ) { + Text( + text = if (!checked) { + stringResource(Res.string.logs_start_recording_fab_title) + } else { + stringResource(Res.string.logs_stop_recording_fab_title) + }, + ) + } + }, + ) { + val items = contentState.value.items + if (items.isEmpty()) { + item("empty") { + EmptyView() + } + } + items(items, key = { it.id }) { item -> + LogItem( + modifier = Modifier + .animateItemPlacement(), + item = item, + ) + } + } +} + +@Composable +private fun LogItem( + modifier: Modifier, + item: LogsItem, +) = when (item) { + is LogsItem.Section -> LogItem( + modifier = modifier, + item = item, + ) + + is LogsItem.Value -> LogItem( + modifier = modifier, + item = item, + ) +} + +@Composable +private fun LogItem( + modifier: Modifier, + item: LogsItem.Section, +) { + Section( + modifier = modifier, + text = item.text, + caps = item.caps, + ) +} + +@Composable +private fun LogItem( + modifier: Modifier, + item: LogsItem.Value, +) { + FlatItemLayout( + modifier = modifier, + content = { + Row { + val levelContainerColor = when (item.level) { + LogLevel.ERROR -> MaterialTheme.colorScheme.errorContainer + LogLevel.WARNING -> MaterialTheme.colorScheme.warningContainer + LogLevel.INFO -> MaterialTheme.colorScheme.infoContainer + LogLevel.DEBUG -> MaterialTheme.colorScheme.surfaceContainer + } + val levelContentColor = when (item.level) { + LogLevel.ERROR -> MaterialTheme.colorScheme.onErrorContainer + LogLevel.WARNING -> MaterialTheme.colorScheme.onWarningContainer + LogLevel.INFO -> MaterialTheme.colorScheme.onInfoContainer + LogLevel.DEBUG -> MaterialTheme.colorScheme.onSurface + } + Box( + modifier = Modifier + .background(levelContainerColor, RoundedCornerShape(4.dp)) + .size(24.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = item.level.letter, + color = levelContentColor, + style = MaterialTheme.typography.labelSmall, + ) + } + + Spacer( + modifier = Modifier + .width(16.dp), + ) + + Column { + Text( + text = item.text, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + ) + Text( + text = item.time, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = LocalContentColor.current + .combineAlpha(MediumEmphasisAlpha), + ) + } + } + }, + enabled = true, + ) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsState.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsState.kt new file mode 100644 index 00000000..1b67c7ff --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsState.kt @@ -0,0 +1,32 @@ +@file:JvmName("GeneratorStateUtils") + +package com.artemchep.keyguard.feature.logs + +import androidx.compose.runtime.Immutable +import com.artemchep.keyguard.common.service.permission.PermissionState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.StateFlow + +@Immutable +data class LogsState( + val contentFlow: StateFlow, + val exportFlow: StateFlow, + val switchFlow: StateFlow, +) { + @Immutable + data class Content( + val items: ImmutableList, + ) + + @Immutable + data class Export( + val writePermission: PermissionState, + val onExportClick: (() -> Unit)? = null, + ) + + @Immutable + data class Switch( + val checked: Boolean, + val onToggle: (() -> Unit)? = null, + ) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsStateProducer.kt new file mode 100644 index 00000000..75ce2d87 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/logs/LogsStateProducer.kt @@ -0,0 +1,169 @@ +package com.artemchep.keyguard.feature.logs + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import arrow.core.partially1 +import com.artemchep.keyguard.common.io.effectTap +import com.artemchep.keyguard.common.io.launchIn +import com.artemchep.keyguard.common.model.Loadable +import com.artemchep.keyguard.common.model.ToastMessage +import com.artemchep.keyguard.common.service.clipboard.ClipboardService +import com.artemchep.keyguard.common.service.permission.Permission +import com.artemchep.keyguard.common.service.permission.PermissionService +import com.artemchep.keyguard.common.service.permission.PermissionState +import com.artemchep.keyguard.common.usecase.DateFormatter +import com.artemchep.keyguard.common.usecase.ExportLogs +import com.artemchep.keyguard.common.usecase.GetGeneratorHistory +import com.artemchep.keyguard.common.usecase.GetInMemoryLogs +import com.artemchep.keyguard.common.usecase.GetInMemoryLogsEnabled +import com.artemchep.keyguard.common.usecase.PutInMemoryLogsEnabled +import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistory +import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistoryById +import com.artemchep.keyguard.feature.navigation.state.produceScreenState +import com.artemchep.keyguard.res.Res +import com.artemchep.keyguard.res.* +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import org.kodein.di.compose.localDI +import org.kodein.di.direct +import org.kodein.di.instance + +private const val MESSAGE_LENGTH_LIMIT = 300 + +@Composable +fun produceLogsState() = with(localDI().direct) { + produceLogsState( + getGeneratorHistory = instance(), + removeGeneratorHistory = instance(), + removeGeneratorHistoryById = instance(), + dateFormatter = instance(), + clipboardService = instance(), + getInMemoryLogs = instance(), + getInMemoryLogsEnabled = instance(), + putInMemoryLogsEnabled = instance(), + permissionService = instance(), + exportLogs = instance(), + ) +} + +@Composable +fun produceLogsState( + getGeneratorHistory: GetGeneratorHistory, + removeGeneratorHistory: RemoveGeneratorHistory, + removeGeneratorHistoryById: RemoveGeneratorHistoryById, + dateFormatter: DateFormatter, + clipboardService: ClipboardService, + getInMemoryLogs: GetInMemoryLogs, + getInMemoryLogsEnabled: GetInMemoryLogsEnabled, + putInMemoryLogsEnabled: PutInMemoryLogsEnabled, + permissionService: PermissionService, + exportLogs: ExportLogs, +): Loadable = produceScreenState( + initial = Loadable.Loading, + key = "generator_history", + args = arrayOf( + getGeneratorHistory, + removeGeneratorHistory, + removeGeneratorHistoryById, + dateFormatter, + clipboardService, + ), +) { + fun onExport( + ) { + exportLogs() + .effectTap { + val msg = ToastMessage( + title = translate(Res.string.logs_export_success), + type = ToastMessage.Type.SUCCESS, + ) + message(msg) + } + .launchIn(appScope) + } + + val writeDownloadsPermissionFlow = permissionService + .getState(Permission.WRITE_EXTERNAL_STORAGE) + + val itemsRawFlow = getInMemoryLogs() + .shareInScreenScope() + val itemsFlow = itemsRawFlow + .map { logs -> + val items = logs + .mapIndexed { index, log -> + val text = buildAnnotatedString { + withStyle( + SpanStyle( + fontWeight = FontWeight.Bold, + ), + ) { + append(log.tag) + } + append(" ") + + // Append only a part of a message if that is + // too long. This is needed because rendering + // huge text will freeze the UI. + if (log.message.length > MESSAGE_LENGTH_LIMIT) { + val part = log.message.take(MESSAGE_LENGTH_LIMIT) + append(part) + append("... [truncated]") + } else { + append(log.message) + } + } + val time = dateFormatter.formatDateTime(log.createdAt) + LogsItem.Value( + id = index.toString(), + text = text, + level = log.level, + time = time, + ) + } + .toPersistentList() + LogsState.Content( + items = items, + ) + } + .stateIn(screenScope) + + val switchFlow = getInMemoryLogsEnabled() + .map { enabled -> + LogsState.Switch( + checked = enabled, + onToggle = { + putInMemoryLogsEnabled(!enabled) + .launchIn(appScope) + }, + ) + } + .stateIn(screenScope) + val exportFlow = writeDownloadsPermissionFlow + .map { writeDownloadsPermission -> + val onExportClick = when (writeDownloadsPermission) { + is PermissionState.Granted -> ::onExport + is PermissionState.Declined -> { + // lambda + writeDownloadsPermission.ask + .partially1(context) + } + } + LogsState.Export( + writePermission = writeDownloadsPermission, + onExportClick = onExportClick, + ) + } + .stateIn(screenScope) + + val state = LogsState( + contentFlow = itemsFlow, + switchFlow = switchFlow, + exportFlow = exportFlow, + ) + flowOf(Loadable.Ok(state)) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt index a8e4d2e4..b7f88af5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt @@ -2,6 +2,7 @@ package com.artemchep.keyguard.provider.bitwarden.api import com.artemchep.keyguard.common.exception.HttpException import com.artemchep.keyguard.common.io.bind +import com.artemchep.keyguard.common.model.SyncScope import com.artemchep.keyguard.common.service.crypto.CipherEncryptor import com.artemchep.keyguard.common.service.crypto.CryptoGenerator import com.artemchep.keyguard.common.service.logging.LogRepository @@ -118,11 +119,15 @@ class SyncEngine( ) } + context(SyncScope) suspend fun sync() = kotlin.run { val env = user.env.back() val api = env.api val token = requireNotNull(user.token).accessToken + post( + title = "Send send request.", + ) val response = api.sync( httpClient = httpClient, env = env, @@ -283,6 +288,10 @@ class SyncEngine( // Profile // + post( + title = "Syncing a profile entity.", + ) + val newProfile = BitwardenProfile .encrypted( accountId = user.id, @@ -345,6 +354,10 @@ class SyncEngine( .transform(this) } + post( + title = "Syncing folder entities.", + ) + val folderDao = db.folderQueries val folderRemoteLens = SyncManager.Lens( getId = { it.id }, @@ -476,7 +489,7 @@ class SyncEngine( ) }, onLog = { msg, logLevel -> - logRepository.post(TAG, "[SyncFolder] $msg", logLevel) + logRepository.add(TAG, msg, logLevel) }, ) @@ -535,6 +548,10 @@ class SyncEngine( .transform(this, codec2) } + post( + title = "Syncing cipher entities.", + ) + val cipherDao = db.cipherQueries val cipherRemoteLens = SyncManager.Lens( getId = { it.id }, @@ -806,7 +823,7 @@ class SyncEngine( } }, onLog = { msg, logLevel -> - logRepository.post(TAG, msg, logLevel) + logRepository.add(TAG, msg, logLevel) }, ) @@ -825,6 +842,10 @@ class SyncEngine( .transform(this) } + post( + title = "Syncing collection entities.", + ) + val collectionDao = db.collectionQueries val collectionRemoteLens = SyncManager.Lens( getId = { it.id }, @@ -915,7 +936,7 @@ class SyncEngine( TODO() }, onLog = { msg, logLevel -> - logRepository.post(TAG, msg, logLevel) + logRepository.add(TAG, msg, logLevel) }, ) @@ -934,6 +955,10 @@ class SyncEngine( .transform(this) } + post( + title = "Syncing organization entities.", + ) + val organizationDao = db.organizationQueries val organizationRemoteLens = SyncManager.Lens( getId = { it.id }, @@ -1020,7 +1045,7 @@ class SyncEngine( TODO() }, onLog = { msg, logLevel -> - logRepository.post(TAG, msg, logLevel) + logRepository.add(TAG, msg, logLevel) }, ) @@ -1044,6 +1069,10 @@ class SyncEngine( .transform(this, codec2) } + post( + title = "Syncing send entities.", + ) + val sendDao = db.sendQueries val sendRemoteLens = SyncManager.Lens( getId = { it.id }, @@ -1217,10 +1246,14 @@ class SyncEngine( ) }, onLog = { msg, logLevel -> - logRepository.post(TAG, msg, logLevel) + logRepository.add(TAG, msg, logLevel) }, ) + post( + title = "Syncing complete.", + ) + Unit } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/fff.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/fff.kt index ff453eda..05a5342a 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/fff.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/fff.kt @@ -11,6 +11,7 @@ import com.artemchep.keyguard.common.io.handleErrorTap import com.artemchep.keyguard.common.io.ioEffect import com.artemchep.keyguard.common.io.measure import com.artemchep.keyguard.common.io.parallel +import com.artemchep.keyguard.common.model.SyncScope import com.artemchep.keyguard.common.service.logging.LogLevel import com.artemchep.keyguard.common.usecase.GetPasswordStrength import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher @@ -107,6 +108,7 @@ interface RemotePutScope { fun updateRemoteModel(remote: Remote) } +context(SyncScope) suspend fun < Local : BitwardenService.Has, LocalDecoded : Any, @@ -130,7 +132,7 @@ suspend fun < remoteDecodedFallback: suspend (Remote, Local?, Throwable) -> RemoteDecoded, remoteDeleteById: suspend (String) -> Unit, remotePut: suspend RemotePutScope.(LocalDecoded) -> RemoteDecoded, - onLog: (String, LogLevel) -> Unit, + onLog: suspend (String, LogLevel) -> Unit, ) { onLog( "[Start] Starting to sync the $name: " + @@ -147,20 +149,36 @@ suspend fun < remoteItems = remoteItems, shouldOverwrite = shouldOverwrite, ) + onLog( + "[Start] Starting to sync the $name: " + + "${localItems.size} local items, " + + "${remoteItems.size} remote items.", + LogLevel.INFO, + ) // // Write changes to local storage as these // are quite fast to do. // - localDeleteById( - df.localDeletedCipherIds - .map { localLens.getLocalId(it.local) }, + val localDeletedCipherIds = df.localDeletedCipherIds + .map { localLens.getLocalId(it.local) } + onLog( + "[local] Deleting ${localDeletedCipherIds.size} $name entries...", + LogLevel.DEBUG, ) + localDeleteById(localDeletedCipherIds) val localPutCipherDecoded = df.localPutCipher .map { (localOrNull, remote) -> - ioEffect { remoteDecoder(remote, localOrNull) } + ioEffect { + val remoteId = remote.let(remoteLens.getId) + onLog( + "[local] Decoding $remoteId $name entry...", + LogLevel.DEBUG, + ) + remoteDecoder(remote, localOrNull) + } .handleError { e -> val remoteId = remoteLens.getId(remote) val localId = localOrNull?.let(localLens.getLocalId) @@ -228,7 +246,13 @@ suspend fun < .map { entry -> val localId = localLens.getLocalId(entry.local) val remoteId = remoteLens.getId(entry.remote) - ioEffect { remoteDeleteById(remoteId) } + ioEffect { + onLog( + "[local] Decoding $remoteId $name entry...", + LogLevel.DEBUG, + ) + remoteDeleteById(remoteId) + } .handleErrorTap { e -> handleFailedToPut(entry.local, e = e) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherOpenedHistoryImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherOpenedHistoryImpl.kt index 14d19c7c..a3feeaf8 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherOpenedHistoryImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherOpenedHistoryImpl.kt @@ -10,12 +10,16 @@ import org.kodein.di.instance class AddCipherOpenedHistoryImpl( private val db: DatabaseManager, ) : AddCipherOpenedHistory { + companion object { + private const val TAG = "AddCipherOpened" + } + constructor(directDI: DirectDI) : this( db = directDI.instance(), ) override fun invoke(request: AddCipherOpenedHistoryRequest) = db - .mutate { + .mutate(TAG) { it.cipherUsageHistoryQueries.insert( cipherId = request.cipherId, credentialId = null, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUsedAutofillHistoryImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUsedAutofillHistoryImpl.kt index 12e0061f..dfa76946 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUsedAutofillHistoryImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUsedAutofillHistoryImpl.kt @@ -10,12 +10,16 @@ import org.kodein.di.instance class AddCipherUsedAutofillHistoryImpl( private val db: DatabaseManager, ) : AddCipherUsedAutofillHistory { + companion object { + private const val TAG = "AddCipherUsedAutofill" + } + constructor(directDI: DirectDI) : this( db = directDI.instance(), ) override fun invoke(request: AddCipherOpenedHistoryRequest) = db - .mutate { + .mutate(TAG) { it.cipherUsageHistoryQueries.insert( cipherId = request.cipherId, credentialId = null, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUsedPasskeyHistoryImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUsedPasskeyHistoryImpl.kt index 166df7cd..bce2ba56 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUsedPasskeyHistoryImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUsedPasskeyHistoryImpl.kt @@ -4,18 +4,23 @@ import com.artemchep.keyguard.common.model.AddCipherUsedPasskeyHistoryRequest import com.artemchep.keyguard.common.model.CipherHistoryType import com.artemchep.keyguard.common.usecase.AddCipherUsedPasskeyHistory import com.artemchep.keyguard.core.store.DatabaseManager +import com.artemchep.keyguard.provider.bitwarden.usecase.internal.SyncByTokenImpl import org.kodein.di.DirectDI import org.kodein.di.instance class AddCipherUsedPasskeyHistoryImpl( private val db: DatabaseManager, ) : AddCipherUsedPasskeyHistory { + companion object { + private const val TAG = "AddCipherUsedPasskey" + } + constructor(directDI: DirectDI) : this( db = directDI.instance(), ) override fun invoke(request: AddCipherUsedPasskeyHistoryRequest) = db - .mutate { + .mutate(TAG) { it.cipherUsageHistoryQueries.insert( cipherId = request.cipherId, credentialId = request.credentialId, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/ExportLogsImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/ExportLogsImpl.kt new file mode 100644 index 00000000..4e1a8e4f --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/ExportLogsImpl.kt @@ -0,0 +1,74 @@ +package com.artemchep.keyguard.provider.bitwarden.usecase + +import com.artemchep.keyguard.common.io.IO +import com.artemchep.keyguard.common.io.bind +import com.artemchep.keyguard.common.io.ioEffect +import com.artemchep.keyguard.common.service.dirs.DirsService +import com.artemchep.keyguard.common.service.zip.ZipConfig +import com.artemchep.keyguard.common.service.zip.ZipEntry +import com.artemchep.keyguard.common.service.zip.ZipService +import com.artemchep.keyguard.common.usecase.DateFormatter +import com.artemchep.keyguard.common.usecase.ExportLogs +import com.artemchep.keyguard.common.usecase.GetInMemoryLogs +import kotlinx.coroutines.flow.first +import kotlinx.datetime.Clock +import org.kodein.di.DirectDI +import org.kodein.di.instance + +/** + * @author Artem Chepurnyi + */ +class ExportLogsImpl( + private val dirsService: DirsService, + private val zipService: ZipService, + private val dateFormatter: DateFormatter, + private val getInMemoryLogs: GetInMemoryLogs, +) : ExportLogs { + companion object { + private const val TAG = "ExportLogs.bitwarden" + } + + constructor(directDI: DirectDI) : this( + dirsService = directDI.instance(), + zipService = directDI.instance(), + dateFormatter = directDI.instance(), + getInMemoryLogs = directDI.instance(), + ) + + override fun invoke( + ): IO = ioEffect { + val logs = getInMemoryLogs() + .first() + + // Map log data to the plain text + val txt = kotlin.run { + logs + .joinToString(separator = "\n") { log -> + val dateTime = dateFormatter.formatDateTimeMachine(log.createdAt) + "$dateTime ${log.level.letter} ${log.tag} ${log.message}" + } + } + + val fileName = kotlin.run { + val now = Clock.System.now() + val dt = dateFormatter.formatDateTimeMachine(now) + "keyguard_logs_$dt.zip" + } + dirsService.saveToDownloads(fileName) { os -> + zipService.zip( + outputStream = os, + config = ZipConfig( + ), + entries = listOf( + ZipEntry( + name = "logs.txt", + stream = { + txt.byteInputStream() + }, + ), + ), + ) + }.bind() + } + +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/RemoveAccountById.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/RemoveAccountById.kt index b3b0daab..24b296c2 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/RemoveAccountById.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/RemoveAccountById.kt @@ -8,6 +8,7 @@ import com.artemchep.keyguard.common.usecase.RemoveAccountById import com.artemchep.keyguard.common.usecase.Watchdog import com.artemchep.keyguard.common.usecase.unit import com.artemchep.keyguard.core.store.DatabaseManager +import com.artemchep.keyguard.provider.bitwarden.usecase.internal.SyncByTokenImpl import org.kodein.di.DirectDI import org.kodein.di.instance @@ -46,7 +47,7 @@ class RemoveAccountByIdImpl( private fun performRemoveAccount( accountId: AccountId, ) = db - .mutate { database -> + .mutate(TAG) { database -> val dao = database.accountQueries dao.deleteByAccountId(accountId.id) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/RemoveAccounts.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/RemoveAccounts.kt index ae8a01d6..71e95cb4 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/RemoveAccounts.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/RemoveAccounts.kt @@ -3,6 +3,7 @@ package com.artemchep.keyguard.provider.bitwarden.usecase import com.artemchep.keyguard.common.io.IO import com.artemchep.keyguard.common.usecase.RemoveAccounts import com.artemchep.keyguard.core.store.DatabaseManager +import com.artemchep.keyguard.provider.bitwarden.usecase.internal.SyncByTokenImpl import org.kodein.di.DirectDI import org.kodein.di.instance @@ -21,7 +22,7 @@ class RemoveAccountsImpl( ) override fun invoke(): IO = db - .mutate { database -> + .mutate(TAG) { database -> val dao = database.accountQueries dao.deleteAll() } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/AddAccountImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/AddAccountImpl.kt index 89415dad..7772365c 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/AddAccountImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/AddAccountImpl.kt @@ -112,7 +112,7 @@ class AddAccountImpl( ), ) - db.mutate { database -> + db.mutate(TAG) { database -> database.accountQueries.insert( accountId = token.id, data = token, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/SyncByTokenImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/SyncByTokenImpl.kt index 1ba3aaeb..06809038 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/SyncByTokenImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/SyncByTokenImpl.kt @@ -9,6 +9,8 @@ import com.artemchep.keyguard.common.io.measure import com.artemchep.keyguard.common.io.toIO import com.artemchep.keyguard.common.model.AccountId import com.artemchep.keyguard.common.model.AccountTask +import com.artemchep.keyguard.common.model.SyncProgress +import com.artemchep.keyguard.common.model.SyncScope import com.artemchep.keyguard.common.service.crypto.CipherEncryptor import com.artemchep.keyguard.common.service.crypto.CryptoGenerator import com.artemchep.keyguard.common.service.logging.LogRepository @@ -70,6 +72,14 @@ class SyncByTokenImpl( accountId = AccountId(user.id), accountTask = AccountTask.SYNC, ) { + val scope = object : SyncScope { + override suspend fun post( + title: String, + progress: SyncProgress.Progress?, + ) { + logRepository.add(TAG, title) + } + } // We want to automatically request a new access token if the old // one has expired. withRefreshableAccessToken( @@ -92,7 +102,9 @@ class SyncByTokenImpl( syncer = dbSyncer, ) mutex.withLock { - syncEngine.sync() + with(scope) { + syncEngine.sync() + } } // sss( // logRepository = logRepository, @@ -107,7 +119,7 @@ class SyncByTokenImpl( } .biFlatTap( ifException = { e -> - db.mutate { + db.mutate(TAG) { val dao = it.metaQueries val existingMeta = dao .getByAccountId(accountId = user.id) @@ -143,7 +155,7 @@ class SyncByTokenImpl( } }, ifSuccess = { - db.mutate { + db.mutate(TAG) { val now = Clock.System.now() val meta = BitwardenMeta( accountId = user.id, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/ModifyDatabase.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/ModifyDatabase.kt index 2c937a2a..34f7e133 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/ModifyDatabase.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/ModifyDatabase.kt @@ -53,7 +53,7 @@ class ModifyDatabase( operator fun invoke( block: suspend (Database) -> Result, ): IO = db - .mutate { database -> + .mutate("ModifyDatabase") { database -> val accountIds = block(database) accountIds } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/RefreshToken.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/RefreshToken.kt index 3fc3377e..7efa566c 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/RefreshToken.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/util/RefreshToken.kt @@ -125,7 +125,7 @@ suspend fun getAndUpdateUserToken( throw IllegalStateException("Help") } - val newUser = db.mutate { + val newUser = db.mutate("RefreshToken") { it.accountQueries .getByAccountId(user.id) .executeAsOneOrNull() @@ -149,7 +149,7 @@ suspend fun getAndUpdateUserToken( expirationDate = login.accessTokenExpiryDate, ) val u = user.copy(token = token) - db.mutate { + db.mutate("RefreshToken") { it.accountQueries.insert( accountId = u.id, data = u, diff --git a/common/src/desktopMain/kotlin/com/artemchep/keyguard/core/session/FingerprintRepositoryModule.kt b/common/src/desktopMain/kotlin/com/artemchep/keyguard/core/session/FingerprintRepositoryModule.kt index 7769b609..6e1fc5b4 100644 --- a/common/src/desktopMain/kotlin/com/artemchep/keyguard/core/session/FingerprintRepositoryModule.kt +++ b/common/src/desktopMain/kotlin/com/artemchep/keyguard/core/session/FingerprintRepositoryModule.kt @@ -309,7 +309,7 @@ fun diFingerprintRepositoryModule() = DI.Module( val m: KeyValueStore = instance(arg = file) m } - bindSingleton { + bindSingleton { LogRepositoryKotlin() } } diff --git a/common/src/jvmMain/kotlin/com/artemchep/keyguard/di/GlobalModuleJvm.kt b/common/src/jvmMain/kotlin/com/artemchep/keyguard/di/GlobalModuleJvm.kt index 579c30ba..dfe95408 100644 --- a/common/src/jvmMain/kotlin/com/artemchep/keyguard/di/GlobalModuleJvm.kt +++ b/common/src/jvmMain/kotlin/com/artemchep/keyguard/di/GlobalModuleJvm.kt @@ -27,7 +27,10 @@ import com.artemchep.keyguard.common.service.justgetmydata.impl.JustGetMyDataSer import com.artemchep.keyguard.common.service.keyvalue.KeyValueStore import com.artemchep.keyguard.common.service.license.LicenseService import com.artemchep.keyguard.common.service.license.impl.LicenseServiceImpl +import com.artemchep.keyguard.common.service.logging.inmemory.InMemoryLogRepository +import com.artemchep.keyguard.common.service.logging.inmemory.InMemoryLogRepositoryImpl import com.artemchep.keyguard.common.service.logging.LogRepository +import com.artemchep.keyguard.common.service.logging.LogRepositoryBridge import com.artemchep.keyguard.common.service.passkey.PassKeyService import com.artemchep.keyguard.common.service.passkey.impl.PassKeyServiceImpl import com.artemchep.keyguard.common.service.placeholder.impl.CipherPlaceholder @@ -103,7 +106,6 @@ import com.artemchep.keyguard.common.usecase.GetAutofillSaveUri import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation import com.artemchep.keyguard.common.usecase.GetBiometricTimeout import com.artemchep.keyguard.common.usecase.GetBiometricTimeoutVariants -import com.artemchep.keyguard.common.usecase.GetBreaches import com.artemchep.keyguard.common.usecase.GetCachePremium import com.artemchep.keyguard.common.usecase.GetCanWrite import com.artemchep.keyguard.common.usecase.GetCheckPasskeys @@ -124,6 +126,8 @@ import com.artemchep.keyguard.common.usecase.GetFont import com.artemchep.keyguard.common.usecase.GetFontVariants import com.artemchep.keyguard.common.usecase.GetGravatar import com.artemchep.keyguard.common.usecase.GetGravatarUrl +import com.artemchep.keyguard.common.usecase.GetInMemoryLogs +import com.artemchep.keyguard.common.usecase.GetInMemoryLogsEnabled import com.artemchep.keyguard.common.usecase.GetJustDeleteMeByUrl import com.artemchep.keyguard.common.usecase.GetJustGetMyDataByUrl import com.artemchep.keyguard.common.usecase.GetKeepScreenOn @@ -184,6 +188,7 @@ import com.artemchep.keyguard.common.usecase.PutDebugPremium import com.artemchep.keyguard.common.usecase.PutDebugScreenDelay import com.artemchep.keyguard.common.usecase.PutFont import com.artemchep.keyguard.common.usecase.PutGravatar +import com.artemchep.keyguard.common.usecase.PutInMemoryLogsEnabled import com.artemchep.keyguard.common.usecase.PutKeepScreenOn import com.artemchep.keyguard.common.usecase.PutMarkdown import com.artemchep.keyguard.common.usecase.PutNavAnimation @@ -235,7 +240,6 @@ import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveUriImpl import com.artemchep.keyguard.common.usecase.impl.GetBiometricRequireConfirmationImpl import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutImpl import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutVariantsImpl -import com.artemchep.keyguard.common.usecase.impl.GetBreachesImpl import com.artemchep.keyguard.common.usecase.impl.GetCachePremiumImpl import com.artemchep.keyguard.common.usecase.impl.GetCanWriteImpl import com.artemchep.keyguard.common.usecase.impl.GetCheckPasskeysImpl @@ -256,6 +260,8 @@ import com.artemchep.keyguard.common.usecase.impl.GetFontImpl import com.artemchep.keyguard.common.usecase.impl.GetFontVariantsImpl import com.artemchep.keyguard.common.usecase.impl.GetGravatarImpl import com.artemchep.keyguard.common.usecase.impl.GetGravatarUrlImpl +import com.artemchep.keyguard.common.usecase.impl.GetInMemoryLogsEnabledImpl +import com.artemchep.keyguard.common.usecase.impl.GetInMemoryLogsImpl import com.artemchep.keyguard.common.usecase.impl.GetJustDeleteMeByUrlImpl import com.artemchep.keyguard.common.usecase.impl.GetJustGetMyDataByUrlImpl import com.artemchep.keyguard.common.usecase.impl.GetKeepScreenOnImpl @@ -313,6 +319,7 @@ import com.artemchep.keyguard.common.usecase.impl.PutDebugPremiumImpl import com.artemchep.keyguard.common.usecase.impl.PutDebugScreenDelayImpl import com.artemchep.keyguard.common.usecase.impl.PutFontImpl import com.artemchep.keyguard.common.usecase.impl.PutGravatarImpl +import com.artemchep.keyguard.common.usecase.impl.PutInMemoryLogsEnabledImpl import com.artemchep.keyguard.common.usecase.impl.PutKeepScreenOnImpl import com.artemchep.keyguard.common.usecase.impl.PutMarkdownImpl import com.artemchep.keyguard.common.usecase.impl.PutNavAnimationImpl @@ -335,10 +342,6 @@ import com.artemchep.keyguard.common.usecase.impl.RemoveAttachmentImpl import com.artemchep.keyguard.common.usecase.impl.RequestAppReviewImpl import com.artemchep.keyguard.common.usecase.impl.UnlockUseCaseImpl import com.artemchep.keyguard.common.usecase.impl.UpdateVersionLogImpl -import com.artemchep.keyguard.common.usecase.impl.WatchtowerInactivePasskey -import com.artemchep.keyguard.common.usecase.impl.WatchtowerInactiveTfa -import com.artemchep.keyguard.common.usecase.impl.WatchtowerIncomplete -import com.artemchep.keyguard.common.usecase.impl.WatchtowerPasswordStrength import com.artemchep.keyguard.common.usecase.impl.WatchtowerSyncerImpl import com.artemchep.keyguard.common.usecase.impl.WindowCoroutineScopeImpl import com.artemchep.keyguard.copy.Base32ServiceJvm @@ -915,6 +918,31 @@ fun globalModuleJvm() = DI.Module( directDI = this, ) } + bindSingleton { + GetInMemoryLogsEnabledImpl( + directDI = this, + ) + } + bindSingleton { + GetInMemoryLogsImpl( + directDI = this, + ) + } + bindSingleton { + PutInMemoryLogsEnabledImpl( + directDI = this, + ) + } + bindSingleton { + InMemoryLogRepositoryImpl( + directDI = this, + ) + } + bindSingleton { + LogRepositoryBridge( + directDI = this, + ) + } bindSingleton { GetJustDeleteMeByUrlImpl( directDI = this,