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..e7bf9f200f 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 MoveActivityToBackgroundBackHandler(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()?.moveTaskToBack(false) + } +} + 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 72fd2461d2..82786b1a0c 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.PinStateService 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 pinStateService: PinStateService, 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, @@ -129,9 +134,19 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.Ftue) } }, - onStop = { - //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. + onResume = { coroutineScope.launch { + pinStateService.entersForeground() + } + }, + onPause = { + coroutineScope.launch { + pinStateService.entersBackground() + } + }, + onStop = { + coroutineScope.launch { + //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. syncService.stopSync() } }, @@ -167,7 +182,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 +214,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.createNode(this, buildContext) + } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { override fun onRoomClicked(roomId: RoomId) { @@ -324,17 +345,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 pinStateService.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 -> { + MoveActivityToBackgroundBackHandler() + PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent) + } } } } 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/build.gradle.kts b/build.gradle.kts index f08c023b1d..fcd67f07fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -250,6 +250,8 @@ koverMerged { excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" // Some options can't be tested at the moment excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" + // Temporary until we have actually something to test. + excludes += "io.element.android.features.pin.impl.*Presenter" } bound { minValue = 85 diff --git a/features/pin/api/build.gradle.kts b/features/pin/api/build.gradle.kts new file mode 100644 index 0000000000..95b062b0c8 --- /dev/null +++ b/features/pin/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.pin.api" +} + +dependencies { + implementation(projects.libraries.architecture) +} diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt new file mode 100644 index 0000000000..1fe3caf574 --- /dev/null +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt @@ -0,0 +1,21 @@ +/* + * 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 io.element.android.libraries.architecture.SimpleFeatureEntryPoint + +interface PinEntryPoint : SimpleFeatureEntryPoint 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/PinStateService.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt new file mode 100644 index 0000000000..4ecb473c18 --- /dev/null +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt @@ -0,0 +1,27 @@ +/* + * 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 PinStateService { + val pinState: StateFlow + + suspend fun entersForeground() + suspend fun entersBackground() + suspend fun unlock() +} diff --git a/features/pin/impl/build.gradle.kts b/features/pin/impl/build.gradle.kts new file mode 100644 index 0000000000..6cfc9fce11 --- /dev/null +++ b/features/pin/impl/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.pin.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + api(projects.features.pin.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.featureflag.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + + ksp(libs.showkase.processor) +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt new file mode 100644 index 0000000000..920691cad2 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt @@ -0,0 +1,33 @@ +/* + * 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 + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.pin.api.PinEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultPinEntryPoint @Inject constructor() : PinEntryPoint { + + override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + return parentNode.createNode(buildContext) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt new file mode 100644 index 0000000000..a76504ce8a --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt @@ -0,0 +1,78 @@ +/* + * 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 + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.pin.impl.auth.PinAuthenticationNode +import io.element.android.features.pin.impl.create.CreatePinNode +import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import kotlinx.parcelize.Parcelize + +@ContributesNode(AppScope::class) +class PinFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Auth, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Auth : NavTarget + + @Parcelize + data object Create : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Auth -> { + createNode(buildContext) + } + NavTarget.Create -> { + createNode(buildContext) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler(), + ) + } +} 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 new file mode 100644 index 0000000000..110c62660a --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt @@ -0,0 +1,21 @@ +/* + * 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.auth + +sealed interface PinAuthenticationEvents { + data object Unlock : PinAuthenticationEvents +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt new file mode 100644 index 0000000000..b5dab44c96 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt @@ -0,0 +1,44 @@ +/* + * 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.auth + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.AppScope + +@ContributesNode(AppScope::class) +class PinAuthenticationNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: PinAuthenticationPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + PinAuthenticationView( + state = state, + modifier = modifier + ) + } +} 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 new file mode 100644 index 0000000000..5e7e274ba7 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt @@ -0,0 +1,43 @@ +/* + * 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.auth + +import androidx.compose.runtime.Composable +import io.element.android.features.pin.api.PinStateService +import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class PinAuthenticationPresenter @Inject constructor( + private val pinStateService: PinStateService, + private val coroutineScope: CoroutineScope, +) : Presenter { + + @Composable + override fun present(): PinAuthenticationState { + + fun handleEvents(event: PinAuthenticationEvents) { + when (event) { + PinAuthenticationEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } + } + } + return PinAuthenticationState( + eventSink = ::handleEvents + ) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt new file mode 100644 index 0000000000..2df1e50f83 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt @@ -0,0 +1,21 @@ +/* + * 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.auth + +data class PinAuthenticationState( + val eventSink: (PinAuthenticationEvents) -> Unit +) 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 new file mode 100644 index 0000000000..8e3f45ac07 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt @@ -0,0 +1,30 @@ +/* + * 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.auth + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class PinAuthenticationStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPinAuthenticationState(), + ) +} + +fun aPinAuthenticationState() = PinAuthenticationState( + eventSink = {} +) 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 new file mode 100644 index 0000000000..9fe689bb39 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt @@ -0,0 +1,84 @@ +/* + * 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.auth + +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.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +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.Button +import io.element.android.libraries.designsystem.theme.components.Surface + +@Composable +fun PinAuthenticationView( + state: PinAuthenticationState, + modifier: Modifier = Modifier, +) { + 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 +internal fun PinAuthenticationViewPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) { + ElementPreview { + PinAuthenticationView( + state = state, + ) + } +} + diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt new file mode 100644 index 0000000000..280856b5c8 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt @@ -0,0 +1,21 @@ +/* + * 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.create + +sealed interface CreatePinEvents { + object MyEvent : CreatePinEvents +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt new file mode 100644 index 0000000000..0ed0343a5b --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt @@ -0,0 +1,44 @@ +/* + * 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.create + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.AppScope + +@ContributesNode(AppScope::class) +class CreatePinNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: CreatePinPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + CreatePinView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt new file mode 100644 index 0000000000..d45257b4bd --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt @@ -0,0 +1,38 @@ +/* + * 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.create + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class CreatePinPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): CreatePinState { + + fun handleEvents(event: CreatePinEvents) { + when (event) { + CreatePinEvents.MyEvent -> Unit + } + } + + return CreatePinState( + eventSink = ::handleEvents + ) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt new file mode 100644 index 0000000000..c405db82ec --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt @@ -0,0 +1,21 @@ +/* + * 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.create + +data class CreatePinState( + val eventSink: (CreatePinEvents) -> Unit +) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt new file mode 100644 index 0000000000..4bff72023e --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt @@ -0,0 +1,31 @@ +/* + * 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.create + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class CreatePinStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aCreatePinState(), + // Add other states here + ) +} + +fun aCreatePinState() = CreatePinState( + eventSink = {} +) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt new file mode 100644 index 0000000000..efdbe62bfa --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt @@ -0,0 +1,52 @@ +/* + * 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.create + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +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 timber.log.Timber + +@Composable +fun CreatePinView( + state: CreatePinState, + modifier: Modifier = Modifier, +) { + Timber.d("CreatePinView: $state") + Box(modifier, contentAlignment = Alignment.Center) { + Text( + "CreatePin feature view", + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@Composable +@PreviewsDayNight +internal fun CreatePinViewLightPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) { + ElementPreview { + CreatePinView( + state = state, + ) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt new file mode 100644 index 0000000000..9accef2b80 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt @@ -0,0 +1,65 @@ +/* + * 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.PinStateService +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultPinStateService @Inject constructor( + private val featureFlagService: FeatureFlagService, +) : PinStateService { + + private val _pinState = MutableStateFlow(PinState.Unlocked) + override val pinState: StateFlow = _pinState + + private var lockJob: Job? = null + + override suspend fun unlock() { + if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { + _pinState.value = PinState.Unlocked + } + } + + override suspend fun entersForeground() { + lockJob?.cancel() + } + + override suspend fun entersBackground() = coroutineScope { + lockJob = launch { + if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { + delay(GRACE_PERIOD_IN_MILLIS) + _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", ) } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 8d35223986..121cf26271 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -49,4 +49,10 @@ enum class FeatureFlags( description = "Send and receive voice messages", defaultValue = false, ), + PinUnlock( + key = "feature.pinunlock", + title = "Pin unlock", + description = "Allow user to lock/unlock the app with a pin code or biometrics", + defaultValue = false, + ), } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 7ef10262c9..48f159de83 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -36,6 +36,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.Polls -> true FeatureFlags.NotificationSettings -> true FeatureFlags.VoiceMessages -> false + FeatureFlags.PinUnlock -> false } } else { false diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f1992899f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f19d53c688e3f862894775756f00040adb5cbba99de71c053ed503c1b8af9518 +size 15239 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f7a21c8025 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11c145595f7713bc7b66f9d07e917bca82d6e06a1b55367801eb4d2cbfef89b0 +size 14353 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e8dd1a28d7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9914a33ba23544bdfce1e21b52ad247024392730fb22b60bc9b6fa6440f004d4 +size 9216 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..157a7c52c3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ad524a918e499fcea6fd5293358167ff52f8877cc31b778c8def01925fa662f +size 8582 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-D_0_null,NEXUS_5,1.0,en].png index 9b578f2a48..8fb30525d0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5a887e4fa9170810c5567933b9c1a80c9ffbbb7b5b9f7187de9d0331482a78a -size 10619 +oid sha256:b514737761239d60aac83a2b5d822c1079b7029474097f5ea4a4a29a88174e81 +size 10687 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-N_1_null,NEXUS_5,1.0,en].png index 1d604d08ee..95a1fb946e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d65a93f6e93d3725f51bb48b1d7dabaac4c4b10cb0897024a02966c04bfc3d5 -size 10508 +oid sha256:d648d16fa94fa4add5784cb0ca32173cc447dfd592bcf88ff82b53894c02f137 +size 10527