improvement(Desktop): Support Lock after a delay #453
This commit is contained in:
parent
8b40f1ad80
commit
3999b41d39
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Reference in New Issue