feature: Record / export logs
This commit is contained in:
parent
c84dfe7b17
commit
842639bcb4
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,7 +236,7 @@ fun diFingerprintRepositoryModule() = DI.Module(
|
||||||
deviceEncryptionKeyUseCase = instance(),
|
deviceEncryptionKeyUseCase = instance(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bindSingleton<LogRepository> {
|
bindSingleton<LogRepositoryAndroid> {
|
||||||
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_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>
|
||||||
|
|
|
@ -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
|
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"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.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>,
|
||||||
|
|
|
@ -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 {
|
} 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.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>()
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.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(
|
||||||
|
|
|
@ -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.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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue