improvement(Desktop): Support Lock after a delay #453

This commit is contained in:
Artem Chepurnoy 2024-07-07 14:41:52 +03:00
parent 8b40f1ad80
commit 3999b41d39
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
7 changed files with 177 additions and 84 deletions

View File

@ -122,6 +122,8 @@ kotlin {
api(libs.arrow.arrow.optics)
api(libs.kodein.kodein.di)
api(libs.kodein.kodein.di.framework.compose)
api(libs.androidx.lifecycle.common)
api(libs.androidx.lifecycle.runtime)
api(libs.ktor.ktor.client.core)
api(libs.ktor.ktor.client.logging)
api(libs.ktor.ktor.client.content.negotiation)

View File

@ -1,39 +0,0 @@
package com.artemchep.keyguard.platform.lifecycle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
actual val LocalLifecycleStateFlow: StateFlow<LeLifecycleState>
@Composable
get() {
val lifecycleOwner = LocalLifecycleOwner.current
val sink = remember(lifecycleOwner) {
val initialState = lifecycleOwner.lifecycle.currentState
MutableStateFlow(initialState.toCommon())
}
DisposableEffect(lifecycleOwner, sink) {
val observer = LifecycleEventObserver { _, event ->
val newState = event.targetState
sink.value = newState.toCommon()
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
return sink
}
fun Lifecycle.State.toCommon() = when (this) {
Lifecycle.State.DESTROYED -> LeLifecycleState.DESTROYED
Lifecycle.State.INITIALIZED -> LeLifecycleState.INITIALIZED
Lifecycle.State.CREATED -> LeLifecycleState.CREATED
Lifecycle.State.STARTED -> LeLifecycleState.STARTED
Lifecycle.State.RESUMED -> LeLifecycleState.RESUMED
}

View File

@ -1,7 +1,39 @@
package com.artemchep.keyguard.platform.lifecycle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@get:Composable
expect val LocalLifecycleStateFlow: StateFlow<LeLifecycleState>
val LocalLifecycleStateFlow: StateFlow<LeLifecycleState>
@Composable
get() {
val lifecycleOwner = LocalLifecycleOwner.current
val sink = remember(lifecycleOwner) {
val initialState = lifecycleOwner.lifecycle.currentState
MutableStateFlow(initialState.toCommon())
}
DisposableEffect(lifecycleOwner, sink) {
val observer = LifecycleEventObserver { _, event ->
val newState = event.targetState
sink.value = newState.toCommon()
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
return sink
}
fun Lifecycle.State.toCommon() = when (this) {
Lifecycle.State.DESTROYED -> LeLifecycleState.DESTROYED
Lifecycle.State.INITIALIZED -> LeLifecycleState.INITIALIZED
Lifecycle.State.CREATED -> LeLifecycleState.CREATED
Lifecycle.State.STARTED -> LeLifecycleState.STARTED
Lifecycle.State.RESUMED -> LeLifecycleState.RESUMED
}

View File

@ -0,0 +1,80 @@
package com.artemchep.keyguard.platform.lifecycle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import com.artemchep.keyguard.common.service.crypto.CryptoGenerator
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class LePlatformLifecycleProvider(
private val scope: CoroutineScope,
private val cryptoGenerator: CryptoGenerator,
) {
private val sink = MutableStateFlow<PersistentMap<String, LeLifecycleState>>(persistentMapOf())
val lifecycleStateFlow: StateFlow<LeLifecycleState> = sink
.map { state ->
state.values.maxOrNull()
?: LeLifecycleState.CREATED
}
.stateIn(
scope,
SharingStarted.Lazily,
initialValue = LeLifecycleState.INITIALIZED,
)
fun register(
lifecycleFlow: StateFlow<LeLifecycleState>,
): () -> Unit {
val id = cryptoGenerator.uuid()
val job = scope.launch {
lifecycleFlow
.onEach { lifecycleState ->
if (!isActive) {
return@onEach
}
sink.update { state ->
state.put(id, lifecycleState)
}
}
.collect()
}
return {
job.cancel()
// Remove the last known state out of the
// lifecycle state map.
sink.update { state ->
state.remove(id)
}
}
}
}
@Composable
fun LaunchLifecycleProviderEffect(
processLifecycleProvider: LePlatformLifecycleProvider,
) {
val lifecycleFlow = LocalLifecycleStateFlow
DisposableEffect(
processLifecycleProvider,
lifecycleFlow,
) {
val unregister = processLifecycleProvider.register(lifecycleFlow)
onDispose {
unregister()
}
}
}

View File

@ -1,15 +0,0 @@
package com.artemchep.keyguard.platform.lifecycle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
actual val LocalLifecycleStateFlow: StateFlow<LeLifecycleState>
@Composable
get() {
val sink = remember {
MutableStateFlow(LeLifecycleState.STARTED)
}
return sink
}

View File

@ -19,7 +19,12 @@ import androidx.compose.ui.window.isTraySupported
import androidx.compose.ui.window.rememberTrayState
import androidx.compose.ui.window.rememberWindowState
import com.artemchep.keyguard.common.AppWorker
import com.artemchep.keyguard.common.io.attempt
import com.artemchep.keyguard.common.io.bind
import com.artemchep.keyguard.common.io.effectMap
import com.artemchep.keyguard.common.io.flatten
import com.artemchep.keyguard.common.io.launchIn
import com.artemchep.keyguard.common.io.toIO
import com.artemchep.keyguard.common.model.MasterSession
import com.artemchep.keyguard.common.model.PersistedSession
import com.artemchep.keyguard.common.model.ToastMessage
@ -27,8 +32,10 @@ import com.artemchep.keyguard.common.service.vault.KeyReadWriteRepository
import com.artemchep.keyguard.common.usecase.GetAccounts
import com.artemchep.keyguard.common.usecase.GetCloseToTray
import com.artemchep.keyguard.common.usecase.GetLocale
import com.artemchep.keyguard.common.usecase.GetVaultLockAfterTimeout
import com.artemchep.keyguard.common.usecase.GetVaultPersist
import com.artemchep.keyguard.common.usecase.GetVaultSession
import com.artemchep.keyguard.common.usecase.PutVaultSession
import com.artemchep.keyguard.common.usecase.ShowMessage
import com.artemchep.keyguard.common.worker.Wrker
import com.artemchep.keyguard.core.session.diFingerprintRepositoryModule
@ -39,14 +46,19 @@ import com.artemchep.keyguard.desktop.util.navigateToFileInFileManager
import com.artemchep.keyguard.feature.favicon.Favicon
import com.artemchep.keyguard.feature.favicon.FaviconUrl
import com.artemchep.keyguard.feature.keyguard.AppRoute
import com.artemchep.keyguard.feature.localization.textResource
import com.artemchep.keyguard.feature.navigation.LocalNavigationBackHandler
import com.artemchep.keyguard.feature.navigation.NavigationController
import com.artemchep.keyguard.feature.navigation.NavigationIntent
import com.artemchep.keyguard.feature.navigation.NavigationNode
import com.artemchep.keyguard.feature.navigation.NavigationRouterBackHandler
import com.artemchep.keyguard.platform.CurrentPlatform
import com.artemchep.keyguard.platform.LeContext
import com.artemchep.keyguard.platform.Platform
import com.artemchep.keyguard.platform.lifecycle.LaunchLifecycleProviderEffect
import com.artemchep.keyguard.platform.lifecycle.LeLifecycleState
import com.artemchep.keyguard.platform.lifecycle.LePlatformLifecycleProvider
import com.artemchep.keyguard.platform.lifecycle.onState
import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.res.*
import com.artemchep.keyguard.ui.LocalComposeWindow
@ -60,7 +72,9 @@ import io.kamel.image.config.Default
import io.kamel.image.config.LocalKamelConfig
import io.ktor.http.Url
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
@ -69,6 +83,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.datetime.Clock
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
@ -104,7 +119,13 @@ fun main() {
}
}
val processLifecycleProvider = LePlatformLifecycleProvider(
scope = GlobalScope,
cryptoGenerator = appDi.direct.instance(),
)
val getVaultSession by appDi.di.instance<GetVaultSession>()
val putVaultSession by appDi.di.instance<PutVaultSession>()
val getVaultPersist by appDi.di.instance<GetVaultPersist>()
val keyReadWriteRepository by appDi.di.instance<KeyReadWriteRepository>()
getVaultSession()
@ -185,34 +206,38 @@ fun main() {
}
// timeout
// var timeoutJob: Job? = null
// val getVaultLockAfterTimeout: GetVaultLockAfterTimeout by instance()
// ProcessLifecycleOwner.get().bindBlock {
// timeoutJob?.cancel()
// timeoutJob = null
//
// try {
// // suspend forever
// suspendCancellableCoroutine<Unit> { }
// } finally {
// timeoutJob = getVaultLockAfterTimeout()
// .toIO()
// // Wait for the timeout duration.
// .effectMap { duration ->
// delay(duration)
// duration
// }
// .flatMap {
// // Clear the current session.
// val session = MasterSession.Empty(
// reason = "Locked due to inactivity."
// )
// putVaultSession(session)
// }
// .attempt()
// .launchIn(GlobalScope)
// }
// }
var timeoutJob: Job? = null
val getVaultLockAfterTimeout: GetVaultLockAfterTimeout by appDi.di.instance()
processLifecycleProvider.lifecycleStateFlow
.onState(minActiveState = LeLifecycleState.RESUMED) {
timeoutJob?.cancel()
timeoutJob = null
try {
// suspend forever
suspendCancellableCoroutine<Unit> { }
} finally {
timeoutJob = getVaultLockAfterTimeout()
.toIO()
// Wait for the timeout duration.
.effectMap { duration ->
delay(duration)
duration
}
.effectMap {
// Clear the current session.
val context = LeContext()
val session = MasterSession.Empty(
reason = textResource(Res.string.lock_reason_inactivity, context),
)
putVaultSession(session)
}
.flatten()
.attempt()
.launchIn(GlobalScope)
}
}
.launchIn(GlobalScope)
val getCloseToTray: GetCloseToTray = appDi.direct.instance()
@ -265,6 +290,7 @@ fun main() {
}
if (isWindowOpenState.value) {
KeyguardWindow(
processLifecycleProvider = processLifecycleProvider,
onCloseRequest = {
val shouldCloseToTray = getCloseToTrayState.value
if (shouldCloseToTray) {
@ -281,6 +307,7 @@ fun main() {
@Composable
private fun ApplicationScope.KeyguardWindow(
processLifecycleProvider: LePlatformLifecycleProvider,
onCloseRequest: () -> Unit,
) {
val windowState = rememberWindowState()
@ -290,6 +317,10 @@ private fun ApplicationScope.KeyguardWindow(
state = windowState,
title = "Keyguard",
) {
LaunchLifecycleProviderEffect(
processLifecycleProvider = processLifecycleProvider,
)
KeyguardTheme {
val containerColor = LocalBackgroundManager.current.colorHighest
val containerColorAnimatedState = animateColorAsState(containerColor)

View File

@ -165,6 +165,8 @@ androidx-credentials = { module = "androidx.credentials:credentials", version.re
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "androidxDatastore" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspresso" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExtJUnit" }
androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidxLifecycle" }
androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidxLifecycle" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }