feature: Record / export logs

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

View File

@ -1,27 +1,21 @@
package com.artemchep.keyguard.copy
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)
}
}
}

View File

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

View File

@ -1023,6 +1023,7 @@
<string name="pref_item_allow_screenshots_text_on">Show the content of the app when switching between recent apps and allow capturing a screen</string>
<string name="pref_item_allow_screenshots_text_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>

View File

@ -0,0 +1,11 @@
package com.artemchep.keyguard.common.model
import com.artemchep.keyguard.common.service.logging.LogLevel
import kotlinx.datetime.Instant
data class Log(
val tag: String,
val message: String,
val level: LogLevel,
val createdAt: Instant,
)

View File

@ -0,0 +1,18 @@
package com.artemchep.keyguard.common.model
data class SyncProgress(
val title: String,
val progress: Progress? = null,
) {
data class Progress(
val at: Int,
val total: Int,
)
}
interface SyncScope {
suspend fun post(
title: String,
progress: SyncProgress.Progress? = null,
)
}

View File

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

View File

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

View File

@ -0,0 +1,24 @@
package com.artemchep.keyguard.common.service.logging
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
interface LogRepositoryBase {
fun post(
tag: String,
message: String,
level: LogLevel = LogLevel.DEBUG,
) {
GlobalScope.launch {
runCatching {
add(tag, message, level)
}
}
}
suspend fun add(
tag: String,
message: String,
level: LogLevel = LogLevel.DEBUG,
)
}

View File

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

View File

@ -0,0 +1,17 @@
package com.artemchep.keyguard.common.service.logging.inmemory
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.model.Log
import com.artemchep.keyguard.common.service.logging.LogRepositoryChild
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
interface InMemoryLogRepository : LogRepositoryChild {
val isEnabled: Boolean
fun setEnabled(enabled: Boolean): IO<Unit>
fun getEnabled(): Flow<Boolean>
fun get(): Flow<ImmutableList<Log>>
}

View File

@ -0,0 +1,62 @@
package com.artemchep.keyguard.common.service.logging.inmemory
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.model.Log
import com.artemchep.keyguard.common.service.logging.LogLevel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.datetime.Clock
import org.kodein.di.DirectDI
class InMemoryLogRepositoryImpl(
) : InMemoryLogRepository {
companion object {
private const val DEFAULT_ENABLED = false
}
private val switchSink = MutableStateFlow(DEFAULT_ENABLED)
private val logsSink = MutableStateFlow(persistentListOf<Log>())
override val isEnabled: Boolean get() = switchSink.value
constructor(
directDI: DirectDI,
) : this()
override fun setEnabled(enabled: Boolean): IO<Unit> = ioEffect {
switchSink.value = enabled
if (!enabled) {
logsSink.value = persistentListOf()
}
}
override fun getEnabled(): Flow<Boolean> = switchSink
override fun get(): Flow<ImmutableList<Log>> = logsSink
override suspend fun add(
tag: String,
message: String,
level: LogLevel,
) {
if (!isEnabled) {
return
}
val now = Clock.System.now()
val entity = Log(
tag = tag,
message = message,
level = level,
createdAt = now,
)
logsSink.update { logs ->
logs.add(entity)
}
}
}

View File

@ -1,26 +1,14 @@
package com.artemchep.keyguard.common.service.logging.kotlin
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")
}
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package com.artemchep.keyguard.common.usecase
import com.artemchep.keyguard.common.model.Log
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
interface GetInMemoryLogs : () -> Flow<ImmutableList<Log>>

View File

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

View File

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

View File

@ -5,13 +5,18 @@ import com.artemchep.keyguard.common.io.bind
import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.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)
}
}
}

View File

@ -0,0 +1,21 @@
package com.artemchep.keyguard.common.usecase.impl
import com.artemchep.keyguard.common.service.logging.inmemory.InMemoryLogRepository
import com.artemchep.keyguard.common.usecase.GetInMemoryLogsEnabled
import kotlinx.coroutines.flow.Flow
import org.kodein.di.DirectDI
import org.kodein.di.instance
class GetInMemoryLogsEnabledImpl(
private val inMemoryLogRepository: InMemoryLogRepository,
) : GetInMemoryLogsEnabled {
constructor(directDI: DirectDI) : this(
inMemoryLogRepository = directDI.instance(),
)
override val value: Boolean
get() = inMemoryLogRepository.isEnabled
override fun invoke(): Flow<Boolean> = inMemoryLogRepository
.getEnabled()
}

View File

@ -0,0 +1,17 @@
package com.artemchep.keyguard.common.usecase.impl
import com.artemchep.keyguard.common.service.logging.inmemory.InMemoryLogRepository
import com.artemchep.keyguard.common.usecase.GetInMemoryLogs
import org.kodein.di.DirectDI
import org.kodein.di.instance
class GetInMemoryLogsImpl(
private val inMemoryLogRepository: InMemoryLogRepository,
) : GetInMemoryLogs {
constructor(directDI: DirectDI) : this(
inMemoryLogRepository = directDI.instance(),
)
override fun invoke() = inMemoryLogRepository
.get()
}

View File

@ -0,0 +1,18 @@
package com.artemchep.keyguard.common.usecase.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.service.logging.inmemory.InMemoryLogRepository
import com.artemchep.keyguard.common.usecase.PutInMemoryLogsEnabled
import org.kodein.di.DirectDI
import org.kodein.di.instance
class PutInMemoryLogsEnabledImpl(
private val inMemoryLogRepository: InMemoryLogRepository,
) : PutInMemoryLogsEnabled {
constructor(directDI: DirectDI) : this(
inMemoryLogRepository = directDI.instance(),
)
override fun invoke(inMemoryLogsEnabled: Boolean): IO<Unit> = inMemoryLogRepository
.setEnabled(inMemoryLogsEnabled)
}

View File

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

View File

@ -68,6 +68,7 @@ import com.artemchep.keyguard.common.usecase.CopyCipherById
import com.artemchep.keyguard.common.usecase.DownloadAttachment
import com.artemchep.keyguard.common.usecase.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>()

View File

@ -13,6 +13,7 @@ import com.artemchep.keyguard.common.io.retry
import com.artemchep.keyguard.common.io.shared
import com.artemchep.keyguard.common.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()
}
}
}

View File

@ -63,6 +63,7 @@ import com.artemchep.keyguard.feature.home.settings.component.settingKeepScreenO
import com.artemchep.keyguard.feature.home.settings.component.settingLaunchAppPicker
import com.artemchep.keyguard.feature.home.settings.component.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,

View File

@ -0,0 +1,66 @@
package com.artemchep.keyguard.feature.home.settings.component
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import com.artemchep.keyguard.feature.logs.LogsRoute
import com.artemchep.keyguard.feature.navigation.LocalNavigationController
import com.artemchep.keyguard.feature.navigation.NavigationIntent
import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.res.*
import com.artemchep.keyguard.ui.FlatItem
import com.artemchep.keyguard.ui.icons.ChevronIcon
import com.artemchep.keyguard.ui.icons.Stub
import com.artemchep.keyguard.ui.icons.icon
import org.jetbrains.compose.resources.stringResource
import kotlinx.coroutines.flow.flowOf
import org.kodein.di.DirectDI
fun settingLogsProvider(
directDI: DirectDI,
) = settingLogsProvider()
fun settingLogsProvider(): SettingComponent = kotlin.run {
val item = SettingIi(
search = SettingIi.Search(
group = "about",
tokens = listOf(
"logs",
"debug",
"crash",
),
),
) {
val navigationController by rememberUpdatedState(LocalNavigationController.current)
SettingLogs(
onClick = {
val intent = NavigationIntent.NavigateToRoute(
route = LogsRoute,
)
navigationController.queue(intent)
},
)
}
flowOf(item)
}
@Composable
private fun SettingLogs(
onClick: (() -> Unit)?,
) {
FlatItem(
leading = icon<RowScope>(Icons.Stub),
trailing = {
ChevronIcon()
},
title = {
Text(
text = stringResource(Res.string.pref_item_logs_title),
)
},
onClick = onClick,
)
}

View File

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

View File

@ -0,0 +1,34 @@
package com.artemchep.keyguard.feature.logs
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.AnnotatedString
import arrow.optics.optics
import com.artemchep.keyguard.common.service.logging.LogLevel
import java.util.UUID
@Immutable
@optics
sealed interface LogsItem {
companion object
val id: String
@Immutable
data class Section(
override val id: String = UUID.randomUUID().toString(),
val text: String? = null,
val caps: Boolean = true,
) : LogsItem {
companion object
}
@Immutable
data class Value(
override val id: String,
val text: AnnotatedString,
val level: LogLevel,
val time: String,
) : LogsItem {
companion object;
}
}

View File

@ -0,0 +1,11 @@
package com.artemchep.keyguard.feature.logs
import androidx.compose.runtime.Composable
import com.artemchep.keyguard.feature.navigation.Route
object LogsRoute : Route {
@Composable
override fun Content() {
LogsScreen()
}
}

View File

@ -0,0 +1,289 @@
package com.artemchep.keyguard.feature.logs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material.icons.outlined.SaveAlt
import androidx.compose.material.icons.outlined.Stop
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import com.artemchep.keyguard.common.model.fold
import com.artemchep.keyguard.common.service.logging.LogLevel
import com.artemchep.keyguard.feature.EmptyView
import com.artemchep.keyguard.feature.home.vault.component.Section
import com.artemchep.keyguard.feature.navigation.NavigationIcon
import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.res.*
import com.artemchep.keyguard.ui.DefaultFab
import com.artemchep.keyguard.ui.FabState
import com.artemchep.keyguard.ui.FlatItemLayout
import com.artemchep.keyguard.ui.MediumEmphasisAlpha
import com.artemchep.keyguard.ui.ScaffoldLazyColumn
import com.artemchep.keyguard.ui.skeleton.SkeletonItem
import com.artemchep.keyguard.ui.theme.Dimens
import com.artemchep.keyguard.ui.theme.combineAlpha
import com.artemchep.keyguard.ui.theme.infoContainer
import com.artemchep.keyguard.ui.theme.onInfoContainer
import com.artemchep.keyguard.ui.theme.onWarningContainer
import com.artemchep.keyguard.ui.theme.warningContainer
import com.artemchep.keyguard.ui.toolbar.LargeToolbar
import com.artemchep.keyguard.ui.toolbar.util.ToolbarBehavior
import org.jetbrains.compose.resources.stringResource
@Composable
fun LogsScreen() {
val modifier = Modifier
val scrollBehavior = ToolbarBehavior.behavior()
val loadableState = produceLogsState()
loadableState.fold(
ifLoading = {
LogsScreenSkeleton(
modifier = modifier,
scrollBehavior = scrollBehavior,
)
},
ifOk = { state ->
LogsScreenContent(
modifier = modifier,
scrollBehavior = scrollBehavior,
state = state,
)
},
)
}
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class,
)
@Composable
private fun LogsScreenSkeleton(
modifier: Modifier,
scrollBehavior: TopAppBarScrollBehavior,
) {
ScaffoldLazyColumn(
modifier = modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
topAppBarScrollBehavior = scrollBehavior,
topBar = {
LargeToolbar(
title = {
Text(stringResource(Res.string.logs_header_title))
},
navigationIcon = {
NavigationIcon()
},
scrollBehavior = scrollBehavior,
)
},
) {
item("skeleton") {
SkeletonItem()
}
}
}
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class,
)
@Composable
private fun LogsScreenContent(
modifier: Modifier,
scrollBehavior: TopAppBarScrollBehavior,
state: LogsState,
) {
val contentState = state.contentFlow.collectAsState()
ScaffoldLazyColumn(
modifier = modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
topAppBarScrollBehavior = scrollBehavior,
topBar = {
LargeToolbar(
title = {
Text(stringResource(Res.string.logs_header_title))
},
navigationIcon = {
NavigationIcon()
},
actions = {
val exportState = state.exportFlow.collectAsState()
TextButton(
enabled = exportState.value.onExportClick != null,
onClick = {
exportState.value.onExportClick?.invoke()
},
) {
Icon(
imageVector = Icons.Outlined.SaveAlt,
contentDescription = null,
)
Spacer(
modifier = Modifier
.width(Dimens.buttonIconPadding),
)
Text(
text = stringResource(Res.string.logs_export_button),
)
}
},
scrollBehavior = scrollBehavior,
)
},
floatingActionState = run {
val switchState = state.switchFlow.collectAsState()
val fabState = FabState(
onClick = switchState.value.onToggle,
model = switchState.value.checked,
)
rememberUpdatedState(fabState)
},
floatingActionButton = {
val checked = this.state.value?.model == true
DefaultFab(
icon = {
Icon(
imageVector = if (!checked) {
Icons.Outlined.PlayArrow
} else {
Icons.Outlined.Stop
},
contentDescription = null,
)
},
) {
Text(
text = if (!checked) {
stringResource(Res.string.logs_start_recording_fab_title)
} else {
stringResource(Res.string.logs_stop_recording_fab_title)
},
)
}
},
) {
val items = contentState.value.items
if (items.isEmpty()) {
item("empty") {
EmptyView()
}
}
items(items, key = { it.id }) { item ->
LogItem(
modifier = Modifier
.animateItemPlacement(),
item = item,
)
}
}
}
@Composable
private fun LogItem(
modifier: Modifier,
item: LogsItem,
) = when (item) {
is LogsItem.Section -> LogItem(
modifier = modifier,
item = item,
)
is LogsItem.Value -> LogItem(
modifier = modifier,
item = item,
)
}
@Composable
private fun LogItem(
modifier: Modifier,
item: LogsItem.Section,
) {
Section(
modifier = modifier,
text = item.text,
caps = item.caps,
)
}
@Composable
private fun LogItem(
modifier: Modifier,
item: LogsItem.Value,
) {
FlatItemLayout(
modifier = modifier,
content = {
Row {
val levelContainerColor = when (item.level) {
LogLevel.ERROR -> MaterialTheme.colorScheme.errorContainer
LogLevel.WARNING -> MaterialTheme.colorScheme.warningContainer
LogLevel.INFO -> MaterialTheme.colorScheme.infoContainer
LogLevel.DEBUG -> MaterialTheme.colorScheme.surfaceContainer
}
val levelContentColor = when (item.level) {
LogLevel.ERROR -> MaterialTheme.colorScheme.onErrorContainer
LogLevel.WARNING -> MaterialTheme.colorScheme.onWarningContainer
LogLevel.INFO -> MaterialTheme.colorScheme.onInfoContainer
LogLevel.DEBUG -> MaterialTheme.colorScheme.onSurface
}
Box(
modifier = Modifier
.background(levelContainerColor, RoundedCornerShape(4.dp))
.size(24.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = item.level.letter,
color = levelContentColor,
style = MaterialTheme.typography.labelSmall,
)
}
Spacer(
modifier = Modifier
.width(16.dp),
)
Column {
Text(
text = item.text,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
)
Text(
text = item.time,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = LocalContentColor.current
.combineAlpha(MediumEmphasisAlpha),
)
}
}
},
enabled = true,
)
}

View File

@ -0,0 +1,32 @@
@file:JvmName("GeneratorStateUtils")
package com.artemchep.keyguard.feature.logs
import androidx.compose.runtime.Immutable
import com.artemchep.keyguard.common.service.permission.PermissionState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.StateFlow
@Immutable
data class LogsState(
val contentFlow: StateFlow<Content>,
val exportFlow: StateFlow<Export>,
val switchFlow: StateFlow<Switch>,
) {
@Immutable
data class Content(
val items: ImmutableList<LogsItem>,
)
@Immutable
data class Export(
val writePermission: PermissionState,
val onExportClick: (() -> Unit)? = null,
)
@Immutable
data class Switch(
val checked: Boolean,
val onToggle: (() -> Unit)? = null,
)
}

View File

@ -0,0 +1,169 @@
package com.artemchep.keyguard.feature.logs
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import arrow.core.partially1
import com.artemchep.keyguard.common.io.effectTap
import com.artemchep.keyguard.common.io.launchIn
import com.artemchep.keyguard.common.model.Loadable
import com.artemchep.keyguard.common.model.ToastMessage
import com.artemchep.keyguard.common.service.clipboard.ClipboardService
import com.artemchep.keyguard.common.service.permission.Permission
import com.artemchep.keyguard.common.service.permission.PermissionService
import com.artemchep.keyguard.common.service.permission.PermissionState
import com.artemchep.keyguard.common.usecase.DateFormatter
import com.artemchep.keyguard.common.usecase.ExportLogs
import com.artemchep.keyguard.common.usecase.GetGeneratorHistory
import com.artemchep.keyguard.common.usecase.GetInMemoryLogs
import com.artemchep.keyguard.common.usecase.GetInMemoryLogsEnabled
import com.artemchep.keyguard.common.usecase.PutInMemoryLogsEnabled
import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistory
import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistoryById
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.res.*
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.kodein.di.compose.localDI
import org.kodein.di.direct
import org.kodein.di.instance
private const val MESSAGE_LENGTH_LIMIT = 300
@Composable
fun produceLogsState() = with(localDI().direct) {
produceLogsState(
getGeneratorHistory = instance(),
removeGeneratorHistory = instance(),
removeGeneratorHistoryById = instance(),
dateFormatter = instance(),
clipboardService = instance(),
getInMemoryLogs = instance(),
getInMemoryLogsEnabled = instance(),
putInMemoryLogsEnabled = instance(),
permissionService = instance(),
exportLogs = instance(),
)
}
@Composable
fun produceLogsState(
getGeneratorHistory: GetGeneratorHistory,
removeGeneratorHistory: RemoveGeneratorHistory,
removeGeneratorHistoryById: RemoveGeneratorHistoryById,
dateFormatter: DateFormatter,
clipboardService: ClipboardService,
getInMemoryLogs: GetInMemoryLogs,
getInMemoryLogsEnabled: GetInMemoryLogsEnabled,
putInMemoryLogsEnabled: PutInMemoryLogsEnabled,
permissionService: PermissionService,
exportLogs: ExportLogs,
): Loadable<LogsState> = produceScreenState(
initial = Loadable.Loading,
key = "generator_history",
args = arrayOf(
getGeneratorHistory,
removeGeneratorHistory,
removeGeneratorHistoryById,
dateFormatter,
clipboardService,
),
) {
fun onExport(
) {
exportLogs()
.effectTap {
val msg = ToastMessage(
title = translate(Res.string.logs_export_success),
type = ToastMessage.Type.SUCCESS,
)
message(msg)
}
.launchIn(appScope)
}
val writeDownloadsPermissionFlow = permissionService
.getState(Permission.WRITE_EXTERNAL_STORAGE)
val itemsRawFlow = getInMemoryLogs()
.shareInScreenScope()
val itemsFlow = itemsRawFlow
.map { logs ->
val items = logs
.mapIndexed { index, log ->
val text = buildAnnotatedString {
withStyle(
SpanStyle(
fontWeight = FontWeight.Bold,
),
) {
append(log.tag)
}
append(" ")
// Append only a part of a message if that is
// too long. This is needed because rendering
// huge text will freeze the UI.
if (log.message.length > MESSAGE_LENGTH_LIMIT) {
val part = log.message.take(MESSAGE_LENGTH_LIMIT)
append(part)
append("... [truncated]")
} else {
append(log.message)
}
}
val time = dateFormatter.formatDateTime(log.createdAt)
LogsItem.Value(
id = index.toString(),
text = text,
level = log.level,
time = time,
)
}
.toPersistentList()
LogsState.Content(
items = items,
)
}
.stateIn(screenScope)
val switchFlow = getInMemoryLogsEnabled()
.map { enabled ->
LogsState.Switch(
checked = enabled,
onToggle = {
putInMemoryLogsEnabled(!enabled)
.launchIn(appScope)
},
)
}
.stateIn(screenScope)
val exportFlow = writeDownloadsPermissionFlow
.map { writeDownloadsPermission ->
val onExportClick = when (writeDownloadsPermission) {
is PermissionState.Granted -> ::onExport
is PermissionState.Declined -> {
// lambda
writeDownloadsPermission.ask
.partially1(context)
}
}
LogsState.Export(
writePermission = writeDownloadsPermission,
onExportClick = onExportClick,
)
}
.stateIn(screenScope)
val state = LogsState(
contentFlow = itemsFlow,
switchFlow = switchFlow,
exportFlow = exportFlow,
)
flowOf(Loadable.Ok(state))
}

View File

@ -2,6 +2,7 @@ package com.artemchep.keyguard.provider.bitwarden.api
import com.artemchep.keyguard.common.exception.HttpException
import com.artemchep.keyguard.common.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
}

View File

@ -11,6 +11,7 @@ import com.artemchep.keyguard.common.io.handleErrorTap
import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.io.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)
}

View File

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

View File

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

View File

@ -4,18 +4,23 @@ import com.artemchep.keyguard.common.model.AddCipherUsedPasskeyHistoryRequest
import com.artemchep.keyguard.common.model.CipherHistoryType
import com.artemchep.keyguard.common.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,

View File

@ -0,0 +1,74 @@
package com.artemchep.keyguard.provider.bitwarden.usecase
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.bind
import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.service.dirs.DirsService
import com.artemchep.keyguard.common.service.zip.ZipConfig
import com.artemchep.keyguard.common.service.zip.ZipEntry
import com.artemchep.keyguard.common.service.zip.ZipService
import com.artemchep.keyguard.common.usecase.DateFormatter
import com.artemchep.keyguard.common.usecase.ExportLogs
import com.artemchep.keyguard.common.usecase.GetInMemoryLogs
import kotlinx.coroutines.flow.first
import kotlinx.datetime.Clock
import org.kodein.di.DirectDI
import org.kodein.di.instance
/**
* @author Artem Chepurnyi
*/
class ExportLogsImpl(
private val dirsService: DirsService,
private val zipService: ZipService,
private val dateFormatter: DateFormatter,
private val getInMemoryLogs: GetInMemoryLogs,
) : ExportLogs {
companion object {
private const val TAG = "ExportLogs.bitwarden"
}
constructor(directDI: DirectDI) : this(
dirsService = directDI.instance(),
zipService = directDI.instance(),
dateFormatter = directDI.instance(),
getInMemoryLogs = directDI.instance(),
)
override fun invoke(
): IO<Unit> = ioEffect {
val logs = getInMemoryLogs()
.first()
// Map log data to the plain text
val txt = kotlin.run {
logs
.joinToString(separator = "\n") { log ->
val dateTime = dateFormatter.formatDateTimeMachine(log.createdAt)
"$dateTime ${log.level.letter} ${log.tag} ${log.message}"
}
}
val fileName = kotlin.run {
val now = Clock.System.now()
val dt = dateFormatter.formatDateTimeMachine(now)
"keyguard_logs_$dt.zip"
}
dirsService.saveToDownloads(fileName) { os ->
zipService.zip(
outputStream = os,
config = ZipConfig(
),
entries = listOf(
ZipEntry(
name = "logs.txt",
stream = {
txt.byteInputStream()
},
),
),
)
}.bind()
}
}

View File

@ -8,6 +8,7 @@ import com.artemchep.keyguard.common.usecase.RemoveAccountById
import com.artemchep.keyguard.common.usecase.Watchdog
import com.artemchep.keyguard.common.usecase.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)
}

View File

@ -3,6 +3,7 @@ package com.artemchep.keyguard.provider.bitwarden.usecase
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.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()
}

View File

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

View File

@ -9,6 +9,8 @@ import com.artemchep.keyguard.common.io.measure
import com.artemchep.keyguard.common.io.toIO
import com.artemchep.keyguard.common.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,

View File

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

View File

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

View File

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

View File

@ -27,7 +27,10 @@ import com.artemchep.keyguard.common.service.justgetmydata.impl.JustGetMyDataSer
import com.artemchep.keyguard.common.service.keyvalue.KeyValueStore
import com.artemchep.keyguard.common.service.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,