diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 7ab3e92b..46249ed8 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -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) diff --git a/common/src/androidMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LocalLifecycleStateFlow.kt b/common/src/androidMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LocalLifecycleStateFlow.kt deleted file mode 100644 index dbe6fe87..00000000 --- a/common/src/androidMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LocalLifecycleStateFlow.kt +++ /dev/null @@ -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 - @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 -} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LeLifecycleProvider.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LeLifecycleProvider.kt index f4324703..d429b536 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LeLifecycleProvider.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LeLifecycleProvider.kt @@ -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 +val LocalLifecycleStateFlow: StateFlow + @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 +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LePlatformLifecycleProvider.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LePlatformLifecycleProvider.kt new file mode 100644 index 00000000..449c3997 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LePlatformLifecycleProvider.kt @@ -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>(persistentMapOf()) + + val lifecycleStateFlow: StateFlow = sink + .map { state -> + state.values.maxOrNull() + ?: LeLifecycleState.CREATED + } + .stateIn( + scope, + SharingStarted.Lazily, + initialValue = LeLifecycleState.INITIALIZED, + ) + + fun register( + lifecycleFlow: StateFlow, + ): () -> 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() + } + } +} diff --git a/common/src/desktopMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LocalLifecycleStateFlow.kt b/common/src/desktopMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LocalLifecycleStateFlow.kt deleted file mode 100644 index 32f6ab0f..00000000 --- a/common/src/desktopMain/kotlin/com/artemchep/keyguard/platform/lifecycle/LocalLifecycleStateFlow.kt +++ /dev/null @@ -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 - @Composable - get() { - val sink = remember { - MutableStateFlow(LeLifecycleState.STARTED) - } - return sink - } diff --git a/desktopApp/src/jvmMain/kotlin/com/artemchep/keyguard/Main.kt b/desktopApp/src/jvmMain/kotlin/com/artemchep/keyguard/Main.kt index ab9d32ba..6bc77fa0 100644 --- a/desktopApp/src/jvmMain/kotlin/com/artemchep/keyguard/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/artemchep/keyguard/Main.kt @@ -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() + val putVaultSession by appDi.di.instance() val getVaultPersist by appDi.di.instance() val keyReadWriteRepository by appDi.di.instance() 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 { } -// } 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 { } + } 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) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8ad22fdd..cd299910 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }