feature: Record / export logs

This commit is contained in:
Artem Chepurnoy 2024-06-16 19:44:11 +03:00
parent c84dfe7b17
commit 842639bcb4
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
46 changed files with 1173 additions and 87 deletions

View File

@ -1,27 +1,21 @@
package com.artemchep.keyguard.copy package com.artemchep.keyguard.copy
import android.util.Log 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.LogLevel
import com.artemchep.keyguard.common.service.logging.LogRepository import com.artemchep.keyguard.common.service.logging.LogRepositoryChild
import kotlinx.coroutines.GlobalScope
class LogRepositoryAndroid() : LogRepository { class LogRepositoryAndroid(
override fun post( ) : LogRepositoryChild {
override suspend fun add(
tag: String, tag: String,
message: String, message: String,
level: LogLevel, level: LogLevel,
) { ) {
add(tag, message, level).attempt().launchIn(GlobalScope) 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)
} }
override fun add(
tag: String,
message: String,
level: LogLevel,
) = ioEffect {
Log.d(tag, message)
} }
} }

View File

@ -236,7 +236,7 @@ fun diFingerprintRepositoryModule() = DI.Module(
deviceEncryptionKeyUseCase = instance(), deviceEncryptionKeyUseCase = instance(),
) )
} }
bindSingleton<LogRepository> { bindSingleton<LogRepositoryAndroid> {
LogRepositoryAndroid() LogRepositoryAndroid()
} }
} }

View File

@ -1023,6 +1023,7 @@
<string name="pref_item_allow_screenshots_text_on">Show the content of the app when switching between recent apps and allow capturing a screen</string> <string name="pref_item_allow_screenshots_text_on">Show the content of the app when switching between recent apps and allow capturing a screen</string>
<string name="pref_item_allow_screenshots_text_off">Hide the content of the app when switching between recent apps and forbid capturing a screen</string> <string name="pref_item_allow_screenshots_text_off">Hide the content of the app when switching between recent apps and forbid capturing a screen</string>
<string name="pref_item_data_safety_title">Data safety</string> <string name="pref_item_data_safety_title">Data safety</string>
<string name="pref_item_logs_title">Logs</string>
<string name="pref_item_lock_vault_title">Lock vault</string> <string name="pref_item_lock_vault_title">Lock vault</string>
<string name="pref_item_lock_vault_after_reboot_title">Lock after a reboot</string> <string name="pref_item_lock_vault_after_reboot_title">Lock after a reboot</string>
<string name="pref_item_lock_vault_after_reboot_text">Lock the vault after a device reboot</string> <string name="pref_item_lock_vault_after_reboot_text">Lock the vault after a device reboot</string>
@ -1150,4 +1151,10 @@
<string name="datasafety_remote_section">Remote data</string> <string name="datasafety_remote_section">Remote data</string>
<string name="datasafety_remote_text">Remote data is stored on Bitwarden servers with zero-knowledge encryption.</string> <string name="datasafety_remote_text">Remote data is stored on Bitwarden servers with zero-knowledge encryption.</string>
<string name="logs_header_title">Logs</string>
<string name="logs_start_recording_fab_title">Start recording</string>
<string name="logs_stop_recording_fab_title">Stop recording</string>
<string name="logs_export_button">Export</string>
<string name="logs_export_success">Export complete</string>
</resources> </resources>

View File

@ -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,
)

View File

@ -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,
)
}

View File

@ -1,8 +1,10 @@
package com.artemchep.keyguard.common.service.logging package com.artemchep.keyguard.common.service.logging
enum class LogLevel { enum class LogLevel(
DEBUG, val letter: String,
INFO, ) {
WARNING, DEBUG("D"),
ERROR, INFO("I"),
WARNING("W"),
ERROR("E"),
} }

View File

@ -1,21 +1,10 @@
package com.artemchep.keyguard.common.service.logging package com.artemchep.keyguard.common.service.logging
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.platform.util.isRelease import com.artemchep.keyguard.platform.util.isRelease
import org.kodein.di.DirectDI
import org.kodein.di.allInstances
interface LogRepository { interface LogRepository : LogRepositoryBase
fun post(
tag: String,
message: String,
level: LogLevel = LogLevel.DEBUG,
)
fun add(
tag: String,
message: String,
level: LogLevel = LogLevel.DEBUG,
): IO<Any?>
}
/** /**
* A version that only exists in debug and gets completely * A version that only exists in debug and gets completely
@ -30,3 +19,41 @@ inline fun LogRepository.postDebug(
post(tag, msg, level = LogLevel.DEBUG) post(tag, msg, level = LogLevel.DEBUG)
} }
} }
class LogRepositoryBridge(
private val logRepositoryList: List<LogRepositoryChild>,
) : 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,
)
}
}
}

View File

@ -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,
)
}

View File

@ -0,0 +1,3 @@
package com.artemchep.keyguard.common.service.logging
interface LogRepositoryChild : LogRepositoryBase

View File

@ -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<Unit>
fun getEnabled(): Flow<Boolean>
fun get(): Flow<ImmutableList<Log>>
}

View File

@ -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<Log>())
override val isEnabled: Boolean get() = switchSink.value
constructor(
directDI: DirectDI,
) : this()
override fun setEnabled(enabled: Boolean): IO<Unit> = ioEffect {
switchSink.value = enabled
if (!enabled) {
logsSink.value = persistentListOf()
}
}
override fun getEnabled(): Flow<Boolean> = switchSink
override fun get(): Flow<ImmutableList<Log>> = 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)
}
}
}

View File

@ -1,26 +1,14 @@
package com.artemchep.keyguard.common.service.logging.kotlin 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.LogLevel
import com.artemchep.keyguard.common.service.logging.LogRepository import com.artemchep.keyguard.common.service.logging.LogRepositoryChild
import kotlinx.coroutines.GlobalScope
class LogRepositoryKotlin : LogRepository { class LogRepositoryKotlin : LogRepositoryChild {
override fun post( override suspend fun add(
tag: String, tag: String,
message: String, message: String,
level: LogLevel, level: LogLevel,
) { ) {
add(tag, message, level).attempt().launchIn(GlobalScope) println("[${level.letter}]/$tag: $message")
}
override fun add(
tag: String,
message: String,
level: LogLevel,
) = ioEffect {
println("$tag: $message")
} }
} }

View File

@ -1,10 +1,14 @@
package com.artemchep.keyguard.common.service.permission package com.artemchep.keyguard.common.service.permission
import androidx.compose.runtime.Immutable
import com.artemchep.keyguard.platform.LeContext import com.artemchep.keyguard.platform.LeContext
@Immutable
sealed interface PermissionState { sealed interface PermissionState {
@Immutable
data object Granted : PermissionState data object Granted : PermissionState
@Immutable
data class Declined( data class Declined(
val ask: (LeContext) -> Unit, val ask: (LeContext) -> Unit,
) : PermissionState ) : PermissionState

View File

@ -0,0 +1,6 @@
package com.artemchep.keyguard.common.usecase
import com.artemchep.keyguard.common.io.IO
interface ExportLogs : (
) -> IO<Unit>

View File

@ -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<ImmutableList<Log>>

View File

@ -0,0 +1,8 @@
package com.artemchep.keyguard.common.usecase
import kotlinx.coroutines.flow.Flow
interface GetInMemoryLogsEnabled : () -> Flow<Boolean> {
/** Latest value */
val value: Boolean
}

View File

@ -0,0 +1,5 @@
package com.artemchep.keyguard.common.usecase
import com.artemchep.keyguard.common.io.IO
interface PutInMemoryLogsEnabled : (Boolean) -> IO<Unit>

View File

@ -5,13 +5,18 @@ import com.artemchep.keyguard.common.io.bind
import com.artemchep.keyguard.common.io.ioEffect import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.model.AccountId import com.artemchep.keyguard.common.model.AccountId
import com.artemchep.keyguard.common.model.AccountTask import com.artemchep.keyguard.common.model.AccountTask
import com.artemchep.keyguard.common.service.logging.LogRepository
import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import org.kodein.di.DirectDI
import org.kodein.di.instance
interface Watchdog { interface Watchdog {
fun <T> track( fun <T> track(
@ -37,23 +42,48 @@ interface SupervisorRead {
fun get(accountTask: AccountTask): Flow<Set<AccountId>> fun get(accountTask: AccountTask): Flow<Set<AccountId>>
} }
class WatchdogImpl() : Watchdog, SupervisorRead { class WatchdogImpl(
private val logRepository: LogRepository,
) : Watchdog, SupervisorRead {
companion object {
private const val TAG = "Watchdog"
}
private val sink = MutableStateFlow( private val sink = MutableStateFlow(
value = persistentMapOf<AccountTask, PersistentMap<AccountId, Int>>(), value = persistentMapOf<AccountTask, PersistentMap<AccountId, Int>>(),
) )
constructor(directDI: DirectDI) : this(
logRepository = directDI.instance(),
)
override fun <T> track( override fun <T> track(
accountIdSet: Set<AccountId>, accountIdSet: Set<AccountId>,
accountTask: AccountTask, accountTask: AccountTask,
io: IO<T>, io: IO<T>,
): IO<T> = ioEffect { ): IO<T> = ioEffect {
val ids = accountIdSet
.joinToString { it.id }
logRepository.add(
tag = TAG,
message = "Adding '$accountTask' marker to accounts: $ids",
)
try { try {
updateState(accountIdSet, accountTask, Int::inc) updateState(accountIdSet, accountTask, Int::inc)
io.bind() io.bind()
} finally {
try {
withContext(NonCancellable) {
logRepository.add(
tag = TAG,
message = "Removing '$accountTask' marker from accounts: $ids",
)
}
} finally { } finally {
updateState(accountIdSet, accountTask, Int::dec) updateState(accountIdSet, accountTask, Int::dec)
} }
} }
}
private fun updateState( private fun updateState(
keys: Set<AccountId>, keys: Set<AccountId>,

View File

@ -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<Boolean> = inMemoryLogRepository
.getEnabled()
}

View File

@ -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()
}

View File

@ -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<Unit> = inMemoryLogRepository
.setEnabled(inMemoryLogsEnabled)
}

View File

@ -28,7 +28,7 @@ inline fun <T> Flow<T>.withLogTimeOfFirstEvent(
} else { } else {
"" ""
} }
"It took ${dt}ms. to load first portion of data. $suffix" "It took ${dt}. to load first portion of data. $suffix"
} }
} }
}, },

View File

@ -68,6 +68,7 @@ import com.artemchep.keyguard.common.usecase.CopyCipherById
import com.artemchep.keyguard.common.usecase.DownloadAttachment import com.artemchep.keyguard.common.usecase.DownloadAttachment
import com.artemchep.keyguard.common.usecase.EditWordlist import com.artemchep.keyguard.common.usecase.EditWordlist
import com.artemchep.keyguard.common.usecase.ExportAccount 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.FavouriteCipherById
import com.artemchep.keyguard.common.usecase.GetAccountHasError import com.artemchep.keyguard.common.usecase.GetAccountHasError
import com.artemchep.keyguard.common.usecase.GetAccountStatus 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.CipherUnsecureUrlCheckImpl
import com.artemchep.keyguard.provider.bitwarden.usecase.CopyCipherByIdImpl import com.artemchep.keyguard.provider.bitwarden.usecase.CopyCipherByIdImpl
import com.artemchep.keyguard.provider.bitwarden.usecase.ExportAccountImpl 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.FavouriteCipherByIdImpl
import com.artemchep.keyguard.provider.bitwarden.usecase.GetAccountHasErrorImpl import com.artemchep.keyguard.provider.bitwarden.usecase.GetAccountHasErrorImpl
import com.artemchep.keyguard.provider.bitwarden.usecase.GetAccountsHasErrorImpl import com.artemchep.keyguard.provider.bitwarden.usecase.GetAccountsHasErrorImpl
@ -534,6 +536,9 @@ fun DI.Builder.createSubDi2(
bindSingleton<ExportAccount> { bindSingleton<ExportAccount> {
ExportAccountImpl(this) ExportAccountImpl(this)
} }
bindSingleton<ExportLogs> {
ExportLogsImpl(this)
}
bindSingleton<PutAccountColorById> { bindSingleton<PutAccountColorById> {
PutAccountColorByIdImpl(this) PutAccountColorByIdImpl(this)
} }
@ -683,7 +688,7 @@ fun DI.Builder.createSubDi2(
SyncByTokenImpl(this) SyncByTokenImpl(this)
} }
bindSingleton<WatchdogImpl> { bindSingleton<WatchdogImpl> {
WatchdogImpl() WatchdogImpl(this)
} }
bindSingleton<Watchdog> { bindSingleton<Watchdog> {
instance<WatchdogImpl>() instance<WatchdogImpl>()

View File

@ -13,6 +13,7 @@ import com.artemchep.keyguard.common.io.retry
import com.artemchep.keyguard.common.io.shared import com.artemchep.keyguard.common.io.shared
import com.artemchep.keyguard.common.model.MasterKey import com.artemchep.keyguard.common.model.MasterKey
import com.artemchep.keyguard.common.service.logging.LogRepository 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.BitwardenCipher
import com.artemchep.keyguard.core.store.bitwarden.BitwardenCollection import com.artemchep.keyguard.core.store.bitwarden.BitwardenCollection
import com.artemchep.keyguard.core.store.bitwarden.BitwardenFolder 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.data.pwnage.PasswordBreach
import com.artemchep.keyguard.provider.bitwarden.entity.HibpBreachGroup import com.artemchep.keyguard.provider.bitwarden.entity.HibpBreachGroup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
@ -53,6 +56,7 @@ interface DatabaseManager {
fun get(): IO<Database> fun get(): IO<Database>
fun <T> mutate( fun <T> mutate(
tag: String,
block: suspend (Database) -> T, block: suspend (Database) -> T,
): IO<T> ): IO<T>
@ -193,11 +197,28 @@ class DatabaseManagerImpl(
override fun get() = dbIo.map { it.database } override fun get() = dbIo.map { it.database }
override fun <T> mutate( override fun <T> mutate(
tag: String,
block: suspend (Database) -> T, block: suspend (Database) -> T,
) = dbIo ) = dbIo
.effectMap(Dispatchers.IO) { db -> .effectMap(Dispatchers.IO) { db ->
mutex.withLock { logRepository.add(
tag = TAG,
message = "Adding '$tag' database lock.",
)
mutex.lock()
try {
block(db.database) block(db.database)
} finally {
try {
withContext(NonCancellable) {
logRepository.add(
tag = TAG,
message = "Removing '$tag' database lock.",
)
}
} finally {
mutex.unlock()
}
} }
} }

View File

@ -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.settingLaunchAppPicker
import com.artemchep.keyguard.feature.home.settings.component.settingLaunchYubiKey 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.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.settingMarkdownProvider
import com.artemchep.keyguard.feature.home.settings.component.settingMasterPasswordProvider import com.artemchep.keyguard.feature.home.settings.component.settingMasterPasswordProvider
import com.artemchep.keyguard.feature.home.settings.component.settingNavAnimationProvider 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_APP_PICKER = "launch_app_picker"
const val LAUNCH_YUBIKEY = "launch_yubikey" const val LAUNCH_YUBIKEY = "launch_yubikey"
const val DATA_SAFETY = "data_safety" const val DATA_SAFETY = "data_safety"
const val LOGS = "logs"
const val FEATURES_OVERVIEW = "features_overview" const val FEATURES_OVERVIEW = "features_overview"
const val URL_OVERRIDE = "url_override" const val URL_OVERRIDE = "url_override"
const val RATE_APP = "rate_app" const val RATE_APP = "rate_app"
@ -253,6 +255,7 @@ val hub = mapOf<String, (DirectDI) -> SettingComponent>(
Setting.LAUNCH_YUBIKEY to ::settingLaunchYubiKey, Setting.LAUNCH_YUBIKEY to ::settingLaunchYubiKey,
Setting.LAUNCH_APP_PICKER to ::settingLaunchAppPicker, Setting.LAUNCH_APP_PICKER to ::settingLaunchAppPicker,
Setting.DATA_SAFETY to ::settingDataSafetyProvider, Setting.DATA_SAFETY to ::settingDataSafetyProvider,
Setting.LOGS to ::settingLogsProvider,
Setting.FEATURES_OVERVIEW to ::settingFeaturesOverviewProvider, Setting.FEATURES_OVERVIEW to ::settingFeaturesOverviewProvider,
Setting.URL_OVERRIDE to ::settingUrlOverrideProvider, Setting.URL_OVERRIDE to ::settingUrlOverrideProvider,
Setting.RATE_APP to ::settingRateAppProvider, Setting.RATE_APP to ::settingRateAppProvider,

View File

@ -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<RowScope>(Icons.Stub),
trailing = {
ChevronIcon()
},
title = {
Text(
text = stringResource(Res.string.pref_item_logs_title),
)
},
onClick = onClick,
)
}

View File

@ -32,6 +32,7 @@ fun OtherSettingsScreen() {
SettingPaneItem.Item(Setting.CRASHLYTICS), SettingPaneItem.Item(Setting.CRASHLYTICS),
SettingPaneItem.Item(Setting.DATA_SAFETY), SettingPaneItem.Item(Setting.DATA_SAFETY),
SettingPaneItem.Item(Setting.PERMISSION_DETAILS), SettingPaneItem.Item(Setting.PERMISSION_DETAILS),
SettingPaneItem.Item(Setting.LOGS),
), ),
), ),
SettingPaneItem.Group( SettingPaneItem.Group(

View File

@ -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;
}
}

View File

@ -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()
}
}

View File

@ -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,
)
}

View File

@ -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<Content>,
val exportFlow: StateFlow<Export>,
val switchFlow: StateFlow<Switch>,
) {
@Immutable
data class Content(
val items: ImmutableList<LogsItem>,
)
@Immutable
data class Export(
val writePermission: PermissionState,
val onExportClick: (() -> Unit)? = null,
)
@Immutable
data class Switch(
val checked: Boolean,
val onToggle: (() -> Unit)? = null,
)
}

View File

@ -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<LogsState> = 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))
}

View File

@ -2,6 +2,7 @@ package com.artemchep.keyguard.provider.bitwarden.api
import com.artemchep.keyguard.common.exception.HttpException import com.artemchep.keyguard.common.exception.HttpException
import com.artemchep.keyguard.common.io.bind 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.CipherEncryptor
import com.artemchep.keyguard.common.service.crypto.CryptoGenerator import com.artemchep.keyguard.common.service.crypto.CryptoGenerator
import com.artemchep.keyguard.common.service.logging.LogRepository import com.artemchep.keyguard.common.service.logging.LogRepository
@ -118,11 +119,15 @@ class SyncEngine(
) )
} }
context(SyncScope)
suspend fun sync() = kotlin.run { suspend fun sync() = kotlin.run {
val env = user.env.back() val env = user.env.back()
val api = env.api val api = env.api
val token = requireNotNull(user.token).accessToken val token = requireNotNull(user.token).accessToken
post(
title = "Send send request.",
)
val response = api.sync( val response = api.sync(
httpClient = httpClient, httpClient = httpClient,
env = env, env = env,
@ -283,6 +288,10 @@ class SyncEngine(
// Profile // Profile
// //
post(
title = "Syncing a profile entity.",
)
val newProfile = BitwardenProfile val newProfile = BitwardenProfile
.encrypted( .encrypted(
accountId = user.id, accountId = user.id,
@ -345,6 +354,10 @@ class SyncEngine(
.transform(this) .transform(this)
} }
post(
title = "Syncing folder entities.",
)
val folderDao = db.folderQueries val folderDao = db.folderQueries
val folderRemoteLens = SyncManager.Lens<FolderEntity>( val folderRemoteLens = SyncManager.Lens<FolderEntity>(
getId = { it.id }, getId = { it.id },
@ -476,7 +489,7 @@ class SyncEngine(
) )
}, },
onLog = { msg, logLevel -> onLog = { msg, logLevel ->
logRepository.post(TAG, "[SyncFolder] $msg", logLevel) logRepository.add(TAG, msg, logLevel)
}, },
) )
@ -535,6 +548,10 @@ class SyncEngine(
.transform(this, codec2) .transform(this, codec2)
} }
post(
title = "Syncing cipher entities.",
)
val cipherDao = db.cipherQueries val cipherDao = db.cipherQueries
val cipherRemoteLens = SyncManager.Lens<CipherEntity>( val cipherRemoteLens = SyncManager.Lens<CipherEntity>(
getId = { it.id }, getId = { it.id },
@ -806,7 +823,7 @@ class SyncEngine(
} }
}, },
onLog = { msg, logLevel -> onLog = { msg, logLevel ->
logRepository.post(TAG, msg, logLevel) logRepository.add(TAG, msg, logLevel)
}, },
) )
@ -825,6 +842,10 @@ class SyncEngine(
.transform(this) .transform(this)
} }
post(
title = "Syncing collection entities.",
)
val collectionDao = db.collectionQueries val collectionDao = db.collectionQueries
val collectionRemoteLens = SyncManager.Lens<CollectionEntity>( val collectionRemoteLens = SyncManager.Lens<CollectionEntity>(
getId = { it.id }, getId = { it.id },
@ -915,7 +936,7 @@ class SyncEngine(
TODO() TODO()
}, },
onLog = { msg, logLevel -> onLog = { msg, logLevel ->
logRepository.post(TAG, msg, logLevel) logRepository.add(TAG, msg, logLevel)
}, },
) )
@ -934,6 +955,10 @@ class SyncEngine(
.transform(this) .transform(this)
} }
post(
title = "Syncing organization entities.",
)
val organizationDao = db.organizationQueries val organizationDao = db.organizationQueries
val organizationRemoteLens = SyncManager.Lens<OrganizationEntity>( val organizationRemoteLens = SyncManager.Lens<OrganizationEntity>(
getId = { it.id }, getId = { it.id },
@ -1020,7 +1045,7 @@ class SyncEngine(
TODO() TODO()
}, },
onLog = { msg, logLevel -> onLog = { msg, logLevel ->
logRepository.post(TAG, msg, logLevel) logRepository.add(TAG, msg, logLevel)
}, },
) )
@ -1044,6 +1069,10 @@ class SyncEngine(
.transform(this, codec2) .transform(this, codec2)
} }
post(
title = "Syncing send entities.",
)
val sendDao = db.sendQueries val sendDao = db.sendQueries
val sendRemoteLens = SyncManager.Lens<SyncSends>( val sendRemoteLens = SyncManager.Lens<SyncSends>(
getId = { it.id }, getId = { it.id },
@ -1217,10 +1246,14 @@ class SyncEngine(
) )
}, },
onLog = { msg, logLevel -> onLog = { msg, logLevel ->
logRepository.post(TAG, msg, logLevel) logRepository.add(TAG, msg, logLevel)
}, },
) )
post(
title = "Syncing complete.",
)
Unit Unit
} }

View File

@ -11,6 +11,7 @@ import com.artemchep.keyguard.common.io.handleErrorTap
import com.artemchep.keyguard.common.io.ioEffect import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.io.measure import com.artemchep.keyguard.common.io.measure
import com.artemchep.keyguard.common.io.parallel 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.service.logging.LogLevel
import com.artemchep.keyguard.common.usecase.GetPasswordStrength import com.artemchep.keyguard.common.usecase.GetPasswordStrength
import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher
@ -107,6 +108,7 @@ interface RemotePutScope<Remote> {
fun updateRemoteModel(remote: Remote) fun updateRemoteModel(remote: Remote)
} }
context(SyncScope)
suspend fun < suspend fun <
Local : BitwardenService.Has<Local>, Local : BitwardenService.Has<Local>,
LocalDecoded : Any, LocalDecoded : Any,
@ -130,7 +132,7 @@ suspend fun <
remoteDecodedFallback: suspend (Remote, Local?, Throwable) -> RemoteDecoded, remoteDecodedFallback: suspend (Remote, Local?, Throwable) -> RemoteDecoded,
remoteDeleteById: suspend (String) -> Unit, remoteDeleteById: suspend (String) -> Unit,
remotePut: suspend RemotePutScope<Remote>.(LocalDecoded) -> RemoteDecoded, remotePut: suspend RemotePutScope<Remote>.(LocalDecoded) -> RemoteDecoded,
onLog: (String, LogLevel) -> Unit, onLog: suspend (String, LogLevel) -> Unit,
) { ) {
onLog( onLog(
"[Start] Starting to sync the $name: " + "[Start] Starting to sync the $name: " +
@ -147,20 +149,36 @@ suspend fun <
remoteItems = remoteItems, remoteItems = remoteItems,
shouldOverwrite = shouldOverwrite, 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 // Write changes to local storage as these
// are quite fast to do. // are quite fast to do.
// //
localDeleteById( val localDeletedCipherIds = df.localDeletedCipherIds
df.localDeletedCipherIds .map { localLens.getLocalId(it.local) }
.map { localLens.getLocalId(it.local) }, onLog(
"[local] Deleting ${localDeletedCipherIds.size} $name entries...",
LogLevel.DEBUG,
) )
localDeleteById(localDeletedCipherIds)
val localPutCipherDecoded = df.localPutCipher val localPutCipherDecoded = df.localPutCipher
.map { (localOrNull, remote) -> .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 -> .handleError { e ->
val remoteId = remoteLens.getId(remote) val remoteId = remoteLens.getId(remote)
val localId = localOrNull?.let(localLens.getLocalId) val localId = localOrNull?.let(localLens.getLocalId)
@ -228,7 +246,13 @@ suspend fun <
.map { entry -> .map { entry ->
val localId = localLens.getLocalId(entry.local) val localId = localLens.getLocalId(entry.local)
val remoteId = remoteLens.getId(entry.remote) val remoteId = remoteLens.getId(entry.remote)
ioEffect { remoteDeleteById(remoteId) } ioEffect {
onLog(
"[local] Decoding $remoteId $name entry...",
LogLevel.DEBUG,
)
remoteDeleteById(remoteId)
}
.handleErrorTap { e -> .handleErrorTap { e ->
handleFailedToPut(entry.local, e = e) handleFailedToPut(entry.local, e = e)
} }

View File

@ -10,12 +10,16 @@ import org.kodein.di.instance
class AddCipherOpenedHistoryImpl( class AddCipherOpenedHistoryImpl(
private val db: DatabaseManager, private val db: DatabaseManager,
) : AddCipherOpenedHistory { ) : AddCipherOpenedHistory {
companion object {
private const val TAG = "AddCipherOpened"
}
constructor(directDI: DirectDI) : this( constructor(directDI: DirectDI) : this(
db = directDI.instance(), db = directDI.instance(),
) )
override fun invoke(request: AddCipherOpenedHistoryRequest) = db override fun invoke(request: AddCipherOpenedHistoryRequest) = db
.mutate { .mutate(TAG) {
it.cipherUsageHistoryQueries.insert( it.cipherUsageHistoryQueries.insert(
cipherId = request.cipherId, cipherId = request.cipherId,
credentialId = null, credentialId = null,

View File

@ -10,12 +10,16 @@ import org.kodein.di.instance
class AddCipherUsedAutofillHistoryImpl( class AddCipherUsedAutofillHistoryImpl(
private val db: DatabaseManager, private val db: DatabaseManager,
) : AddCipherUsedAutofillHistory { ) : AddCipherUsedAutofillHistory {
companion object {
private const val TAG = "AddCipherUsedAutofill"
}
constructor(directDI: DirectDI) : this( constructor(directDI: DirectDI) : this(
db = directDI.instance(), db = directDI.instance(),
) )
override fun invoke(request: AddCipherOpenedHistoryRequest) = db override fun invoke(request: AddCipherOpenedHistoryRequest) = db
.mutate { .mutate(TAG) {
it.cipherUsageHistoryQueries.insert( it.cipherUsageHistoryQueries.insert(
cipherId = request.cipherId, cipherId = request.cipherId,
credentialId = null, credentialId = null,

View File

@ -4,18 +4,23 @@ import com.artemchep.keyguard.common.model.AddCipherUsedPasskeyHistoryRequest
import com.artemchep.keyguard.common.model.CipherHistoryType import com.artemchep.keyguard.common.model.CipherHistoryType
import com.artemchep.keyguard.common.usecase.AddCipherUsedPasskeyHistory import com.artemchep.keyguard.common.usecase.AddCipherUsedPasskeyHistory
import com.artemchep.keyguard.core.store.DatabaseManager 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.DirectDI
import org.kodein.di.instance import org.kodein.di.instance
class AddCipherUsedPasskeyHistoryImpl( class AddCipherUsedPasskeyHistoryImpl(
private val db: DatabaseManager, private val db: DatabaseManager,
) : AddCipherUsedPasskeyHistory { ) : AddCipherUsedPasskeyHistory {
companion object {
private const val TAG = "AddCipherUsedPasskey"
}
constructor(directDI: DirectDI) : this( constructor(directDI: DirectDI) : this(
db = directDI.instance(), db = directDI.instance(),
) )
override fun invoke(request: AddCipherUsedPasskeyHistoryRequest) = db override fun invoke(request: AddCipherUsedPasskeyHistoryRequest) = db
.mutate { .mutate(TAG) {
it.cipherUsageHistoryQueries.insert( it.cipherUsageHistoryQueries.insert(
cipherId = request.cipherId, cipherId = request.cipherId,
credentialId = request.credentialId, credentialId = request.credentialId,

View File

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

View File

@ -8,6 +8,7 @@ import com.artemchep.keyguard.common.usecase.RemoveAccountById
import com.artemchep.keyguard.common.usecase.Watchdog import com.artemchep.keyguard.common.usecase.Watchdog
import com.artemchep.keyguard.common.usecase.unit import com.artemchep.keyguard.common.usecase.unit
import com.artemchep.keyguard.core.store.DatabaseManager 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.DirectDI
import org.kodein.di.instance import org.kodein.di.instance
@ -46,7 +47,7 @@ class RemoveAccountByIdImpl(
private fun performRemoveAccount( private fun performRemoveAccount(
accountId: AccountId, accountId: AccountId,
) = db ) = db
.mutate { database -> .mutate(TAG) { database ->
val dao = database.accountQueries val dao = database.accountQueries
dao.deleteByAccountId(accountId.id) dao.deleteByAccountId(accountId.id)
} }

View File

@ -3,6 +3,7 @@ package com.artemchep.keyguard.provider.bitwarden.usecase
import com.artemchep.keyguard.common.io.IO import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.usecase.RemoveAccounts import com.artemchep.keyguard.common.usecase.RemoveAccounts
import com.artemchep.keyguard.core.store.DatabaseManager 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.DirectDI
import org.kodein.di.instance import org.kodein.di.instance
@ -21,7 +22,7 @@ class RemoveAccountsImpl(
) )
override fun invoke(): IO<Unit> = db override fun invoke(): IO<Unit> = db
.mutate { database -> .mutate(TAG) { database ->
val dao = database.accountQueries val dao = database.accountQueries
dao.deleteAll() dao.deleteAll()
} }

View File

@ -112,7 +112,7 @@ class AddAccountImpl(
), ),
) )
db.mutate { database -> db.mutate(TAG) { database ->
database.accountQueries.insert( database.accountQueries.insert(
accountId = token.id, accountId = token.id,
data = token, data = token,

View File

@ -9,6 +9,8 @@ import com.artemchep.keyguard.common.io.measure
import com.artemchep.keyguard.common.io.toIO import com.artemchep.keyguard.common.io.toIO
import com.artemchep.keyguard.common.model.AccountId import com.artemchep.keyguard.common.model.AccountId
import com.artemchep.keyguard.common.model.AccountTask 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.CipherEncryptor
import com.artemchep.keyguard.common.service.crypto.CryptoGenerator import com.artemchep.keyguard.common.service.crypto.CryptoGenerator
import com.artemchep.keyguard.common.service.logging.LogRepository import com.artemchep.keyguard.common.service.logging.LogRepository
@ -70,6 +72,14 @@ class SyncByTokenImpl(
accountId = AccountId(user.id), accountId = AccountId(user.id),
accountTask = AccountTask.SYNC, 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 // We want to automatically request a new access token if the old
// one has expired. // one has expired.
withRefreshableAccessToken( withRefreshableAccessToken(
@ -92,8 +102,10 @@ class SyncByTokenImpl(
syncer = dbSyncer, syncer = dbSyncer,
) )
mutex.withLock { mutex.withLock {
with(scope) {
syncEngine.sync() syncEngine.sync()
} }
}
// sss( // sss(
// logRepository = logRepository, // logRepository = logRepository,
// cipherEncryptor = cipherEncryptor, // cipherEncryptor = cipherEncryptor,
@ -107,7 +119,7 @@ class SyncByTokenImpl(
} }
.biFlatTap( .biFlatTap(
ifException = { e -> ifException = { e ->
db.mutate { db.mutate(TAG) {
val dao = it.metaQueries val dao = it.metaQueries
val existingMeta = dao val existingMeta = dao
.getByAccountId(accountId = user.id) .getByAccountId(accountId = user.id)
@ -143,7 +155,7 @@ class SyncByTokenImpl(
} }
}, },
ifSuccess = { ifSuccess = {
db.mutate { db.mutate(TAG) {
val now = Clock.System.now() val now = Clock.System.now()
val meta = BitwardenMeta( val meta = BitwardenMeta(
accountId = user.id, accountId = user.id,

View File

@ -53,7 +53,7 @@ class ModifyDatabase(
operator fun <T> invoke( operator fun <T> invoke(
block: suspend (Database) -> Result<T>, block: suspend (Database) -> Result<T>,
): IO<T> = db ): IO<T> = db
.mutate { database -> .mutate("ModifyDatabase") { database ->
val accountIds = block(database) val accountIds = block(database)
accountIds accountIds
} }

View File

@ -125,7 +125,7 @@ suspend fun getAndUpdateUserToken(
throw IllegalStateException("Help") throw IllegalStateException("Help")
} }
val newUser = db.mutate { val newUser = db.mutate("RefreshToken") {
it.accountQueries it.accountQueries
.getByAccountId(user.id) .getByAccountId(user.id)
.executeAsOneOrNull() .executeAsOneOrNull()
@ -149,7 +149,7 @@ suspend fun getAndUpdateUserToken(
expirationDate = login.accessTokenExpiryDate, expirationDate = login.accessTokenExpiryDate,
) )
val u = user.copy(token = token) val u = user.copy(token = token)
db.mutate { db.mutate("RefreshToken") {
it.accountQueries.insert( it.accountQueries.insert(
accountId = u.id, accountId = u.id,
data = u, data = u,

View File

@ -309,7 +309,7 @@ fun diFingerprintRepositoryModule() = DI.Module(
val m: KeyValueStore = instance(arg = file) val m: KeyValueStore = instance(arg = file)
m m
} }
bindSingleton<LogRepository> { bindSingleton<LogRepositoryKotlin> {
LogRepositoryKotlin() LogRepositoryKotlin()
} }
} }

View File

@ -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.keyvalue.KeyValueStore
import com.artemchep.keyguard.common.service.license.LicenseService import com.artemchep.keyguard.common.service.license.LicenseService
import com.artemchep.keyguard.common.service.license.impl.LicenseServiceImpl 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.LogRepository
import com.artemchep.keyguard.common.service.logging.LogRepositoryBridge
import com.artemchep.keyguard.common.service.passkey.PassKeyService import com.artemchep.keyguard.common.service.passkey.PassKeyService
import com.artemchep.keyguard.common.service.passkey.impl.PassKeyServiceImpl import com.artemchep.keyguard.common.service.passkey.impl.PassKeyServiceImpl
import com.artemchep.keyguard.common.service.placeholder.impl.CipherPlaceholder 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.GetBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.GetBiometricTimeout import com.artemchep.keyguard.common.usecase.GetBiometricTimeout
import com.artemchep.keyguard.common.usecase.GetBiometricTimeoutVariants 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.GetCachePremium
import com.artemchep.keyguard.common.usecase.GetCanWrite import com.artemchep.keyguard.common.usecase.GetCanWrite
import com.artemchep.keyguard.common.usecase.GetCheckPasskeys 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.GetFontVariants
import com.artemchep.keyguard.common.usecase.GetGravatar import com.artemchep.keyguard.common.usecase.GetGravatar
import com.artemchep.keyguard.common.usecase.GetGravatarUrl 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.GetJustDeleteMeByUrl
import com.artemchep.keyguard.common.usecase.GetJustGetMyDataByUrl import com.artemchep.keyguard.common.usecase.GetJustGetMyDataByUrl
import com.artemchep.keyguard.common.usecase.GetKeepScreenOn 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.PutDebugScreenDelay
import com.artemchep.keyguard.common.usecase.PutFont import com.artemchep.keyguard.common.usecase.PutFont
import com.artemchep.keyguard.common.usecase.PutGravatar 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.PutKeepScreenOn
import com.artemchep.keyguard.common.usecase.PutMarkdown import com.artemchep.keyguard.common.usecase.PutMarkdown
import com.artemchep.keyguard.common.usecase.PutNavAnimation 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.GetBiometricRequireConfirmationImpl
import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutImpl import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutImpl
import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutVariantsImpl 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.GetCachePremiumImpl
import com.artemchep.keyguard.common.usecase.impl.GetCanWriteImpl import com.artemchep.keyguard.common.usecase.impl.GetCanWriteImpl
import com.artemchep.keyguard.common.usecase.impl.GetCheckPasskeysImpl 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.GetFontVariantsImpl
import com.artemchep.keyguard.common.usecase.impl.GetGravatarImpl import com.artemchep.keyguard.common.usecase.impl.GetGravatarImpl
import com.artemchep.keyguard.common.usecase.impl.GetGravatarUrlImpl 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.GetJustDeleteMeByUrlImpl
import com.artemchep.keyguard.common.usecase.impl.GetJustGetMyDataByUrlImpl import com.artemchep.keyguard.common.usecase.impl.GetJustGetMyDataByUrlImpl
import com.artemchep.keyguard.common.usecase.impl.GetKeepScreenOnImpl 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.PutDebugScreenDelayImpl
import com.artemchep.keyguard.common.usecase.impl.PutFontImpl import com.artemchep.keyguard.common.usecase.impl.PutFontImpl
import com.artemchep.keyguard.common.usecase.impl.PutGravatarImpl 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.PutKeepScreenOnImpl
import com.artemchep.keyguard.common.usecase.impl.PutMarkdownImpl import com.artemchep.keyguard.common.usecase.impl.PutMarkdownImpl
import com.artemchep.keyguard.common.usecase.impl.PutNavAnimationImpl 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.RequestAppReviewImpl
import com.artemchep.keyguard.common.usecase.impl.UnlockUseCaseImpl import com.artemchep.keyguard.common.usecase.impl.UnlockUseCaseImpl
import com.artemchep.keyguard.common.usecase.impl.UpdateVersionLogImpl 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.WatchtowerSyncerImpl
import com.artemchep.keyguard.common.usecase.impl.WindowCoroutineScopeImpl import com.artemchep.keyguard.common.usecase.impl.WindowCoroutineScopeImpl
import com.artemchep.keyguard.copy.Base32ServiceJvm import com.artemchep.keyguard.copy.Base32ServiceJvm
@ -915,6 +918,31 @@ fun globalModuleJvm() = DI.Module(
directDI = this, directDI = this,
) )
} }
bindSingleton<GetInMemoryLogsEnabled> {
GetInMemoryLogsEnabledImpl(
directDI = this,
)
}
bindSingleton<GetInMemoryLogs> {
GetInMemoryLogsImpl(
directDI = this,
)
}
bindSingleton<PutInMemoryLogsEnabled> {
PutInMemoryLogsEnabledImpl(
directDI = this,
)
}
bindSingleton<InMemoryLogRepository> {
InMemoryLogRepositoryImpl(
directDI = this,
)
}
bindSingleton<LogRepository> {
LogRepositoryBridge(
directDI = this,
)
}
bindSingleton<GetJustDeleteMeByUrl> { bindSingleton<GetJustDeleteMeByUrl> {
GetJustDeleteMeByUrlImpl( GetJustDeleteMeByUrlImpl(
directDI = this, directDI = this,