feature: Record / export logs
This commit is contained in:
parent
c84dfe7b17
commit
842639bcb4
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -236,7 +236,7 @@ fun diFingerprintRepositoryModule() = DI.Module(
|
|||
deviceEncryptionKeyUseCase = instance(),
|
||||
)
|
||||
}
|
||||
bindSingleton<LogRepository> {
|
||||
bindSingleton<LogRepositoryAndroid> {
|
||||
LogRepositoryAndroid()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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_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_logs_title">Logs</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_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_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>
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
@ -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<Any?>
|
||||
}
|
||||
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<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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package com.artemchep.keyguard.common.service.logging
|
||||
|
||||
interface LogRepositoryChild : LogRepositoryBase
|
|
@ -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>>
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
|
||||
interface ExportLogs : (
|
||||
) -> IO<Unit>
|
|
@ -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>>
|
|
@ -0,0 +1,8 @@
|
|||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface GetInMemoryLogsEnabled : () -> Flow<Boolean> {
|
||||
/** Latest value */
|
||||
val value: Boolean
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
|
||||
interface PutInMemoryLogsEnabled : (Boolean) -> IO<Unit>
|
|
@ -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 <T> track(
|
||||
|
@ -37,21 +42,46 @@ interface SupervisorRead {
|
|||
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(
|
||||
value = persistentMapOf<AccountTask, PersistentMap<AccountId, Int>>(),
|
||||
)
|
||||
|
||||
constructor(directDI: DirectDI) : this(
|
||||
logRepository = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun <T> track(
|
||||
accountIdSet: Set<AccountId>,
|
||||
accountTask: AccountTask,
|
||||
io: IO<T>,
|
||||
): IO<T> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -28,7 +28,7 @@ inline fun <T> Flow<T>.withLogTimeOfFirstEvent(
|
|||
} else {
|
||||
""
|
||||
}
|
||||
"It took ${dt}ms. to load first portion of data. $suffix"
|
||||
"It took ${dt}. to load first portion of data. $suffix"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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<ExportAccount> {
|
||||
ExportAccountImpl(this)
|
||||
}
|
||||
bindSingleton<ExportLogs> {
|
||||
ExportLogsImpl(this)
|
||||
}
|
||||
bindSingleton<PutAccountColorById> {
|
||||
PutAccountColorByIdImpl(this)
|
||||
}
|
||||
|
@ -683,7 +688,7 @@ fun DI.Builder.createSubDi2(
|
|||
SyncByTokenImpl(this)
|
||||
}
|
||||
bindSingleton<WatchdogImpl> {
|
||||
WatchdogImpl()
|
||||
WatchdogImpl(this)
|
||||
}
|
||||
bindSingleton<Watchdog> {
|
||||
instance<WatchdogImpl>()
|
||||
|
|
|
@ -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<Database>
|
||||
|
||||
fun <T> mutate(
|
||||
tag: String,
|
||||
block: suspend (Database) -> T,
|
||||
): IO<T>
|
||||
|
||||
|
@ -193,11 +197,28 @@ class DatabaseManagerImpl(
|
|||
override fun get() = dbIo.map { it.database }
|
||||
|
||||
override fun <T> 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String, (DirectDI) -> 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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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<FolderEntity>(
|
||||
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<CipherEntity>(
|
||||
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<CollectionEntity>(
|
||||
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<OrganizationEntity>(
|
||||
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<SyncSends>(
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Remote> {
|
|||
fun updateRemoteModel(remote: Remote)
|
||||
}
|
||||
|
||||
context(SyncScope)
|
||||
suspend fun <
|
||||
Local : BitwardenService.Has<Local>,
|
||||
LocalDecoded : Any,
|
||||
|
@ -130,7 +132,7 @@ suspend fun <
|
|||
remoteDecodedFallback: suspend (Remote, Local?, Throwable) -> RemoteDecoded,
|
||||
remoteDeleteById: suspend (String) -> Unit,
|
||||
remotePut: suspend RemotePutScope<Remote>.(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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<Unit> = db
|
||||
.mutate { database ->
|
||||
.mutate(TAG) { database ->
|
||||
val dao = database.accountQueries
|
||||
dao.deleteAll()
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ class AddAccountImpl(
|
|||
),
|
||||
)
|
||||
|
||||
db.mutate { database ->
|
||||
db.mutate(TAG) { database ->
|
||||
database.accountQueries.insert(
|
||||
accountId = token.id,
|
||||
data = token,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -53,7 +53,7 @@ class ModifyDatabase(
|
|||
operator fun <T> invoke(
|
||||
block: suspend (Database) -> Result<T>,
|
||||
): IO<T> = db
|
||||
.mutate { database ->
|
||||
.mutate("ModifyDatabase") { database ->
|
||||
val accountIds = block(database)
|
||||
accountIds
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -309,7 +309,7 @@ fun diFingerprintRepositoryModule() = DI.Module(
|
|||
val m: KeyValueStore = instance(arg = file)
|
||||
m
|
||||
}
|
||||
bindSingleton<LogRepository> {
|
||||
bindSingleton<LogRepositoryKotlin> {
|
||||
LogRepositoryKotlin()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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> {
|
||||
GetJustDeleteMeByUrlImpl(
|
||||
directDI = this,
|
||||
|
|
Loading…
Reference in New Issue