From 2d5a3a473cb00edad6bd0e5f8028638ff81a8b47 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Oct 2023 16:25:27 +0200 Subject: [PATCH] Pin setup with fake lock --- .../io/element/android/appnav/BackstackExt.kt | 21 ++++++++ .../android/appnav/LoggedInFlowNode.kt | 47 ++++++++++++----- .../io/element/android/appnav/RootFlowNode.kt | 1 + .../android/appnav/loggedin/LoggedInNode.kt | 5 +- .../android/features/pin/api/PinState.kt | 22 ++++++++ .../features/pin/api/PinStateDataSource.kt | 26 ++++++++++ .../pin/impl/auth/PinAuthenticationEvents.kt | 2 +- .../impl/auth/PinAuthenticationPresenter.kt | 9 ++-- .../auth/PinAuthenticationStateProvider.kt | 1 - .../pin/impl/auth/PinAuthenticationView.kt | 51 ++++++++++++++++--- .../impl/state/DefaultPinStateDataSource.kt | 42 +++++++++++++++ .../molecules/IconTitleSubtitleMolecule.kt | 24 +++++---- 12 files changed, 211 insertions(+), 40 deletions(-) create mode 100644 features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt create mode 100644 features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt index 36b267debb..73bb9b9b85 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt @@ -16,6 +16,12 @@ package io.element.android.appnav +import android.content.Context +import android.content.ContextWrapper +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.NewRoot import com.bumble.appyx.navmodel.backstack.operation.Remove @@ -41,3 +47,18 @@ fun BackStack.removeLast(element: T) { accept(Remove(lastExpectedNavElement.key)) } +@Composable +fun FinishActivityBackHandler(enabled: Boolean = true) { + + fun Context.findActivity(): ComponentActivity? = when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null + } + + val context = LocalContext.current + BackHandler(enabled = enabled) { + context.findActivity()?.finish() + } +} + diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index cf67a765d7..276f0ebbde 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -50,6 +50,9 @@ import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.invitelist.api.InviteListEntryPoint import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.pin.api.PinEntryPoint +import io.element.android.features.pin.api.PinState +import io.element.android.features.pin.api.PinStateDataSource import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -90,6 +93,8 @@ class LoggedInFlowNode @AssistedInject constructor( private val networkMonitor: NetworkMonitor, private val notificationDrawerManager: NotificationDrawerManager, private val ftueState: FtueState, + private val pinEntryPoint: PinEntryPoint, + private val pinStateDataSource: PinStateDataSource, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( @@ -98,7 +103,7 @@ class LoggedInFlowNode @AssistedInject constructor( savedStateMap = buildContext.savedStateMap, ), permanentNavModel = PermanentNavModel( - NavTarget.Permanent, + navTargets = setOf(NavTarget.LoggedInPermanent, NavTarget.LockPermanent), savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -130,6 +135,7 @@ class LoggedInFlowNode @AssistedInject constructor( } }, onStop = { + pinStateDataSource.lock() //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. coroutineScope.launch { syncService.stopSync() @@ -167,7 +173,10 @@ class LoggedInFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - data object Permanent : NavTarget + data object LoggedInPermanent : NavTarget + + @Parcelize + data object LockPermanent : NavTarget @Parcelize data object RoomList : NavTarget @@ -196,9 +205,12 @@ class LoggedInFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Permanent -> { + NavTarget.LoggedInPermanent -> { createNode(buildContext) } + NavTarget.LockPermanent -> { + pinEntryPoint.nodeBuilder(this, buildContext).build() + } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { override fun onRoomClicked(roomId: RoomId) { @@ -324,17 +336,24 @@ class LoggedInFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { - Children( - navModel = backstack, - modifier = Modifier, - // Animate navigation to settings and to a room - transitionHandler = rememberDefaultTransitionHandler(), - ) - - val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState() - - if (!isFtueDisplayed) { - PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.Permanent) + val pinState by pinStateDataSource.pinState.collectAsState() + when (pinState) { + PinState.Unlocked -> { + Children( + navModel = backstack, + modifier = Modifier, + // Animate navigation to settings and to a room + transitionHandler = rememberDefaultTransitionHandler(), + ) + val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState() + if (!isFtueDisplayed) { + PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) + } + } + PinState.Locked -> { + FinishActivityBackHandler() + PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent) + } } } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 94f344be7e..403b53f0e1 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -44,6 +44,7 @@ import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcActionFlow +import io.element.android.features.pin.api.PinEntryPoint import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt index 6950b9b699..5ddbb164d8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt @@ -31,7 +31,10 @@ class LoggedInNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val loggedInPresenter: LoggedInPresenter, -) : Node(buildContext, plugins = plugins) { +) : Node( + buildContext = buildContext, + plugins = plugins +) { @Composable override fun View(modifier: Modifier) { diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt new file mode 100644 index 0000000000..0ff1b0b3d5 --- /dev/null +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.pin.api + +sealed interface PinState { + data object Unlocked : PinState + data object Locked : PinState +} diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt new file mode 100644 index 0000000000..c0b52af8b7 --- /dev/null +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.pin.api + +import kotlinx.coroutines.flow.StateFlow + +interface PinStateDataSource { + val pinState: StateFlow + + fun lock() + fun unlock() +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt index a56412aa6e..110c62660a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt @@ -17,5 +17,5 @@ package io.element.android.features.pin.impl.auth sealed interface PinAuthenticationEvents { - object MyEvent : PinAuthenticationEvents + data object Unlock : PinAuthenticationEvents } diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt index 79eace0072..36970f34ab 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt @@ -17,20 +17,21 @@ package io.element.android.features.pin.impl.auth import androidx.compose.runtime.Composable +import io.element.android.features.pin.api.PinStateDataSource import io.element.android.libraries.architecture.Presenter import javax.inject.Inject -class PinAuthenticationPresenter @Inject constructor() : Presenter { +class PinAuthenticationPresenter @Inject constructor( + private val pinStateDataSource: PinStateDataSource, +) : Presenter { @Composable override fun present(): PinAuthenticationState { - fun handleEvents(event: PinAuthenticationEvents) { when (event) { - PinAuthenticationEvents.MyEvent -> Unit + PinAuthenticationEvents.Unlock -> pinStateDataSource.unlock() } } - return PinAuthenticationState( eventSink = ::handleEvents ) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt index 7aea002ee9..8e3f45ac07 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt @@ -22,7 +22,6 @@ open class PinAuthenticationStateProvider : PreviewParameterProvider get() = sequenceOf( aPinAuthenticationState(), - // Add other states here ) } diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt index b318eebe79..9aa2527099 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt @@ -16,29 +16,64 @@ package io.element.android.features.pin.impl.auth -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent @Composable fun PinAuthenticationView( state: PinAuthenticationState, modifier: Modifier = Modifier, ) { - Box(modifier, contentAlignment = Alignment.Center) { - Text( - "PinAuthentication feature view", - color = MaterialTheme.colorScheme.primary, + Surface(modifier) { + HeaderFooterPage( + modifier = Modifier + .systemBarsPadding() + .fillMaxSize(), + header = { PinAuthenticationHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, + footer = { PinAuthenticationFooter(state) }, ) } } +@Composable +private fun PinAuthenticationHeader( + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier, + title = "Element X is locked", + subTitle = null, + iconImageVector = Icons.Default.Lock, + ) +} + +@Composable +private fun PinAuthenticationFooter(state: PinAuthenticationState) { + Button( + modifier = Modifier.fillMaxWidth(), + text = "Unlock", + onClick = { + state.eventSink(PinAuthenticationEvents.Unlock) + } + ) +} + @Composable @PreviewsDayNight fun PinAuthenticationViewLightPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) = diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt new file mode 100644 index 0000000000..e0f8c6e6f9 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.pin.impl.state + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.pin.api.PinState +import io.element.android.features.pin.api.PinStateDataSource +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultPinStateDataSource @Inject constructor() : PinStateDataSource { + + private val _pinState = MutableStateFlow(PinState.Locked) + override val pinState: StateFlow = _pinState + + override fun unlock() { + _pinState.value = PinState.Unlocked + } + + override fun lock() { + _pinState.value = PinState.Locked + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt index 88bc258348..52d8eb5baf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme @@ -49,7 +49,7 @@ import io.element.android.libraries.theme.ElementTheme @Composable fun IconTitleSubtitleMolecule( title: String, - subTitle: String, + subTitle: String?, modifier: Modifier = Modifier, iconResourceId: Int? = null, iconImageVector: ImageVector? = null, @@ -73,14 +73,16 @@ fun IconTitleSubtitleMolecule( style = ElementTheme.typography.fontHeadingMdBold, color = MaterialTheme.colorScheme.primary, ) - Spacer(Modifier.height(8.dp)) - Text( - text = subTitle, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, - ) + if (subTitle != null) { + Spacer(Modifier.height(8.dp)) + Text( + text = subTitle, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.secondary, + ) + } } } @@ -90,6 +92,6 @@ internal fun IconTitleSubtitleMoleculePreview() = ElementPreview { IconTitleSubtitleMolecule( iconResourceId = R.drawable.ic_compound_chat, title = "Title", - subTitle = "Sub iitle", + subTitle = "Subtitle", ) }