From 53feff04a184a1591d11b0cf3438c0b0c0efe10b Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Oct 2023 17:04:23 +0200 Subject: [PATCH 01/70] Pin : start create pin view --- .../lockscreen/impl/create/CreatePinView.kt | 71 +++++++++++++++---- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 120c0b6079..d86c2f296d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -14,31 +14,77 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.features.lockscreen.impl.create -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.ExperimentalMaterial3Api 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.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.button.BackButton 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 +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar @Composable fun CreatePinView( state: CreatePinState, + onBackClicked: () -> Unit, modifier: Modifier = Modifier, ) { - Timber.d("CreatePinView: $state") - Box(modifier, contentAlignment = Alignment.Center) { - Text( - "CreatePin feature view", - color = MaterialTheme.colorScheme.primary, - ) - } + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { + BackButton(onClick = onBackClicked) + }, + title = {} + ) + }, + content = { padding -> + HeaderFooterPage( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding), + header = { CreatePinHeader() }, + footer = { CreatePinFooter() }, + ) + } + ) +} + +@Composable +private fun CreatePinHeader( + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier, + title = "Choose 4 digit PIN", + subTitle = "Lock Element to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app", + iconImageVector = Icons.Default.Lock, + ) +} + +@Composable +private fun CreatePinFooter() { + Button( + modifier = Modifier.fillMaxWidth(), + text = "Continue", + onClick = { + + } + ) } @Composable @@ -47,6 +93,7 @@ internal fun CreatePinViewPreview(@PreviewParameter(CreatePinStateProvider::clas ElementPreview { CreatePinView( state = state, + onBackClicked = {}, ) } } From f07a687630534a4a733ebe204385d83d8a4e55f4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Oct 2023 21:20:47 +0200 Subject: [PATCH 02/70] Create pin : start handling the text field --- .../lockscreen/impl/create/CreatePinEvents.kt | 2 +- .../lockscreen/impl/create/CreatePinNode.kt | 1 + .../impl/create/CreatePinPresenter.kt | 12 ++- .../lockscreen/impl/create/CreatePinState.kt | 3 + .../impl/create/CreatePinStateProvider.kt | 11 ++ .../lockscreen/impl/create/CreatePinView.kt | 102 ++++++++++++++++++ .../lockscreen/impl/create/model/PinDigit.kt | 29 +++++ .../lockscreen/impl/create/model/PinEntry.kt | 62 +++++++++++ .../designsystem/theme/ColorAliases.kt | 10 ++ 9 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt index deb3095e69..f492f5ab05 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt @@ -17,5 +17,5 @@ package io.element.android.features.lockscreen.impl.create sealed interface CreatePinEvents { - object MyEvent : CreatePinEvents + data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt index 3689c0cc76..331d6ada84 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt @@ -38,6 +38,7 @@ class CreatePinNode @AssistedInject constructor( val state = presenter.present() CreatePinView( state = state, + onBackClicked = { }, modifier = modifier ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 08ba24e074..4599a06fc9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -17,6 +17,10 @@ package io.element.android.features.lockscreen.impl.create import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -24,14 +28,20 @@ class CreatePinPresenter @Inject constructor() : Presenter { @Composable override fun present(): CreatePinState { + val pinEntry by remember { + mutableStateOf(PinEntry.empty(4)) + } fun handleEvents(event: CreatePinEvents) { when (event) { - CreatePinEvents.MyEvent -> Unit + is CreatePinEvents.OnPinEntryChanged -> { + pinEntry.fillWith(event.entryAsText) + } } } return CreatePinState( + pinEntry = pinEntry, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 67311639ad..9b3835193f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -16,6 +16,9 @@ package io.element.android.features.lockscreen.impl.create +import io.element.android.features.lockscreen.impl.create.model.PinEntry + data class CreatePinState( + val pinEntry: PinEntry, val eventSink: (CreatePinEvents) -> Unit ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index a918b5193e..dbce5dddc5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -17,6 +17,9 @@ package io.element.android.features.lockscreen.impl.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.lockscreen.impl.create.model.PinDigit +import io.element.android.features.lockscreen.impl.create.model.PinEntry +import kotlinx.collections.immutable.persistentListOf open class CreatePinStateProvider : PreviewParameterProvider { override val values: Sequence @@ -27,5 +30,13 @@ open class CreatePinStateProvider : PreviewParameterProvider { } fun aCreatePinState() = CreatePinState( + pinEntry = PinEntry( + digits = persistentListOf( + PinDigit.Filled('1'), + PinDigit.Filled('2'), + PinDigit.Empty, + PinDigit.Empty, + ) + ), eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index d86c2f296d..6035f9d5d6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -18,15 +18,30 @@ package io.element.android.features.lockscreen.impl.create +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.create.model.PinDigit +import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.button.BackButton @@ -34,7 +49,10 @@ 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.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.theme.pinDigitBg +import io.element.android.libraries.theme.ElementTheme @Composable fun CreatePinView( @@ -59,6 +77,7 @@ fun CreatePinView( .consumeWindowInsets(padding), header = { CreatePinHeader() }, footer = { CreatePinFooter() }, + content = { CreatePinContent(state) } ) } ) @@ -87,6 +106,89 @@ private fun CreatePinFooter() { ) } +@Composable +private fun CreatePinContent( + state: CreatePinState, + modifier: Modifier = Modifier, +) { + + PinEntryTextField( + state.pinEntry, + onValueChange = { + state.eventSink(CreatePinEvents.OnPinEntryChanged(it)) + }, + modifier = modifier + .padding(top = 36.dp) + .fillMaxWidth() + ) +} + +@Composable +fun PinEntryTextField( + pinEntry: PinEntry, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + BasicTextField( + modifier = modifier, + value = TextFieldValue(pinEntry.toText()), + onValueChange = { + onValueChange(it.text) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + decorationBox = { + PinEntryRow(pinEntry = pinEntry) + } + ) +} + +@Composable +private fun PinEntryRow( + pinEntry: PinEntry, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + for (digit in pinEntry.digits) { + PinDigitView(digit = digit) + } + } +} + +@Composable +private fun PinDigitView( + digit: PinDigit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(8.dp) + val appearanceModifier = when (digit) { + PinDigit.Empty -> { + Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) + } + is PinDigit.Filled -> { + Modifier.background(ElementTheme.colors.pinDigitBg, shape) + } + } + Box( + modifier = modifier + .size(40.dp, 50.dp) + .then(appearanceModifier), + contentAlignment = Alignment.Center, + + ) { + if (digit is PinDigit.Filled) { + Text( + text = digit.toText(), + style = ElementTheme.typography.fontHeadingMdBold + ) + } + + } +} + @Composable @PreviewsDayNight internal fun CreatePinViewPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt new file mode 100644 index 0000000000..741a61cafe --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt @@ -0,0 +1,29 @@ +/* + * 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.lockscreen.impl.create.model + +sealed interface PinDigit { + data object Empty : PinDigit + data class Filled(val value: Char) : PinDigit + + fun toText(): String { + return when (this) { + is Empty -> "" + is Filled -> value.toString() + } + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt new file mode 100644 index 0000000000..587fde955d --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt @@ -0,0 +1,62 @@ +/* + * 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.lockscreen.impl.create.model + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +data class PinEntry( + val digits: ImmutableList, +) { + + companion object { + fun empty(size: Int): PinEntry { + val digits = List(size) { PinDigit.Empty } + return PinEntry( + digits = digits.toPersistentList() + ) + } + } + + private val size = digits.size + + /** + * Fill the first digits with the given text. + * Can't be more than the size of the PinEntry + * Keep the Empty digits at the end + * @return the new PinEntry + */ + fun fillWith(text: String): PinEntry { + val newDigits = digits.toMutableList() + text.forEachIndexed { index, char -> + if (index < size) { + newDigits[index] = PinDigit.Filled(char) + } + } + return copy(digits = newDigits.toPersistentList()) + } + + fun isPinComplete(): Boolean { + return digits.all { it is PinDigit.Filled } + } + + fun toText(): String { + return digits.joinToString("") { + it.toText() + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt index b347402b41..e85abb396b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -98,6 +98,16 @@ val SemanticColors.bgSubtleTertiary val SemanticColors.temporaryColorBgSpecial get() = if (isLight) Color(0xFFE4E8F0) else Color(0xFF3A4048) +// This color is not present in Semantic color, so put hard-coded value for now +val SemanticColors.pinDigitBg + get() = if (isLight) { + // We want LightDesignTokens.colorGray300 + Color(0xFFF0F2F5) + } else { + // We want DarkDesignTokens.colorGray400 + Color(0xFF26282D) + } + @PreviewsDayNight @Composable internal fun ColorAliasesPreview() = ElementPreview { From 6b00d9693ebcfcacb091c4f7a6f1816452eef8fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:47:52 +0000 Subject: [PATCH 03/70] Update dependency androidx.sqlite:sqlite-ktx to v2.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a0bfc3a04..f231381a65 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -155,7 +155,7 @@ sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", ver sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4" -sqlite = "androidx.sqlite:sqlite-ktx:2.3.1" +sqlite = "androidx.sqlite:sqlite-ktx:2.4.0" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" From 59c2728df3e593eb6ed6ef789953bdfb6d540ef0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 01:04:02 +0000 Subject: [PATCH 04/70] Update dependency com.google.firebase:firebase-bom to v32.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a0bfc3a04..cd8e7b165c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:32.3.1" +google_firebase_bom = "com.google.firebase:firebase-bom:32.4.0" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } From 63fbb4412b894a13011d50054ea5df3a7a3b8e58 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 12:11:14 +0200 Subject: [PATCH 05/70] Pin create : add some more states to manage validation and confirmation --- .../lockscreen/impl/create/CreatePinEvents.kt | 1 + .../impl/create/CreatePinPresenter.kt | 55 +++++++++++++++++-- .../lockscreen/impl/create/CreatePinState.kt | 14 ++++- .../impl/create/CreatePinStateProvider.kt | 31 +++++++---- .../lockscreen/impl/create/CreatePinView.kt | 10 ++-- .../lockscreen/impl/create/model/PinEntry.kt | 4 ++ .../create/validation/PinCreationFailure.kt | 22 ++++++++ .../impl/create/validation/PinValidator.kt | 40 ++++++++++++++ 8 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt index f492f5ab05..9e53762c07 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt @@ -18,4 +18,5 @@ package io.element.android.features.lockscreen.impl.create sealed interface CreatePinEvents { data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents + data object OnClearValidationFailure : CreatePinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 4599a06fc9..18a17acb62 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -20,28 +20,73 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure +import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.libraries.architecture.Presenter import javax.inject.Inject -class CreatePinPresenter @Inject constructor() : Presenter { +private const val PIN_SIZE = 4 + +class CreatePinPresenter @Inject constructor( + private val pinValidator: PinValidator, + private val pinCodeManager: PinCodeManager, +) : Presenter { @Composable override fun present(): CreatePinState { - val pinEntry by remember { - mutableStateOf(PinEntry.empty(4)) + var choosePinEntry by remember { + mutableStateOf(PinEntry.empty(PIN_SIZE)) + } + var confirmPinEntry by remember { + mutableStateOf(PinEntry.empty(PIN_SIZE)) + } + var isConfirmationStep by remember { + mutableStateOf(false) + } + var creationFailure by remember { + mutableStateOf(null) } fun handleEvents(event: CreatePinEvents) { when (event) { is CreatePinEvents.OnPinEntryChanged -> { - pinEntry.fillWith(event.entryAsText) + if (isConfirmationStep) { + confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) + if (confirmPinEntry.isPinComplete()) { + if (confirmPinEntry == choosePinEntry) { + //pinCodeManager.savePin(confirmPinEntry.toText()) + } else { + confirmPinEntry = PinEntry.empty(PIN_SIZE) + creationFailure = PinCreationFailure.ConfirmationPinNotMatching + } + } + } else { + choosePinEntry = choosePinEntry.fillWith(event.entryAsText) + if (choosePinEntry.isPinComplete()) { + when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { + is PinValidator.Result.Invalid -> { + choosePinEntry = PinEntry.empty(PIN_SIZE) + creationFailure = pinValidationResult.failure + } + PinValidator.Result.Valid -> isConfirmationStep = true + } + } + } + } + CreatePinEvents.OnClearValidationFailure -> { + creationFailure = null } } } return CreatePinState( - pinEntry = pinEntry, + choosePinEntry = choosePinEntry, + confirmPinEntry = confirmPinEntry, + isConfirmationStep = isConfirmationStep, + creationFailure = creationFailure, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 9b3835193f..799d4b20a8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -17,8 +17,18 @@ package io.element.android.features.lockscreen.impl.create import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure data class CreatePinState( - val pinEntry: PinEntry, + val choosePinEntry: PinEntry, + val confirmPinEntry: PinEntry, + val isConfirmationStep: Boolean, + val creationFailure: PinCreationFailure?, val eventSink: (CreatePinEvents) -> Unit -) +) { + val activePinEntry = if (isConfirmationStep) { + confirmPinEntry + } else { + choosePinEntry + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index dbce5dddc5..f4d778a296 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -17,26 +17,33 @@ package io.element.android.features.lockscreen.impl.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry -import kotlinx.collections.immutable.persistentListOf +import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure open class CreatePinStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aCreatePinState(), - // Add other states here + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("12") + ), + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("1789"), + isConfirmationStep = true, + ), ) } -fun aCreatePinState() = CreatePinState( - pinEntry = PinEntry( - digits = persistentListOf( - PinDigit.Filled('1'), - PinDigit.Filled('2'), - PinDigit.Empty, - PinDigit.Empty, - ) - ), +fun aCreatePinState( + choosePinEntry: PinEntry = PinEntry.empty(4), + confirmPinEntry: PinEntry = PinEntry.empty(4), + isConfirmationStep: Boolean = false, + creationFailure: PinCreationFailure? = null, +) = CreatePinState( + choosePinEntry = choosePinEntry, + confirmPinEntry = confirmPinEntry, + isConfirmationStep = isConfirmationStep, + creationFailure = creationFailure, eventSink = {} ) + diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 6035f9d5d6..f5b2e49df8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -75,7 +75,7 @@ fun CreatePinView( modifier = Modifier .padding(padding) .consumeWindowInsets(padding), - header = { CreatePinHeader() }, + header = { CreatePinHeader(state.isConfirmationStep) }, footer = { CreatePinFooter() }, content = { CreatePinContent(state) } ) @@ -85,11 +85,12 @@ fun CreatePinView( @Composable private fun CreatePinHeader( + isValidationStep: Boolean, modifier: Modifier = Modifier, ) { IconTitleSubtitleMolecule( modifier = modifier, - title = "Choose 4 digit PIN", + title = if (isValidationStep) "Confirm PIN" else "Choose 4 digit PIN", subTitle = "Lock Element to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app", iconImageVector = Icons.Default.Lock, ) @@ -111,9 +112,8 @@ private fun CreatePinContent( state: CreatePinState, modifier: Modifier = Modifier, ) { - PinEntryTextField( - state.pinEntry, + state.activePinEntry, onValueChange = { state.eventSink(CreatePinEvents.OnPinEntryChanged(it)) }, @@ -135,7 +135,7 @@ fun PinEntryTextField( onValueChange = { onValueChange(it.text) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), decorationBox = { PinEntryRow(pinEntry = pinEntry) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt index 587fde955d..2228110156 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt @@ -50,6 +50,10 @@ data class PinEntry( return copy(digits = newDigits.toPersistentList()) } + fun clear(): PinEntry { + return fillWith("") + } + fun isPinComplete(): Boolean { return digits.all { it is PinDigit.Filled } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt new file mode 100644 index 0000000000..26b1eb5fd8 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.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.lockscreen.impl.create.validation + +sealed interface PinCreationFailure { + data object ChosenPinBlacklisted : PinCreationFailure + data object ConfirmationPinNotMatching : PinCreationFailure +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt new file mode 100644 index 0000000000..1d97cda60d --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt @@ -0,0 +1,40 @@ +/* + * 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.lockscreen.impl.create.validation + +import io.element.android.features.lockscreen.impl.create.model.PinEntry +import javax.inject.Inject + +private val BLACKLIST = listOf("0000", "1234") + +class PinValidator @Inject constructor() { + + sealed interface Result { + data object Valid : Result + data class Invalid(val failure: PinCreationFailure) : Result + } + + fun isPinValid(pinEntry: PinEntry): Result { + val pinAsText = pinEntry.toText() + val isBlacklisted = BLACKLIST.any { it == pinAsText } + return if (isBlacklisted) { + Result.Invalid(PinCreationFailure.ChosenPinBlacklisted) + } else { + Result.Valid + } + } +} From cbd2ba50e72b7ea9071297ff977579463a253812 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 12:16:30 +0200 Subject: [PATCH 06/70] Pin create : improve clear validation --- .../lockscreen/impl/create/CreatePinPresenter.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 18a17acb62..a39c199256 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -59,7 +59,6 @@ class CreatePinPresenter @Inject constructor( if (confirmPinEntry == choosePinEntry) { //pinCodeManager.savePin(confirmPinEntry.toText()) } else { - confirmPinEntry = PinEntry.empty(PIN_SIZE) creationFailure = PinCreationFailure.ConfirmationPinNotMatching } } @@ -68,7 +67,6 @@ class CreatePinPresenter @Inject constructor( if (choosePinEntry.isPinComplete()) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { is PinValidator.Result.Invalid -> { - choosePinEntry = PinEntry.empty(PIN_SIZE) creationFailure = pinValidationResult.failure } PinValidator.Result.Valid -> isConfirmationStep = true @@ -77,6 +75,17 @@ class CreatePinPresenter @Inject constructor( } } CreatePinEvents.OnClearValidationFailure -> { + when (creationFailure) { + is PinCreationFailure.ConfirmationPinNotMatching -> { + choosePinEntry = PinEntry.empty(PIN_SIZE) + confirmPinEntry = PinEntry.empty(PIN_SIZE) + } + is PinCreationFailure.ChosenPinBlacklisted -> { + choosePinEntry = PinEntry.empty(PIN_SIZE) + } + null -> Unit + } + isConfirmationStep = false creationFailure = null } } From e5bcfb393633edd8f24992d71ba80ea789f19a3c Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 12:18:26 +0200 Subject: [PATCH 07/70] Create pin : remove PinCodeManager and add TODO --- .../features/lockscreen/impl/create/CreatePinPresenter.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index a39c199256..435de0ebe0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure import io.element.android.features.lockscreen.impl.create.validation.PinValidator -import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -32,7 +31,6 @@ private const val PIN_SIZE = 4 class CreatePinPresenter @Inject constructor( private val pinValidator: PinValidator, - private val pinCodeManager: PinCodeManager, ) : Presenter { @Composable @@ -57,7 +55,7 @@ class CreatePinPresenter @Inject constructor( confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) if (confirmPinEntry.isPinComplete()) { if (confirmPinEntry == choosePinEntry) { - //pinCodeManager.savePin(confirmPinEntry.toText()) + //TODO save in db and navigate to next screen } else { creationFailure = PinCreationFailure.ConfirmationPinNotMatching } From 7ba9a0af77fd24af9b0b8082515237ce16433118 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 12:34:29 +0200 Subject: [PATCH 08/70] Create pin : render failures --- .../lockscreen/impl/create/CreatePinEvents.kt | 2 +- .../impl/create/CreatePinPresenter.kt | 22 +++++----- .../lockscreen/impl/create/CreatePinState.kt | 4 +- .../impl/create/CreatePinStateProvider.kt | 17 ++++++-- .../lockscreen/impl/create/CreatePinView.kt | 41 +++++++++++++------ ...CreationFailure.kt => CreatePinFailure.kt} | 6 +-- .../impl/create/validation/PinValidator.kt | 4 +- 7 files changed, 61 insertions(+), 35 deletions(-) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/{PinCreationFailure.kt => CreatePinFailure.kt} (80%) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt index 9e53762c07..78ce529325 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt @@ -18,5 +18,5 @@ package io.element.android.features.lockscreen.impl.create sealed interface CreatePinEvents { data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents - data object OnClearValidationFailure : CreatePinEvents + data object ClearFailure : CreatePinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 435de0ebe0..525b80314b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.create.validation.PinValidator import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -44,8 +44,8 @@ class CreatePinPresenter @Inject constructor( var isConfirmationStep by remember { mutableStateOf(false) } - var creationFailure by remember { - mutableStateOf(null) + var createPinFailure by remember { + mutableStateOf(null) } fun handleEvents(event: CreatePinEvents) { @@ -57,7 +57,7 @@ class CreatePinPresenter @Inject constructor( if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { - creationFailure = PinCreationFailure.ConfirmationPinNotMatching + createPinFailure = CreatePinFailure.ConfirmationPinNotMatching } } } else { @@ -65,26 +65,26 @@ class CreatePinPresenter @Inject constructor( if (choosePinEntry.isPinComplete()) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { is PinValidator.Result.Invalid -> { - creationFailure = pinValidationResult.failure + createPinFailure = pinValidationResult.failure } PinValidator.Result.Valid -> isConfirmationStep = true } } } } - CreatePinEvents.OnClearValidationFailure -> { - when (creationFailure) { - is PinCreationFailure.ConfirmationPinNotMatching -> { + CreatePinEvents.ClearFailure -> { + when (createPinFailure) { + is CreatePinFailure.ConfirmationPinNotMatching -> { choosePinEntry = PinEntry.empty(PIN_SIZE) confirmPinEntry = PinEntry.empty(PIN_SIZE) } - is PinCreationFailure.ChosenPinBlacklisted -> { + is CreatePinFailure.ChosenPinBlacklisted -> { choosePinEntry = PinEntry.empty(PIN_SIZE) } null -> Unit } isConfirmationStep = false - creationFailure = null + createPinFailure = null } } } @@ -93,7 +93,7 @@ class CreatePinPresenter @Inject constructor( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - creationFailure = creationFailure, + createPinFailure = createPinFailure, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 799d4b20a8..914e12ca96 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -17,13 +17,13 @@ package io.element.android.features.lockscreen.impl.create import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure data class CreatePinState( val choosePinEntry: PinEntry, val confirmPinEntry: PinEntry, val isConfirmationStep: Boolean, - val creationFailure: PinCreationFailure?, + val createPinFailure: CreatePinFailure?, val eventSink: (CreatePinEvents) -> Unit ) { val activePinEntry = if (isConfirmationStep) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index f4d778a296..40287622fd 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -18,7 +18,7 @@ package io.element.android.features.lockscreen.impl.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure open class CreatePinStateProvider : PreviewParameterProvider { override val values: Sequence @@ -31,6 +31,17 @@ open class CreatePinStateProvider : PreviewParameterProvider { choosePinEntry = PinEntry.empty(4).fillWith("1789"), isConfirmationStep = true, ), + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("1789"), + confirmPinEntry = PinEntry.empty(4).fillWith("1788"), + isConfirmationStep = true, + creationFailure = CreatePinFailure.ConfirmationPinNotMatching + ), + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("1111"), + creationFailure = CreatePinFailure.ChosenPinBlacklisted + ), + ) } @@ -38,12 +49,12 @@ fun aCreatePinState( choosePinEntry: PinEntry = PinEntry.empty(4), confirmPinEntry: PinEntry = PinEntry.empty(4), isConfirmationStep: Boolean = false, - creationFailure: PinCreationFailure? = null, + creationFailure: CreatePinFailure? = null, ) = CreatePinState( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - creationFailure = creationFailure, + createPinFailure = creationFailure, eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index f5b2e49df8..fdce08c229 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -42,12 +42,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog 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.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -76,7 +77,6 @@ fun CreatePinView( .padding(padding) .consumeWindowInsets(padding), header = { CreatePinHeader(state.isConfirmationStep) }, - footer = { CreatePinFooter() }, content = { CreatePinContent(state) } ) } @@ -96,17 +96,6 @@ private fun CreatePinHeader( ) } -@Composable -private fun CreatePinFooter() { - Button( - modifier = Modifier.fillMaxWidth(), - text = "Continue", - onClick = { - - } - ) -} - @Composable private fun CreatePinContent( state: CreatePinState, @@ -121,6 +110,32 @@ private fun CreatePinContent( .padding(top = 36.dp) .fillMaxWidth() ) + if (state.createPinFailure != null) { + ErrorDialog( + modifier = modifier, + title = state.createPinFailure.title(), + content = state.createPinFailure.content(), + onDismiss = { + state.eventSink(CreatePinEvents.ClearFailure) + } + ) + } +} + +@Composable +private fun CreatePinFailure.content(): String { + return when (this) { + CreatePinFailure.ChosenPinBlacklisted -> "You cannot choose this as your PIN code for security reasons" + CreatePinFailure.ConfirmationPinNotMatching -> "Please enter the same PIN twice" + } +} + +@Composable +private fun CreatePinFailure.title(): String { + return when (this) { + CreatePinFailure.ChosenPinBlacklisted -> "Choose a different PIN" + CreatePinFailure.ConfirmationPinNotMatching -> "PINs don't match" + } } @Composable diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt index 26b1eb5fd8..96c0de0056 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt @@ -16,7 +16,7 @@ package io.element.android.features.lockscreen.impl.create.validation -sealed interface PinCreationFailure { - data object ChosenPinBlacklisted : PinCreationFailure - data object ConfirmationPinNotMatching : PinCreationFailure +sealed interface CreatePinFailure { + data object ChosenPinBlacklisted : CreatePinFailure + data object ConfirmationPinNotMatching : CreatePinFailure } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt index 1d97cda60d..8c1854ecee 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt @@ -25,14 +25,14 @@ class PinValidator @Inject constructor() { sealed interface Result { data object Valid : Result - data class Invalid(val failure: PinCreationFailure) : Result + data class Invalid(val failure: CreatePinFailure) : Result } fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() val isBlacklisted = BLACKLIST.any { it == pinAsText } return if (isBlacklisted) { - Result.Invalid(PinCreationFailure.ChosenPinBlacklisted) + Result.Invalid(CreatePinFailure.ChosenPinBlacklisted) } else { Result.Valid } From 537fc68778f1922e21cb09af3d85ccf841479447 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 19 Oct 2023 10:47:03 +0000 Subject: [PATCH 09/70] Update screenshots --- ...create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png | 3 +++ 10 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png index e8dd1a28d7..03b059496a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9914a33ba23544bdfce1e21b52ad247024392730fb22b60bc9b6fa6440f004d4 -size 9216 +oid sha256:df5f2cc45255cec07dc99de8df0f2e8dd06fdc3afded3ba43c8deec2bb7c1d0b +size 34529 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ffcb20390e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c5f338938adeeb280be0b68f57b5ba0b4c9a904e8660e99fb6e57cd6b4774fc +size 34534 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5e82145176 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a412e9c38549341f707e9a6379e165d554294c0f452fec82ab783fa9e0533ad7 +size 32374 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2fdd3802d4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fbba51aa681041018dd41f9f2e69159e229411b2cf438a62367a7f1edbb857a +size 28781 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..26305ae91e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:843141e92c23f9cbba98296fc543c365d33a708daaabe0ff9549ccb8d6f69af4 +size 36969 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png index 157a7c52c3..c3a42144c6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ad524a918e499fcea6fd5293358167ff52f8877cc31b778c8def01925fa662f -size 8582 +oid sha256:be489eaf748f7d79ee7d8dd3f0177ab47626728f5b4dae9851b98e708f31e97c +size 32962 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a37f885bb6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb6369c1574670c81f2d0f5a541cc86da708cf087539250b50a1bb86fee33894 +size 33146 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f5c161a105 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72836a8215d294ccb10dd961c5bde18342f50ab4aedf23c6bd5e655edd733a4a +size 31348 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e78f91910a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86591b97276d7379ae68c500a81a977ee0adb2f8b28e0896d0b32a658fb359bf +size 25430 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..968d9fdff5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:110139dd6177e5e1bd126bcd60e4442394867c2e0a3656bee7f15cc5c58c6a6e +size 32584 From bde270565418ba53e41e66d286f132420f3c081a Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 15:45:11 +0200 Subject: [PATCH 10/70] Pin create: add test for presenter --- build.gradle.kts | 1 - features/lockscreen/impl/build.gradle.kts | 1 + .../impl/create/CreatePinPresenter.kt | 6 +- .../impl/create/CreatePinStateProvider.kt | 4 +- .../lockscreen/impl/create/CreatePinView.kt | 8 +- .../create/validation/CreatePinFailure.kt | 4 +- .../impl/create/validation/PinValidator.kt | 10 +- .../impl/create/CreatePinPresenterTest.kt | 113 ++++++++++++++++++ .../android/tests/testutils/ReceiveTurbine.kt | 10 ++ 9 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 487776d948..e14ad71981 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -253,7 +253,6 @@ koverMerged { // Temporary until we have actually something to test. excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter" excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter$*" - excludes += "io.element.android.features.lockscreen.impl.create.CreatePinPresenter" } bound { minValue = 85 diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index af63538db5..028d8bee3c 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 525b80314b..e72e636ed4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -57,7 +57,7 @@ class CreatePinPresenter @Inject constructor( if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { - createPinFailure = CreatePinFailure.ConfirmationPinNotMatching + createPinFailure = CreatePinFailure.PinsDontMatch } } } else { @@ -74,11 +74,11 @@ class CreatePinPresenter @Inject constructor( } CreatePinEvents.ClearFailure -> { when (createPinFailure) { - is CreatePinFailure.ConfirmationPinNotMatching -> { + is CreatePinFailure.PinsDontMatch -> { choosePinEntry = PinEntry.empty(PIN_SIZE) confirmPinEntry = PinEntry.empty(PIN_SIZE) } - is CreatePinFailure.ChosenPinBlacklisted -> { + is CreatePinFailure.PinBlacklisted -> { choosePinEntry = PinEntry.empty(PIN_SIZE) } null -> Unit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index 40287622fd..543360f91e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -35,11 +35,11 @@ open class CreatePinStateProvider : PreviewParameterProvider { choosePinEntry = PinEntry.empty(4).fillWith("1789"), confirmPinEntry = PinEntry.empty(4).fillWith("1788"), isConfirmationStep = true, - creationFailure = CreatePinFailure.ConfirmationPinNotMatching + creationFailure = CreatePinFailure.PinsDontMatch ), aCreatePinState( choosePinEntry = PinEntry.empty(4).fillWith("1111"), - creationFailure = CreatePinFailure.ChosenPinBlacklisted + creationFailure = CreatePinFailure.PinBlacklisted ), ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index fdce08c229..915bd2b4b0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -125,16 +125,16 @@ private fun CreatePinContent( @Composable private fun CreatePinFailure.content(): String { return when (this) { - CreatePinFailure.ChosenPinBlacklisted -> "You cannot choose this as your PIN code for security reasons" - CreatePinFailure.ConfirmationPinNotMatching -> "Please enter the same PIN twice" + CreatePinFailure.PinBlacklisted -> "You cannot choose this as your PIN code for security reasons" + CreatePinFailure.PinsDontMatch -> "Please enter the same PIN twice" } } @Composable private fun CreatePinFailure.title(): String { return when (this) { - CreatePinFailure.ChosenPinBlacklisted -> "Choose a different PIN" - CreatePinFailure.ConfirmationPinNotMatching -> "PINs don't match" + CreatePinFailure.PinBlacklisted -> "Choose a different PIN" + CreatePinFailure.PinsDontMatch -> "PINs don't match" } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt index 96c0de0056..8c0cb78921 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt @@ -17,6 +17,6 @@ package io.element.android.features.lockscreen.impl.create.validation sealed interface CreatePinFailure { - data object ChosenPinBlacklisted : CreatePinFailure - data object ConfirmationPinNotMatching : CreatePinFailure + data object PinBlacklisted : CreatePinFailure + data object PinsDontMatch : CreatePinFailure } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt index 8c1854ecee..7353ec47d0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt @@ -16,13 +16,17 @@ package io.element.android.features.lockscreen.impl.create.validation +import androidx.annotation.VisibleForTesting import io.element.android.features.lockscreen.impl.create.model.PinEntry import javax.inject.Inject -private val BLACKLIST = listOf("0000", "1234") - class PinValidator @Inject constructor() { + companion object { + @VisibleForTesting + val BLACKLIST = listOf("0000", "1234") + } + sealed interface Result { data object Valid : Result data class Invalid(val failure: CreatePinFailure) : Result @@ -32,7 +36,7 @@ class PinValidator @Inject constructor() { val pinAsText = pinEntry.toText() val isBlacklisted = BLACKLIST.any { it == pinAsText } return if (isBlacklisted) { - Result.Invalid(CreatePinFailure.ChosenPinBlacklisted) + Result.Invalid(CreatePinFailure.PinBlacklisted) } else { Result.Valid } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt new file mode 100644 index 0000000000..9c86039fe1 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt @@ -0,0 +1,113 @@ +/* + * 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.lockscreen.impl.create + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.create.model.PinDigit +import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CreatePinPresenterTest { + + private val blacklistedPin = PinValidator.BLACKLIST.first() + private val halfCompletePin = "12" + private val completePin = "1235" + private val mismatchedPin = "1236" + + @Test + fun `present - complete flow`() = runTest { + + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().also { state -> + state.choosePinEntry.assertEmpty() + state.confirmPinEntry.assertEmpty() + assertThat(state.createPinFailure).isNull() + assertThat(state.isConfirmationStep).isFalse() + state.eventSink(CreatePinEvents.OnPinEntryChanged(halfCompletePin)) + } + awaitItem().also { state -> + state.choosePinEntry.assertText(halfCompletePin) + state.confirmPinEntry.assertEmpty() + assertThat(state.createPinFailure).isNull() + assertThat(state.isConfirmationStep).isFalse() + state.eventSink(CreatePinEvents.OnPinEntryChanged(blacklistedPin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(blacklistedPin) + assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinBlacklisted) + state.eventSink(CreatePinEvents.ClearFailure) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertEmpty() + assertThat(state.createPinFailure).isNull() + state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertEmpty() + assertThat(state.isConfirmationStep).isTrue() + state.eventSink(CreatePinEvents.OnPinEntryChanged(mismatchedPin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertText(mismatchedPin) + assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinsDontMatch) + state.eventSink(CreatePinEvents.ClearFailure) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertEmpty() + state.confirmPinEntry.assertEmpty() + assertThat(state.isConfirmationStep).isFalse() + assertThat(state.createPinFailure).isNull() + state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertEmpty() + assertThat(state.isConfirmationStep).isTrue() + state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + } + awaitItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertText(completePin) + } + } + } + + private fun PinEntry.assertText(text: String) { + assertThat(toText()).isEqualTo(text) + } + + private fun PinEntry.assertEmpty() { + val isEmpty = digits.all { it is PinDigit.Empty } + assertThat(isEmpty).isTrue() + } + + private fun createPresenter(): CreatePinPresenter { + return CreatePinPresenter(PinValidator()) + } +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt index 06b6b3d3ea..3e47dd63ce 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt @@ -32,6 +32,16 @@ suspend fun ReceiveTurbine.consumeItemsUntilTimeout(timeout: Durati return consumeItemsUntilPredicate(timeout) { false } } +/** + * Consume all items which are emitted sequentially. + * Use the smallest timeout possible internally to avoid wasting time. + * Same as calling skipItems(x) and then awaitItem() but without assumption on the number of items. + * @return the last item emitted. + */ +suspend fun ReceiveTurbine.awaitLastSequentialItem(): T { + return consumeItemsUntilTimeout(1.milliseconds).last() +} + /** * Consume items until predicate is true, or timeout is reached waiting for an event, or we receive terminal event. * The timeout is applied for each event. From c08cd13e0ecc107ccfb607df7ccd48f64164bb38 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 16:28:20 +0200 Subject: [PATCH 11/70] Pin create: use localazy strings --- .../lockscreen/impl/create/CreatePinState.kt | 1 + .../lockscreen/impl/create/CreatePinView.kt | 42 +++++++++++---- .../lockscreen/impl/create/model/PinEntry.kt | 2 +- .../impl/src/main/res/values/localazy.xml | 24 +++++++++ .../api/src/main/res/values/localazy.xml | 4 ++ .../src/main/res/values/localazy.xml | 54 ++++++++++++++++++- tools/localazy/config.json | 6 +++ 7 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 features/lockscreen/impl/src/main/res/values/localazy.xml diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 914e12ca96..5bb632f04e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -26,6 +26,7 @@ data class CreatePinState( val createPinFailure: CreatePinFailure?, val eventSink: (CreatePinEvents) -> Unit ) { + val pinSize = choosePinEntry.size val activePinEntry = if (isConfirmationStep) { confirmPinEntry } else { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 915bd2b4b0..dc9956abaa 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth @@ -33,13 +34,17 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure @@ -76,7 +81,7 @@ fun CreatePinView( modifier = Modifier .padding(padding) .consumeWindowInsets(padding), - header = { CreatePinHeader(state.isConfirmationStep) }, + header = { CreatePinHeader(state.isConfirmationStep, state.pinSize) }, content = { CreatePinContent(state) } ) } @@ -86,14 +91,31 @@ fun CreatePinView( @Composable private fun CreatePinHeader( isValidationStep: Boolean, + pinSize: Int, modifier: Modifier = Modifier, ) { - IconTitleSubtitleMolecule( + Column( modifier = modifier, - title = if (isValidationStep) "Confirm PIN" else "Choose 4 digit PIN", - subTitle = "Lock Element to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app", - iconImageVector = Icons.Default.Lock, - ) + horizontalAlignment = Alignment.CenterHorizontally, + ) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), + title = if (isValidationStep) { + stringResource(id = R.string.screen_app_lock_setup_confirm_pin) + } else { + stringResource(id = R.string.screen_app_lock_setup_choose_pin, pinSize) + }, + subTitle = stringResource(id = R.string.screen_app_lock_setup_pin_context), + iconImageVector = Icons.Filled.Lock, + ) + Text( + text = stringResource(id = R.string.screen_app_lock_setup_pin_context_warning), + modifier = Modifier.padding(8.dp), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.secondary, + ) + } } @Composable @@ -125,16 +147,16 @@ private fun CreatePinContent( @Composable private fun CreatePinFailure.content(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> "You cannot choose this as your PIN code for security reasons" - CreatePinFailure.PinsDontMatch -> "Please enter the same PIN twice" + CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_content) + CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_content) } } @Composable private fun CreatePinFailure.title(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> "Choose a different PIN" - CreatePinFailure.PinsDontMatch -> "PINs don't match" + CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_title) + CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_title) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt index 2228110156..a97315f2e8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt @@ -32,7 +32,7 @@ data class PinEntry( } } - private val size = digits.size + val size = digits.size /** * Fill the first digits with the given text. diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..fb5c2be73c --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -0,0 +1,24 @@ + + + + "Wrong PIN. You have %1$d more chance" + "Wrong PIN. You have %1$d more chances" + + "Forgot PIN?" + "Change PIN code" + "Allow biometric unlock" + "Remove PIN" + "Are you sure you want to remove PIN?" + "Remove PIN?" + "Choose %1$d digit PIN" + "Confirm PIN" + "You cannot choose this as your PIN code for security reasons" + "Choose a different PIN" + "Lock Element to add extra security to your chats." + "Choose something memorable. If you forget this PIN, you will be logged out of the app." + "Please enter the same PIN twice" + "PINs don\'t match" + "You’ll need to re-login and create a new PIN to proceed" + "You are being signed out" + "You have 3 attempts to unlock" + diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/api/src/main/res/values/localazy.xml index 9ea4bb77fd..5a5c9c64ca 100644 --- a/features/logout/api/src/main/res/values/localazy.xml +++ b/features/logout/api/src/main/res/values/localazy.xml @@ -1,8 +1,12 @@ + "Please wait for this to complete before signing out." + "Your keys are still being backed up" "Are you sure you want to sign out?" "Sign out" "Signing out…" + "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." + "Have you saved your recovery key?" "Sign out" "Sign out" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index b9a6460bec..c7bd2f2ca9 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -5,6 +5,7 @@ "Mentions only" "Muted" "Pause" + "PIN field" "Play" "Poll" "Ended poll" @@ -69,6 +70,8 @@ "Share" "Share link" "Sign in again" + "Sign out" + "Sign out anyway" "Skip" "Start" "Start chat" @@ -84,6 +87,7 @@ "Analytics" "Audio" "Bubbles" + "Chat backup" "Copyright" "Creating room…" "Left room" @@ -93,6 +97,7 @@ "Editing" "* %1$s %2$s" "Encryption enabled" + "Enter your PIN" "Error" "Everyone" "File" @@ -122,6 +127,7 @@ "Privacy policy" "Reaction" "Reactions" + "Recovery key" "Refreshing…" "Replying to %1$s" "Report a bug" @@ -129,9 +135,9 @@ "Rich text editor" "Room name" "e.g. your project name" + "Screen lock" "Search for someone" "Search results" - "Secure backup" "Security" "Sending…" "Server not supported" @@ -151,6 +157,7 @@ "Unable to decrypt" "Invites couldn\'t be sent to one or more users." "Unable to send invite(s)" + "Unlock" "Unmute" "Unsupported event" "Username" @@ -177,6 +184,7 @@ "%1$s could not access your location. Please try again later." "%1$s does not have permission to access your location. You can enable access in Settings." "%1$s does not have permission to access your location. Enable access below." + "%1$s does not have permission to access your microphone. Enable access to record a voice message." "Some messages have not been sent" "Sorry, an error occurred" "🔐️ Join me on %1$s" @@ -185,6 +193,10 @@ "Are you sure that you want to leave this room? This room is not public and you won\'t be able to rejoin without an invite." "Are you sure that you want to leave the room?" "%1$s Android" + + "%1$d digit entered" + "%1$d digits entered" + "%1$d member" "%1$d members" @@ -199,7 +211,26 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" + "Custom Element Call base URL" + "Set a custom base URL for Element Call." + "Invalid URL, please make sure you include the protocol (http/https) and the correct address." "Share analytics data" + "Turn off backup" + "Turn on backup" + "Backup ensures that you don\'t lose your message history. %1$s." + "Backup" + "Change recovery key" + "Confirm recovery key" + "Your chat backup is currently out of sync." + "Set up recovery" + "Get access to your encrypted messages if you lose all your devices or are signed out of %1$s everywhere." + "Turn off" + "You will lose your encrypted messages if you are signed out of all devices." + "Are you sure you want to turn off backup?" + "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:" + "Not have encrypted message history on new devices" + "Lose access to your encrypted messages if you are signed out of %1$s everywhere" + "Are you sure you want to turn off backup?" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." @@ -228,6 +259,27 @@ If you proceed, some of your settings may change." "system settings" "System notifications turned off" "Notifications" + "Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work." + "Generate a new recovery key" + "Make sure you can store your recovery key somewhere safe" + "Recovery key changed" + "Change recovery key?" + "Enter your recovery key to confirm access to your chat backup." + "Enter the 48 character code." + "Enter…" + "Recovery key confirmed" + "Confirm your recovery key" + "Save recovery key" + "Write down your recovery key somewhere safe or save it in a password manager." + "Tap to copy recovery key" + "Save your recovery key" + "You will not be able to access your new recovery key after this step." + "Have you saved your recovery key?" + "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’." + "Generate your recovery key" + "Make sure you can store your recovery key somewhere safe" + "Recovery setup successful" + "Set up recovery" "Check if you want to hide all current and future messages from this user" "Share location" "Share my location" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 7e07d269c0..fd35feb36e 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -153,6 +153,12 @@ "includeRegex": [ "call_.*" ] + }, + { + "name": ":features:lockscreen:impl", + "includeRegex": [ + "screen_app_lock_.*" + ] } ] } From 6c8e0bd86e24cfed03605aeebc9dd74b1f3edba8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 16:29:16 +0200 Subject: [PATCH 12/70] Create pin : change digit size box --- .../android/features/lockscreen/impl/create/CreatePinView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index dc9956abaa..fa79016472 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -211,7 +211,7 @@ private fun PinDigitView( } Box( modifier = modifier - .size(40.dp, 50.dp) + .size(48.dp) .then(appearanceModifier), contentAlignment = Alignment.Center, From 8ae07ba74b89dec02ca9a1ba0505ce00699cd837 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 16:32:58 +0200 Subject: [PATCH 13/70] Create pin : fix konsist --- .../android/features/lockscreen/impl/create/CreatePinView.kt | 2 +- .../features/lockscreen/impl/create/CreatePinPresenterTest.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index fa79016472..4e01de8c2f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -161,7 +161,7 @@ private fun CreatePinFailure.title(): String { } @Composable -fun PinEntryTextField( +private fun PinEntryTextField( pinEntry: PinEntry, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt index 9c86039fe1..c1af14f519 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt @@ -38,7 +38,7 @@ class CreatePinPresenterTest { @Test fun `present - complete flow`() = runTest { - val presenter = createPresenter() + val presenter = createCreatePinPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -107,7 +107,7 @@ class CreatePinPresenterTest { assertThat(isEmpty).isTrue() } - private fun createPresenter(): CreatePinPresenter { + private fun createCreatePinPresenter(): CreatePinPresenter { return CreatePinPresenter(PinValidator()) } } From 1c1db6a444ebd9f1f0afc4c65f857d1679c99e20 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 19 Oct 2023 15:24:30 +0000 Subject: [PATCH 14/70] Update screenshots --- ...create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png index 03b059496a..f110f617f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df5f2cc45255cec07dc99de8df0f2e8dd06fdc3afded3ba43c8deec2bb7c1d0b -size 34529 +oid sha256:4e2d58ea747cf87fd6e4f70b3ebb1f1e638a4ec3f7ac8cb712566a889167c3e1 +size 35045 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png index ffcb20390e..e5a3fe28af 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c5f338938adeeb280be0b68f57b5ba0b4c9a904e8660e99fb6e57cd6b4774fc -size 34534 +oid sha256:4be560206e4fc9101d8a793ad5f5f9cc2057fb47e60f37f91ce6dae8e2b995e2 +size 35147 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png index 5e82145176..87cefa723f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a412e9c38549341f707e9a6379e165d554294c0f452fec82ab783fa9e0533ad7 -size 32374 +oid sha256:da83f8b634c8217636b08792498704d9ffc1623c8d4aedfb15265d2f5e085e86 +size 32805 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png index 2fdd3802d4..d3d3ba4038 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fbba51aa681041018dd41f9f2e69159e229411b2cf438a62367a7f1edbb857a -size 28781 +oid sha256:598214e07c2545ac4dd061f6237a737e91e9c47eeea01a63ab064b3398e39b68 +size 24046 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png index 26305ae91e..7476004b1f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:843141e92c23f9cbba98296fc543c365d33a708daaabe0ff9549ccb8d6f69af4 -size 36969 +oid sha256:0ec4a6f05bfb15e018c19205cb919f6619440e25392ed0a74fb2896ddb1decb6 +size 28266 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png index c3a42144c6..41a349426e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be489eaf748f7d79ee7d8dd3f0177ab47626728f5b4dae9851b98e708f31e97c -size 32962 +oid sha256:b7c84953e8c1c3b2384078be4ca8cd5c763c032bfa22bb2ba034dcbee8ef9c72 +size 33594 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png index a37f885bb6..8f6c3fdbba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb6369c1574670c81f2d0f5a541cc86da708cf087539250b50a1bb86fee33894 -size 33146 +oid sha256:0917206ffe964311097f05de3fb838e0cc23f6fdbb2cd106d3015a751b378cfc +size 33864 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png index f5c161a105..64bc9a11cd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72836a8215d294ccb10dd961c5bde18342f50ab4aedf23c6bd5e655edd733a4a -size 31348 +oid sha256:32b2c6514d9a23da09c932e198f29ec112b5afa872a0fa42cd5de85689660891 +size 31658 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png index e78f91910a..6d10aeb87d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86591b97276d7379ae68c500a81a977ee0adb2f8b28e0896d0b32a658fb359bf -size 25430 +oid sha256:4bdedf444797479efa73d6ca7d3d6545f4bd01faa80d50980b0ad6ea427b09ee +size 20828 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png index 968d9fdff5..7c2736059e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:110139dd6177e5e1bd126bcd60e4442394867c2e0a3656bee7f15cc5c58c6a6e -size 32584 +oid sha256:51774321b68ee4d1f5a605edf1ce630e0f7651191e93cab6a885fc90f1639061 +size 24493 From 5e547269e703586df93e9b126fccdffefc994a87 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 19 Oct 2023 17:38:43 +0200 Subject: [PATCH 15/70] Integrate Element Call with widget API (#1581) * Integrate Element Call with widget API. - Add `appconfig` module and extract constants that can be overridden in forks there. - Add an Element Call feature flag, disabled by default. - Refactor the whole `ElementCallActivity`, move most logic out of it. - Integrate with the Rust Widget Driver API (note the Rust SDK version used in this PR lacks some needed changes to make the calls actually work). - Handle calls differently based on `CallType`. - Add UI to create/join a call. --------- Co-authored-by: ElementBot --- appconfig/build.gradle.kts | 24 +++ .../android/appconfig/AuthenticationConfig.kt | 24 +++ .../android/appconfig/ElementCallConfig.kt | 21 ++ .../android/appconfig}/MatrixConfiguration.kt | 2 +- features/call/build.gradle.kts | 26 ++- features/call/src/main/AndroidManifest.xml | 2 +- .../features/call/CallForegroundService.kt | 1 + .../element/android/features/call/CallType.kt | 34 +++ .../features/call/data/WidgetMessage.kt | 43 ++++ .../android/features/call/di/CallBindings.kt | 2 +- .../features/call/ui/CallScreeEvents.kt | 24 +++ .../features/call/ui/CallScreenPresenter.kt | 172 ++++++++++++++++ .../features/call/ui/CallScreenState.kt | 26 +++ .../features/call/{ => ui}/CallScreenView.kt | 141 +++++++------ .../call/{ => ui}/ElementCallActivity.kt | 96 ++++++--- .../call/{ => utils}/CallIntentDataParser.kt | 2 +- .../features/call/utils/CallWidgetProvider.kt | 31 +++ .../call/utils/DefaultCallWidgetProvider.kt | 50 +++++ .../utils/WebViewWidgetMessageInterceptor.kt | 100 +++++++++ .../call/utils/WidgetMessageInterceptor.kt | 24 +++ .../call/utils/WidgetMessageSerializer.kt | 33 +++ .../features/call/MapWebkitPermissionsTest.kt | 1 + .../call/ui/CallScreenPresenterTest.kt | 194 ++++++++++++++++++ .../call/ui/FakeCallScreenNavigator.kt | 26 +++ .../{ => utils}/CallIntentDataParserTest.kt | 2 +- .../utils/DefaultCallWidgetProviderTest.kt | 121 +++++++++++ .../call/utils/FakeCallWidgetProvider.kt | 42 ++++ .../utils/FakeWidgetMessageInterceptor.kt | 33 +++ features/login/impl/build.gradle.kts | 1 + .../AccountProviderProvider.kt | 4 +- .../ChangeAccountProviderPresenter.kt | 4 +- .../SearchAccountProviderStateProvider.kt | 4 +- .../SearchAccountProviderView.kt | 7 +- .../login/impl/util/LoginConstants.kt | 14 +- .../android/features/login/impl/util/Util.kt | 3 +- .../api/src/main/res/values/localazy.xml | 4 + features/messages/impl/build.gradle.kts | 1 + .../messages/impl/MessagesFlowNode.kt | 15 ++ .../features/messages/impl/MessagesNode.kt | 7 + .../messages/impl/MessagesPresenter.kt | 3 + .../features/messages/impl/MessagesState.kt | 1 + .../messages/impl/MessagesStateProvider.kt | 1 + .../features/messages/impl/MessagesView.kt | 16 ++ features/preferences/impl/build.gradle.kts | 1 + .../impl/advanced/AdvancedSettingsEvents.kt | 1 + .../advanced/AdvancedSettingsPresenter.kt | 38 ++++ .../impl/advanced/AdvancedSettingsState.kt | 9 +- .../advanced/AdvancedSettingsStateProvider.kt | 3 + .../impl/advanced/AdvancedSettingsView.kt | 27 ++- .../impl/src/main/res/values/localazy.xml | 3 + .../advanced/AdvancedSettingsPresenterTest.kt | 71 ++++++- .../libraries/architecture/Bindings.kt | 8 +- .../components/dialogs/ListDialog.kt | 6 +- .../components/list/TextFieldListItem.kt | 65 +++++- .../preferences/PreferenceTextField.kt | 141 +++++++++++++ .../theme/components/AlertDialogContent.kt | 2 + .../libraries/featureflag/api/FeatureFlags.kt | 6 + .../impl/StaticFeatureFlagProvider.kt | 1 + libraries/matrix/api/build.gradle.kts | 1 + .../matrix/api/permalink/MatrixToConverter.kt | 2 +- .../matrix/api/permalink/PermalinkBuilder.kt | 2 +- .../libraries/matrix/api/room/MatrixRoom.kt | 24 +++ .../api/widget/CallWidgetSettingsProvider.kt | 26 +++ .../matrix/api/widget/MatrixWidgetDriver.kt | 27 +++ .../matrix/api/widget/MatrixWidgetSettings.kt | 29 +++ .../matrix/impl/room/RustMatrixRoom.kt | 27 +++ .../DefaultCallWidgetSettingsProvider.kt | 46 +++++ .../impl/widget/MatrixWidgetSettings.kt | 50 +++++ .../matrix/impl/widget/RustWidgetDriver.kt | 78 +++++++ .../libraries/matrix/test/FakeMatrixClient.kt | 8 +- .../matrix/test/FakeMatrixClientProvider.kt | 27 +++ .../matrix/test/room/FakeMatrixRoom.kt | 22 ++ .../widget/FakeCallWidgetSettingsProvider.kt | 32 +++ .../matrix/test/widget/FakeWidgetDriver.kt | 52 +++++ .../preferences/api/store/PreferencesStore.kt | 3 + .../impl/store/DefaultPreferencesStore.kt | 18 ++ .../test/InMemoryPreferencesStore.kt | 10 + .../src/main/res/values-cs/translations.xml | 5 +- .../src/main/res/values-ru/translations.xml | 1 - .../src/main/res/values-sk/translations.xml | 1 - .../src/main/res/values/localazy.xml | 42 +++- settings.gradle.kts | 1 + ...lScreenView-D-0_0_null,NEXUS_5,1.0,en].png | 3 + ...lScreenView-N-0_1_null,NEXUS_5,1.0,en].png | 3 + ...lScreenView-D-0_0_null,NEXUS_5,1.0,en].png | 3 - ...lScreenView-N-0_1_null,NEXUS_5,1.0,en].png | 3 - ...sagesView-D-0_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_0,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_4,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_5,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_6,NEXUS_5,1.0,en].png | 4 +- ...tingsView-D-1_1_null_3,NEXUS_5,1.0,en].png | 3 + ...tingsView-N-1_2_null_3,NEXUS_5,1.0,en].png | 3 + ...-textfieldvalue_0_null,NEXUS_5,1.0,en].png | 3 + tools/localazy/config.json | 1 + 102 files changed, 2202 insertions(+), 166 deletions(-) create mode 100644 appconfig/build.gradle.kts create mode 100644 appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt create mode 100644 appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt rename {libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/config => appconfig/src/main/kotlin/io/element/android/appconfig}/MatrixConfiguration.kt (93%) create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/CallType.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreeEvents.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt rename features/call/src/main/kotlin/io/element/android/features/call/{ => ui}/CallScreenView.kt (52%) rename features/call/src/main/kotlin/io/element/android/features/call/{ => ui}/ElementCallActivity.kt (72%) rename features/call/src/main/kotlin/io/element/android/features/call/{ => utils}/CallIntentDataParser.kt (98%) create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt rename features/call/src/test/kotlin/io/element/android/features/call/{ => utils}/CallIntentDataParserTest.kt (99%) create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetSettings.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeWidgetDriver.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_TextfieldListitem-textfieldvalue_0_null,NEXUS_5,1.0,en].png diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts new file mode 100644 index 0000000000..3c03739553 --- /dev/null +++ b/appconfig/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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("java-library") + alias(libs.plugins.kotlin.jvm) +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt new file mode 100644 index 0000000000..186b84f8f0 --- /dev/null +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt @@ -0,0 +1,24 @@ +/* + * 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.appconfig + +object AuthenticationConfig { + const val MATRIX_ORG_URL = "https://matrix.org" + + const val DEFAULT_HOMESERVER_URL = MATRIX_ORG_URL + const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" +} diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt new file mode 100644 index 0000000000..bbd9f62689 --- /dev/null +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.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.appconfig + +object ElementCallConfig { + const val DEFAULT_BASE_URL = "https://call.element.io" +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/config/MatrixConfiguration.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt similarity index 93% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/config/MatrixConfiguration.kt rename to appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt index ddce776627..e4d6ee7ca2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/config/MatrixConfiguration.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.matrix.api.config +package io.element.android.appconfig object MatrixConfiguration { const val matrixToPermalinkBaseUrl: String = "https://matrix.to/#/" diff --git a/features/call/build.gradle.kts b/features/call/build.gradle.kts index 69046e33b4..c59f1ea855 100644 --- a/features/call/build.gradle.kts +++ b/features/call/build.gradle.kts @@ -18,20 +18,44 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) alias(libs.plugins.ksp) + id("kotlin-parcelize") + alias(libs.plugins.kotlin.serialization) } android { namespace = "io.element.android.features.call" + + buildFeatures { + buildConfig = true + } +} + +anvil { + generateDaggerFactories.set(true) } dependencies { + implementation(projects.appnav) + implementation(projects.appconfig) + implementation(projects.anvilannotations) implementation(projects.libraries.architecture) + implementation(projects.libraries.core) implementation(projects.libraries.designsystem) + implementation(projects.libraries.matrix.impl) implementation(projects.libraries.network) + implementation(projects.libraries.preferences.api) + implementation(projects.services.toolbox.api) implementation(libs.androidx.webkit) + implementation(libs.serialization.json) ksp(libs.showkase.processor) - testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) testImplementation(libs.test.robolectric) + testImplementation(projects.libraries.featureflag.test) + testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/features/call/src/main/AndroidManifest.xml b/features/call/src/main/AndroidManifest.xml index 877b7fb0a8..c7db9cc38f 100644 --- a/features/call/src/main/AndroidManifest.xml +++ b/features/call/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ { + + @AssistedFactory + interface Factory { + fun create(callType: CallType, navigator: CallScreenNavigator): CallScreenPresenter + } + + private val isInWidgetMode = callType is CallType.RoomCall + private val userAgent = userAgentProvider.provide() + + @Composable + override fun present(): CallScreenState { + val coroutineScope = rememberCoroutineScope() + val urlState = remember { mutableStateOf>(Async.Uninitialized) } + val callWidgetDriver = remember { mutableStateOf(null) } + val messageInterceptor = remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + loadUrl(callType, urlState, callWidgetDriver) + } + + callWidgetDriver.value?.let { driver -> + LaunchedEffect(Unit) { + driver.incomingMessages + .onEach { + // Relay message to the WebView + messageInterceptor.value?.sendMessage(it) + } + .launchIn(this) + + driver.run() + } + } + + messageInterceptor.value?.let { interceptor -> + LaunchedEffect(Unit) { + interceptor.interceptedMessages + .onEach { + // Relay message to Widget Driver + callWidgetDriver.value?.send(it) + + val parsedMessage = parseMessage(it) + if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget && parsedMessage.action == WidgetMessage.Action.HangUp) { + close(callWidgetDriver.value, navigator) + } + } + .launchIn(this) + } + } + + fun handleEvents(event: CallScreeEvents) { + when (event) { + is CallScreeEvents.Hangup -> { + val widgetId = callWidgetDriver.value?.id + val interceptor = messageInterceptor.value + if (widgetId != null && interceptor != null) { + sendHangupMessage(widgetId, interceptor) + } + coroutineScope.launch { + close(callWidgetDriver.value, navigator) + } + } + is CallScreeEvents.SetupMessageChannels -> { + messageInterceptor.value = event.widgetMessageInterceptor + } + } + } + + return CallScreenState( + urlState = urlState.value, + userAgent = userAgent, + isInWidgetMode = isInWidgetMode, + eventSink = ::handleEvents, + ) + } + + private fun CoroutineScope.loadUrl( + inputs: CallType, + urlState: MutableState>, + callWidgetDriver: MutableState, + ) = launch { + urlState.runCatchingUpdatingState { + when (inputs) { + is CallType.ExternalUrl -> { + inputs.url + } + is CallType.RoomCall -> { + val (driver, url) = callWidgetProvider.getWidget( + sessionId = inputs.sessionId, + roomId = inputs.roomId, + clientId = UUID.randomUUID().toString(), + ).getOrThrow() + callWidgetDriver.value = driver + url + } + } + } + } + + private fun parseMessage(message: String): WidgetMessage? { + return WidgetMessageSerializer.deserialize(message).getOrNull() + } + + private fun sendHangupMessage(widgetId: String, messageInterceptor: WidgetMessageInterceptor) { + val message = WidgetMessage( + direction = WidgetMessage.Direction.ToWidget, + widgetId = widgetId, + requestId = "widgetapi-${clock.epochMillis()}", + action = WidgetMessage.Action.HangUp, + ) + messageInterceptor.sendMessage(WidgetMessageSerializer.serialize(message)) + } + + private fun CoroutineScope.close(widgetDriver: MatrixWidgetDriver?, navigator: CallScreenNavigator) = launch(dispatchers.io) { + navigator.close() + widgetDriver?.close() + } + +} + diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt new file mode 100644 index 0000000000..d9716251fc --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.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.call.ui + +import io.element.android.libraries.architecture.Async + +data class CallScreenState( + val urlState: Async, + val userAgent: String, + val isInWidgetMode: Boolean, + val eventSink: (CallScreeEvents) -> Unit, +) diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt similarity index 52% rename from features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt rename to features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt index 0f5b90cbc8..9ac06a63b7 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt @@ -14,106 +14,128 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.ui import android.annotation.SuppressLint import android.view.ViewGroup import android.webkit.PermissionRequest import android.webkit.WebChromeClient import android.webkit.WebView +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView +import io.element.android.features.call.R +import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor +import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.theme.ElementTheme typealias RequestPermissionCallback = (Array) -> Unit +interface CallScreenNavigator { + fun close() +} + @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun CallScreenView( - url: String?, - userAgent: String, + state: CallScreenState, requestPermissions: (Array, RequestPermissionCallback) -> Unit, - onClose: () -> Unit, modifier: Modifier = Modifier, ) { - ElementTheme { - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.element_call)) }, - navigationIcon = { - BackButton( - resourceId = CommonDrawables.ic_compound_close, - onClick = onClose - ) - } - ) - } - ) { padding -> - CallWebView( - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - .fillMaxSize(), - url = url, - userAgent = userAgent, - onPermissionsRequested = { request -> - val androidPermissions = mapWebkitPermissions(request.resources) - val callback: RequestPermissionCallback = { request.grant(it) } - requestPermissions(androidPermissions.toTypedArray(), callback) + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.element_call)) }, + navigationIcon = { + BackButton( + resourceId = CommonDrawables.ic_compound_close, + onClick = { state.eventSink(CallScreeEvents.Hangup) } + ) } ) } + ) { padding -> + BackHandler { + state.eventSink(CallScreeEvents.Hangup) + } + CallWebView( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .fillMaxSize(), + url = state.urlState, + userAgent = state.userAgent, + onPermissionsRequested = { request -> + val androidPermissions = mapWebkitPermissions(request.resources) + val callback: RequestPermissionCallback = { request.grant(it) } + requestPermissions(androidPermissions.toTypedArray(), callback) + }, + onWebViewCreated = { webView -> + val interceptor = WebViewWidgetMessageInterceptor(webView) + state.eventSink(CallScreeEvents.SetupMessageChannels(interceptor)) + } + ) } } @Composable private fun CallWebView( - url: String?, + url: Async, userAgent: String, onPermissionsRequested: (PermissionRequest) -> Unit, + onWebViewCreated: (WebView) -> Unit, modifier: Modifier = Modifier, ) { - val isInpectionMode = LocalInspectionMode.current - AndroidView( - modifier = modifier, - factory = { context -> - WebView(context).apply { - if (!isInpectionMode) { - setup(userAgent, onPermissionsRequested) - if (url != null) { - loadUrl(url) - } - } - } - }, - update = { webView -> - if (!isInpectionMode && url != null) { - webView.loadUrl(url) - } - }, - onRelease = { webView -> - webView.destroy() + if (LocalInspectionMode.current) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Text("WebView - can't be previewed") } - ) + } else { + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { + setup(userAgent, onPermissionsRequested) + if (url is Async.Success) { + loadUrl(url.data) + } + + onWebViewCreated(this) + } + }, + update = { webView -> + if (url is Async.Success && webView.url != url.data) { + webView.loadUrl(url.data) + } + }, + onRelease = { webView -> + webView.destroy() + } + ) + } } @SuppressLint("SetJavaScriptEnabled") -private fun WebView.setup(userAgent: String, onPermissionsRequested: (PermissionRequest) -> Unit) { +private fun WebView.setup( + userAgent: String, + onPermissionsRequested: (PermissionRequest) -> Unit, +) { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT @@ -140,12 +162,15 @@ private fun WebView.setup(userAgent: String, onPermissionsRequested: (Permission @PreviewsDayNight @Composable internal fun CallScreenViewPreview() { - ElementTheme { + ElementPreview { CallScreenView( - url = "https://call.element.io/some-actual-call?with=parameters", - userAgent = "", + state = CallScreenState( + urlState = Async.Success("https://call.element.io/some-actual-call?with=parameters"), + isInWidgetMode = false, + userAgent = "", + eventSink = {}, + ), requestPermissions = { _, _ -> }, - onClose = { }, ) } } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt similarity index 72% rename from features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt rename to features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt index 481634a4ca..651a2176f3 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt @@ -14,10 +14,12 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.ui import android.Manifest +import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.res.Configuration import android.media.AudioAttributes import android.media.AudioFocusRequest @@ -26,20 +28,40 @@ import android.os.Build import android.os.Bundle import android.view.WindowManager import android.webkit.PermissionRequest -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.mutableStateOf +import androidx.core.content.IntentCompat +import com.bumble.appyx.core.integrationpoint.NodeComponentActivity +import io.element.android.features.call.CallForegroundService +import io.element.android.features.call.CallType import io.element.android.features.call.di.CallBindings +import io.element.android.features.call.utils.CallIntentDataParser import io.element.android.libraries.architecture.bindings -import io.element.android.libraries.network.useragent.UserAgentProvider +import io.element.android.libraries.theme.ElementTheme import javax.inject.Inject -class ElementCallActivity : ComponentActivity() { +class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator { + companion object { + private const val EXTRA_CALL_WIDGET_SETTINGS = "EXTRA_CALL_WIDGET_SETTINGS" + + fun start( + context: Context, + callInputs: CallType, + ) { + val intent = Intent(context, ElementCallActivity::class.java).apply { + putExtra(EXTRA_CALL_WIDGET_SETTINGS, callInputs) + addFlags(FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + } - @Inject lateinit var userAgentProvider: UserAgentProvider @Inject lateinit var callIntentDataParser: CallIntentDataParser + @Inject lateinit var presenterFactory: CallScreenPresenter.Factory + + private lateinit var presenter: CallScreenPresenter private lateinit var audioManager: AudioManager @@ -51,7 +73,7 @@ class ElementCallActivity : ComponentActivity() { private val requestPermissionsLauncher = registerPermissionResultLauncher() private var isDarkMode = false - private val urlState = mutableStateOf(null) + private val webViewTarget = mutableStateOf(null) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -60,10 +82,7 @@ class ElementCallActivity : ComponentActivity() { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - urlState.value = intent?.dataString?.let(::parseUrl) ?: run { - finish() - return - } + setCallType(intent) if (savedInstanceState == null) { updateUiMode(resources.configuration) @@ -72,18 +91,17 @@ class ElementCallActivity : ComponentActivity() { audioManager = getSystemService(AUDIO_SERVICE) as AudioManager requestAudioFocus() - val userAgent = userAgentProvider.provide() - setContent { - CallScreenView( - url = urlState.value, - userAgent = userAgent, - onClose = this::finish, - requestPermissions = { permissions, callback -> - requestPermissionCallback = callback - requestPermissionsLauncher.launch(permissions) - } - ) + val state = presenter.present() + ElementTheme { + CallScreenView( + state = state, + requestPermissions = { permissions, callback -> + requestPermissionCallback = callback + requestPermissionsLauncher.launch(permissions) + } + ) + } } } @@ -96,15 +114,7 @@ class ElementCallActivity : ComponentActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - val intentUrl = intent?.dataString?.let(::parseUrl) - when { - // New URL, update it and reload the webview - intentUrl != null -> urlState.value = intentUrl - // Re-opened the activity but we have no url to load or a cached one, finish the activity - intent?.dataString == null && urlState.value == null -> finish() - // Coming back from notification, do nothing - else -> return - } + setCallType(intent) } override fun onStart() { @@ -130,6 +140,32 @@ class ElementCallActivity : ComponentActivity() { finishAndRemoveTask() } + override fun close() { + finish() + } + + private fun setCallType(intent: Intent?) { + val inputs = intent?.let { + IntentCompat.getParcelableExtra(it, EXTRA_CALL_WIDGET_SETTINGS, CallType::class.java) + } + val intentUrl = intent?.dataString?.let(::parseUrl) + when { + // Re-opened the activity but we have no url to load or a cached one, finish the activity + intent?.dataString == null && inputs == null && webViewTarget.value == null -> finish() + inputs != null -> { + webViewTarget.value = inputs + presenter = presenterFactory.create(inputs, this) + } + intentUrl != null -> { + val fallbackInputs = CallType.ExternalUrl(intentUrl) + webViewTarget.value = fallbackInputs + presenter = presenterFactory.create(fallbackInputs, this) + } + // Coming back from notification, do nothing + else -> return + } + } + private fun parseUrl(url: String?): String? = callIntentDataParser.parse(url) private fun registerPermissionResultLauncher(): ActivityResultLauncher> { diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt similarity index 98% rename from features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt rename to features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt index b903b437d8..0814216745 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.utils import android.net.Uri import javax.inject.Inject diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt new file mode 100644 index 0000000000..b65298854d --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.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.call.utils + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver + +interface CallWidgetProvider { + suspend fun getWidget( + sessionId: SessionId, + roomId: RoomId, + clientId: String, + languageTag: String? = null, + theme: String? = null, + ): Result> +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt new file mode 100644 index 0000000000..f3cb9cbcd5 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt @@ -0,0 +1,50 @@ +/* + * 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.call.utils + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.ElementCallConfig +import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultCallWidgetProvider @Inject constructor( + private val matrixClientsProvider: MatrixClientProvider, + private val preferencesStore: PreferencesStore, + private val callWidgetSettingsProvider: CallWidgetSettingsProvider, +) : CallWidgetProvider { + override suspend fun getWidget( + sessionId: SessionId, + roomId: RoomId, + clientId: String, + languageTag: String?, + theme: String?, + ): Result> = runCatching { + val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found") + val baseUrl = preferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL + val widgetSettings = callWidgetSettingsProvider.provide(baseUrl) + val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow() + room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt new file mode 100644 index 0000000000..bdb6ee48f5 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt @@ -0,0 +1,100 @@ +/* + * 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.call.utils + +import android.graphics.Bitmap +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature +import io.element.android.features.call.BuildConfig +import kotlinx.coroutines.flow.MutableSharedFlow + +class WebViewWidgetMessageInterceptor( + private val webView: WebView, +) : WidgetMessageInterceptor { + + companion object { + // We call both the WebMessageListener and the JavascriptInterface objects in JS with this + // 'listenerName' so they can both receive the data from the WebView when + // `${LISTENER_NAME}.postMessage(...)` is called + const val LISTENER_NAME = "elementX" + } + + override val interceptedMessages = MutableSharedFlow(replay = 1, extraBufferCapacity = 2) + + init { + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + // We inject this JS code when the page starts loading to attach a message listener to the window. + // This listener will receive both messages: + // - EC widget API -> Element X (message.data.api == "fromWidget") + // - Element X -> EC widget API (message.data.api == "toWidget"), we should ignore these + view?.evaluateJavascript( + """ + window.addEventListener('message', function(event) { + let message = {data: event.data, origin: event.origin} + if (message.data.response && message.data.api == "toWidget" + || !message.data.response && message.data.api == "fromWidget") { + let json = JSON.stringify(event.data) + ${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG } } + ${LISTENER_NAME}.postMessage(json); + } else { + ${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG } } + } + }); + """.trimIndent(), + null + ) + } + } + + // Create a WebMessageListener, which will receive messages from the WebView and reply to them + val webMessageListener = WebViewCompat.WebMessageListener { _, message, _, _, _ -> + onMessageReceived(message.data) + } + + // Use WebMessageListener if supported, otherwise use JavascriptInterface + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + WebViewCompat.addWebMessageListener( + webView, + LISTENER_NAME, + setOf("*"), + webMessageListener + ) + } else { + webView.addJavascriptInterface(object { + @JavascriptInterface + fun postMessage(json: String?) { + onMessageReceived(json) + } + }, LISTENER_NAME) + } + } + + override fun sendMessage(message: String) { + webView.evaluateJavascript("postMessage($message, '*')", null) + } + + private fun onMessageReceived(json: String?) { + // Here is where we would handle the messages from the WebView, passing them to the Rust SDK + json?.let { interceptedMessages.tryEmit(it) } + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt new file mode 100644 index 0000000000..fa5c3bea67 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt @@ -0,0 +1,24 @@ +/* + * 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.call.utils + +import kotlinx.coroutines.flow.Flow + +interface WidgetMessageInterceptor { + val interceptedMessages: Flow + fun sendMessage(message: String) +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt new file mode 100644 index 0000000000..5ed9db028c --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.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.call.utils + +import io.element.android.features.call.data.WidgetMessage +import kotlinx.serialization.json.Json + +object WidgetMessageSerializer { + + private val coder = Json { ignoreUnknownKeys = true } + + fun deserialize(message: String): Result { + return runCatching { coder.decodeFromString(WidgetMessage.serializer(), message) } + } + + fun serialize(message: WidgetMessage): String { + return coder.encodeToString(WidgetMessage.serializer(), message) + } +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt index f82e31c068..55b5f16771 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt @@ -19,6 +19,7 @@ package io.element.android.features.call import android.Manifest import android.webkit.PermissionRequest import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.ui.mapWebkitPermissions import org.junit.Test class MapWebkitPermissionsTest { diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt new file mode 100644 index 0000000000..c318b1dfaa --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -0,0 +1,194 @@ +/* + * 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.call.ui + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.CallType +import io.element.android.features.call.utils.FakeCallWidgetProvider +import io.element.android.features.call.utils.FakeWidgetMessageInterceptor +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver +import io.element.android.libraries.network.useragent.UserAgentProvider +import io.element.android.services.toolbox.api.systemclock.SystemClock +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class CallScreenPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - with CallType ExternalUrl just loads the URL`() = runTest { + val presenter = createCallScreenPresenter(CallType.ExternalUrl("https://call.element.io")) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Wait until the URL is loaded + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.urlState).isEqualTo(Async.Success("https://call.element.io")) + assertThat(initialState.isInWidgetMode).isFalse() + } + } + + @Test + fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest { + val widgetDriver = FakeWidgetDriver() + val widgetProvider = FakeCallWidgetProvider(widgetDriver) + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + widgetProvider = widgetProvider, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Wait until the URL is loaded + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.urlState).isInstanceOf(Async.Success::class.java) + assertThat(initialState.isInWidgetMode).isTrue() + assertThat(widgetProvider.getWidgetCalled).isTrue() + assertThat(widgetDriver.runCalledCount).isEqualTo(1) + } + } + + @Test + fun `present - set message interceptor, send and receive messages`() = runTest { + val widgetDriver = FakeWidgetDriver() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + ) + val messageInterceptor = FakeWidgetMessageInterceptor() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + + // And incoming message from the Widget Driver is passed to the WebView + widgetDriver.givenIncomingMessage("A message") + assertThat(messageInterceptor.sentMessages).containsExactly("A message") + + // And incoming message from the WebView is passed to the Widget Driver + messageInterceptor.givenInterceptedMessage("A reply") + assertThat(widgetDriver.sentMessages).containsExactly("A reply") + + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeWidgetDriver() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + ) + val messageInterceptor = FakeWidgetMessageInterceptor() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + + initialState.eventSink(CallScreeEvents.Hangup) + + // Let background coroutines run + runCurrent() + + assertThat(navigator.closeCalled).isTrue() + assertThat(widgetDriver.closeCalledCount).isEqualTo(1) + + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeWidgetDriver() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + ) + val messageInterceptor = FakeWidgetMessageInterceptor() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + + messageInterceptor.givenInterceptedMessage("""{"action":"im.vector.hangup","api":"fromWidget","widgetId":"1","requestId":"1"}""") + + // Let background coroutines run + runCurrent() + + assertThat(navigator.closeCalled).isTrue() + assertThat(widgetDriver.closeCalledCount).isEqualTo(1) + + cancelAndIgnoreRemainingEvents() + } + } + + private fun TestScope.createCallScreenPresenter( + callType: CallType, + navigator: CallScreenNavigator = FakeCallScreenNavigator(), + widgetDriver: FakeWidgetDriver = FakeWidgetDriver(), + widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + ): CallScreenPresenter { + val userAgentProvider = object : UserAgentProvider { + override fun provide(): String { + return "Test" + } + } + val clock = SystemClock { 0 } + return CallScreenPresenter( + callType, + navigator, + widgetProvider, + userAgentProvider, + clock, + dispatchers, + ) + } +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt b/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt new file mode 100644 index 0000000000..498503cb15 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.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.call.ui + +class FakeCallScreenNavigator : CallScreenNavigator { + var closeCalled = false + private set + + override fun close() { + closeCalled = true + } + } diff --git a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt similarity index 99% rename from features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTest.kt rename to features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt index ae82767f45..eb8e756182 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTest.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.utils import com.google.common.truth.Truth.assertThat import org.junit.Test diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt new file mode 100644 index 0000000000..f7f17d794d --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt @@ -0,0 +1,121 @@ +/* + * 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.call.utils + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider +import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultCallWidgetProviderTest { + + @Test + fun `getWidget - fails if the session does not exist`() = runTest { + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.failure(Exception("Session not found")) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() + } + + @Test + fun `getWidget - fails if the room does not exist`() = runTest { + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, null) + } + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() + } + + @Test + fun `getWidget - fails if it can't generate the URL for the widget`() = runTest { + val room = FakeMatrixRoom().apply { + givenGenerateWidgetWebViewUrlResult(Result.failure(Exception("Can't generate URL for widget"))) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() + } + + @Test + fun `getWidget - fails if it can't get the widget driver`() = runTest { + val room = FakeMatrixRoom().apply { + givenGenerateWidgetWebViewUrlResult(Result.success("url")) + givenGetWidgetDriverResult(Result.failure(Exception("Can't get a widget driver"))) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() + } + + @Test + fun `getWidget - returns a widget driver when all steps are successful`() = runTest { + val room = FakeMatrixRoom().apply { + givenGenerateWidgetWebViewUrlResult(Result.success("url")) + givenGetWidgetDriverResult(Result.success(FakeWidgetDriver())) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull() + } + + @Test + fun `getWidget - will use a custom base url if it exists`() = runTest { + val room = FakeMatrixRoom().apply { + givenGenerateWidgetWebViewUrlResult(Result.success("url")) + givenGetWidgetDriverResult(Result.success(FakeWidgetDriver())) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val preferencesStore = InMemoryPreferencesStore().apply { + setCustomElementCallBaseUrl("https://custom.element.io") + } + val settingsProvider = FakeCallWidgetSettingsProvider() + val provider = createProvider( + matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }, + callWidgetSettingsProvider = settingsProvider, + preferencesStore = preferencesStore, + ) + provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme") + + assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io") + } + + private fun createProvider( + matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(), + preferencesStore: PreferencesStore = InMemoryPreferencesStore(), + callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider() + ) = DefaultCallWidgetProvider( + matrixClientProvider, + preferencesStore, + callWidgetSettingsProvider, + ) +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt b/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt new file mode 100644 index 0000000000..69ae340648 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.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.call.utils + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver + +class FakeCallWidgetProvider( + private val widgetDriver: FakeWidgetDriver = FakeWidgetDriver(), + private val url: String = "https://call.element.io", + ) : CallWidgetProvider { + + var getWidgetCalled = false + private set + + override suspend fun getWidget( + sessionId: SessionId, + roomId: RoomId, + clientId: String, + languageTag: String?, + theme: String? + ): Result> { + getWidgetCalled = true + return Result.success(widgetDriver to url) + } + } diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt b/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt new file mode 100644 index 0000000000..6e36dfff81 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.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.call.utils + +import kotlinx.coroutines.flow.MutableSharedFlow + +class FakeWidgetMessageInterceptor : WidgetMessageInterceptor { + val sentMessages = mutableListOf() + + override val interceptedMessages = MutableSharedFlow(extraBufferCapacity = 1) + + override fun sendMessage(message: String) { + sentMessages += message + } + + fun givenInterceptedMessage(message: String) { + interceptedMessages.tryEmit(message) + } + } diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index ae13197f05..6f4e959499 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -38,6 +38,7 @@ anvil { dependencies { implementation(projects.anvilannotations) + implementation(projects.appconfig) anvil(projects.anvilcodegen) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt index 35fd7246f2..0d3b9c5dc3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.login.impl.accountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.util.LoginConstants +import io.element.android.appconfig.AuthenticationConfig open class AccountProviderProvider : PreviewParameterProvider { override val values: Sequence @@ -32,7 +32,7 @@ open class AccountProviderProvider : PreviewParameterProvider { } fun anAccountProvider() = AccountProvider( - url = LoginConstants.MATRIX_ORG_URL, + url = AuthenticationConfig.MATRIX_ORG_URL, subtitle = "Matrix.org is an open network for secure, decentralized communication.", isPublic = true, isMatrixOrg = true, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt index 786d8aaeae..96fc115cfa 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -17,9 +17,9 @@ package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.runtime.Composable +import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.changeserver.ChangeServerPresenter -import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -34,7 +34,7 @@ class ChangeAccountProviderPresenter @Inject constructor( // Just matrix.org by default for now accountProviders = listOf( AccountProvider( - url = LoginConstants.MATRIX_ORG_URL, + url = AuthenticationConfig.MATRIX_ORG_URL, subtitle = null, isPublic = true, isMatrixOrg = true, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt index 8dce1bd78e..50b24b3964 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt @@ -17,9 +17,9 @@ package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.resolver.HomeserverData -import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async open class SearchAccountProviderStateProvider : PreviewParameterProvider { @@ -50,7 +50,7 @@ fun aHomeserverDataList(): List { } fun aHomeserverData( - homeserverUrl: String = LoginConstants.MATRIX_ORG_URL, + homeserverUrl: String = AuthenticationConfig.MATRIX_ORG_URL, isWellknownValid: Boolean = true, supportSlidingSync: Boolean = true, ): HomeserverData { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt index 29781acff1..47dfe248b7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt @@ -14,12 +14,11 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@file:OptIn(ExperimentalMaterial3Api::class) package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -48,13 +47,13 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.R import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderView import io.element.android.features.login.impl.changeserver.ChangeServerEvents import io.element.android.features.login.impl.changeserver.ChangeServerView import io.element.android.features.login.impl.resolver.HomeserverData -import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.components.button.BackButton @@ -196,7 +195,7 @@ fun SearchAccountProviderView( @Composable private fun HomeserverData.toAccountProvider(): AccountProvider { - val isMatrixOrg = homeserverUrl == LoginConstants.MATRIX_ORG_URL + val isMatrixOrg = homeserverUrl == AuthenticationConfig.MATRIX_ORG_URL return AccountProvider( url = homeserverUrl, subtitle = if (isMatrixOrg) stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle) else null, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt index 98fd62d7b0..91c19e4052 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt @@ -16,18 +16,12 @@ package io.element.android.features.login.impl.util +import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.accountprovider.AccountProvider -object LoginConstants { - const val MATRIX_ORG_URL = "https://matrix.org" - - const val DEFAULT_HOMESERVER_URL = "https://matrix.org" - const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" -} - val defaultAccountProvider = AccountProvider( - url = LoginConstants.DEFAULT_HOMESERVER_URL, + url = AuthenticationConfig.DEFAULT_HOMESERVER_URL, subtitle = null, - isPublic = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL, - isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL, + isPublic = AuthenticationConfig.DEFAULT_HOMESERVER_URL == AuthenticationConfig.MATRIX_ORG_URL, + isMatrixOrg = AuthenticationConfig.DEFAULT_HOMESERVER_URL == AuthenticationConfig.MATRIX_ORG_URL, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt index 261b02c1b8..6726105bce 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt @@ -19,9 +19,10 @@ package io.element.android.features.login.impl.util import android.content.Context import android.content.Intent import android.net.Uri +import io.element.android.appconfig.AuthenticationConfig import io.element.android.libraries.core.data.tryOrNull fun openLearnMorePage(context: Context) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL)) + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(AuthenticationConfig.SLIDING_SYNC_READ_MORE_URL)) tryOrNull { context.startActivity(intent) } } diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/api/src/main/res/values/localazy.xml index 9ea4bb77fd..5a5c9c64ca 100644 --- a/features/logout/api/src/main/res/values/localazy.xml +++ b/features/logout/api/src/main/res/values/localazy.xml @@ -1,8 +1,12 @@ + "Please wait for this to complete before signing out." + "Your keys are still being backed up" "Are you sure you want to sign out?" "Sign out" "Signing out…" + "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." + "Have you saved your recovery key?" "Sign out" "Sign out" diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index e886a3aeaa..956b80949a 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) api(projects.features.messages.api) + implementation(projects.features.call) implementation(projects.features.location.api) implementation(projects.features.poll.api) implementation(projects.libraries.androidutils) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 21e384906e..128c531374 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl +import android.content.Context import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -29,6 +30,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.call.CallType +import io.element.android.features.call.ui.ElementCallActivity import io.element.android.features.location.api.Location import io.element.android.features.location.api.SendLocationEntryPoint import io.element.android.features.location.api.ShowLocationEntryPoint @@ -50,7 +53,9 @@ import io.element.android.features.poll.api.create.CreatePollEntryPoint 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.ApplicationContext import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -63,6 +68,8 @@ import kotlinx.parcelize.Parcelize class MessagesFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + @ApplicationContext private val context: Context, + private val matrixClient: MatrixClient, private val sendLocationEntryPoint: SendLocationEntryPoint, private val showLocationEntryPoint: ShowLocationEntryPoint, private val createPollEntryPoint: CreatePollEntryPoint, @@ -149,6 +156,14 @@ class MessagesFlowNode @AssistedInject constructor( override fun onCreatePollClicked() { backstack.push(NavTarget.CreatePoll) } + + override fun onJoinCallClicked(roomId: RoomId) { + val inputs = CallType.RoomCall( + sessionId = matrixClient.sessionId, + roomId = roomId, + ) + ElementCallActivity.start(context, inputs) + } } createNode(buildContext, listOf(callback)) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index dbf7e2fbb2..50b59afbcc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -33,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo @@ -63,6 +64,7 @@ class MessagesNode @AssistedInject constructor( fun onReportMessage(eventId: EventId, senderId: UserId) fun onSendLocationClicked() fun onCreatePollClicked() + fun onJoinCallClicked(roomId: RoomId) } init { @@ -108,6 +110,10 @@ class MessagesNode @AssistedInject constructor( callback?.onCreatePollClicked() } + private fun onJoinCallClicked() { + callback?.onJoinCallClicked(room.roomId) + } + @Composable override fun View(modifier: Modifier) { CompositionLocalProvider( @@ -123,6 +129,7 @@ class MessagesNode @AssistedInject constructor( onUserDataClicked = this::onUserDataClicked, onSendLocationClicked = this::onSendLocationClicked, onCreatePollClicked = this::onCreatePollClicked, + onJoinCallClicked = this::onJoinCallClicked, modifier = modifier, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 8645553f0b..6125e920b8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -152,8 +152,10 @@ class MessagesPresenter @AssistedInject constructor( val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) var enableVoiceMessages by remember { mutableStateOf(false) } + var enableInRoomCalls by remember { mutableStateOf(false) } LaunchedEffect(featureFlagsService) { enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages) + enableInRoomCalls = featureFlagsService.isFeatureEnabled(FeatureFlags.InRoomCalls) } fun handleEvents(event: MessagesEvents) { @@ -200,6 +202,7 @@ class MessagesPresenter @AssistedInject constructor( inviteProgress = inviteProgress.value, enableTextFormatting = enableTextFormatting, enableVoiceMessages = enableVoiceMessages, + enableInRoomCalls = enableInRoomCalls, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 0a121b50a3..3a0585f390 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -49,5 +49,6 @@ data class MessagesState( val showReinvitePrompt: Boolean, val enableTextFormatting: Boolean, val enableVoiceMessages: Boolean, + val enableInRoomCalls: Boolean, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 3b0b87ea39..4222a0889d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -85,5 +85,6 @@ fun aMessagesState() = MessagesState( showReinvitePrompt = false, enableTextFormatting = true, enableVoiceMessages = true, + enableInRoomCalls = true, eventSink = {} ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index b79e84a2e0..5a7168e7ce 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -76,9 +76,12 @@ import io.element.android.libraries.designsystem.components.dialogs.Confirmation import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState @@ -99,6 +102,7 @@ fun MessagesView( onPreviewAttachments: (ImmutableList) -> Unit, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, + onJoinCallClicked: () -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") @@ -160,8 +164,10 @@ fun MessagesView( MessagesViewTopBar( roomName = state.roomName.dataOrNull(), roomAvatar = state.roomAvatar.dataOrNull(), + inRoomCallsEnabled = state.enableInRoomCalls, onBackPressed = onBackPressed, onRoomDetailsClicked = onRoomDetailsClicked, + onJoinCallClicked = onJoinCallClicked, ) } }, @@ -349,8 +355,10 @@ private fun MessagesViewContent( private fun MessagesViewTopBar( roomName: String?, roomAvatar: AvatarData?, + inRoomCallsEnabled: Boolean, modifier: Modifier = Modifier, onRoomDetailsClicked: () -> Unit = {}, + onJoinCallClicked: () -> Unit = {}, onBackPressed: () -> Unit = {}, ) { TopAppBar( @@ -373,6 +381,13 @@ private fun MessagesViewTopBar( ) } }, + actions = { + if (inRoomCallsEnabled) { + IconButton(onClick = onJoinCallClicked) { + Icon(CommonDrawables.ic_compound_video_call, contentDescription = null) // TODO add proper content description once we have the state + } + } + }, windowInsets = WindowInsets(0.dp) ) } @@ -432,5 +447,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onUserDataClicked = {}, onSendLocationClicked = {}, onCreatePollClicked = {}, + onJoinCallClicked = {}, ) } diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index a227d24b8b..4fcc69ff6b 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) implementation(projects.libraries.androidutils) + implementation(projects.appconfig) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt index 37641d684c..fea42baf5f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt @@ -19,4 +19,5 @@ package io.element.android.features.preferences.impl.advanced sealed interface AdvancedSettingsEvents { data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents + data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AdvancedSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index 5738fe43c8..6359b34d0f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -17,16 +17,25 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import io.element.android.appconfig.ElementCallConfig import io.element.android.features.preferences.api.store.PreferencesStore import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import kotlinx.coroutines.launch +import java.net.URL import javax.inject.Inject class AdvancedSettingsPresenter @Inject constructor( private val preferencesStore: PreferencesStore, + private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable @@ -38,6 +47,14 @@ class AdvancedSettingsPresenter @Inject constructor( val isDeveloperModeEnabled by preferencesStore .isDeveloperModeEnabledFlow() .collectAsState(initial = false) + val customElementCallBaseUrl by preferencesStore + .getCustomElementCallBaseUrlFlow() + .collectAsState(initial = null) + + var canDisplayElementCallSettings by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + canDisplayElementCallSettings = featureFlagService.isFeatureEnabled(FeatureFlags.InRoomCalls) + } fun handleEvents(event: AdvancedSettingsEvents) { when (event) { @@ -47,13 +64,34 @@ class AdvancedSettingsPresenter @Inject constructor( is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch { preferencesStore.setDeveloperModeEnabled(event.enabled) } + is AdvancedSettingsEvents.SetCustomElementCallBaseUrl -> localCoroutineScope.launch { + // If the URL is either empty or the default one, we want to save 'null' to remove the custom URL + val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL } + preferencesStore.setCustomElementCallBaseUrl(urlToSave) + } } } return AdvancedSettingsState( isRichTextEditorEnabled = isRichTextEditorEnabled, isDeveloperModeEnabled = isDeveloperModeEnabled, + customElementCallBaseUrlState = if (canDisplayElementCallSettings) { + CustomElementCallBaseUrlState( + baseUrl = customElementCallBaseUrl, + defaultUrl = ElementCallConfig.DEFAULT_BASE_URL, + validator = ::customElementCallUrlValidator, + ) + } else null, eventSink = ::handleEvents ) } + + private fun customElementCallUrlValidator(url: String?): Boolean { + return runCatching { + if (url.isNullOrEmpty()) return@runCatching + val parsedUrl = URL(url) + if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol") + if (parsedUrl.host.isNullOrBlank()) error("Missing host") + }.isSuccess + } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt index 19625b9ebc..cd56078b27 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt @@ -16,8 +16,15 @@ package io.element.android.features.preferences.impl.advanced -data class AdvancedSettingsState constructor( +data class AdvancedSettingsState( val isRichTextEditorEnabled: Boolean, val isDeveloperModeEnabled: Boolean, + val customElementCallBaseUrlState: CustomElementCallBaseUrlState?, val eventSink: (AdvancedSettingsEvents) -> Unit ) + +data class CustomElementCallBaseUrlState( + val baseUrl: String?, + val defaultUrl: String, + val validator: (String?) -> Boolean, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt index 5ab50c8a16..d3a2dee3f4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt @@ -24,14 +24,17 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider Unit, modifier: Modifier = Modifier, ) { + fun isUsingDefaultUrl(value: String?): Boolean { + val defaultUrl = state.customElementCallBaseUrlState?.defaultUrl ?: return false + return value.isNullOrEmpty() || value == defaultUrl + } + PreferencePage( modifier = modifier, onBackPressed = onBackPressed, @@ -50,6 +58,23 @@ fun AdvancedSettingsView( isChecked = state.isDeveloperModeEnabled, onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) }, ) + state.customElementCallBaseUrlState?.let { callUrlState -> + val supportingText = if (isUsingDefaultUrl(callUrlState.baseUrl)) { + stringResource(R.string.screen_advanced_settings_element_call_base_url_description) + } else { + callUrlState.baseUrl + } + PreferenceTextField( + headline = stringResource(R.string.screen_advanced_settings_element_call_base_url), + value = callUrlState.baseUrl ?: callUrlState.defaultUrl, + supportingText = supportingText, + validation = callUrlState.validator, + onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error), + displayValue = { value -> !isUsingDefaultUrl(value) }, + keyboardOptions = KeyboardOptions.Default.copy(autoCorrect = false, keyboardType = KeyboardType.Uri), + onChange = { state.eventSink(AdvancedSettingsEvents.SetCustomElementCallBaseUrl(it)) } + ) + } } } diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index b94db7a565..1ca7071436 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -1,5 +1,8 @@ + "Custom Element Call base URL" + "Set a custom base URL for Element Call." + "Invalid URL, please make sure you include the protocol (http/https) and the correct address." "Developer mode" "Enable to have access to features and functionality for developers." "Disable the rich text editor to type Markdown manually." diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index 76808ee5f9..11c79657ce 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest @@ -34,7 +36,8 @@ class AdvancedSettingsPresenterTest { @Test fun `present - initial state`() = runTest { val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val featureFlagService = FakeFeatureFlagService() + val presenter = AdvancedSettingsPresenter(store, featureFlagService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -47,7 +50,8 @@ class AdvancedSettingsPresenterTest { @Test fun `present - developer mode on off`() = runTest { val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val featureFlagService = FakeFeatureFlagService() + val presenter = AdvancedSettingsPresenter(store, featureFlagService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -63,7 +67,8 @@ class AdvancedSettingsPresenterTest { @Test fun `present - rich text editor on off`() = runTest { val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val featureFlagService = FakeFeatureFlagService() + val presenter = AdvancedSettingsPresenter(store, featureFlagService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -75,4 +80,64 @@ class AdvancedSettingsPresenterTest { assertThat(awaitItem().isRichTextEditorEnabled).isFalse() } } + + @Test + fun `present - custom element call url state is null if the feature flag is disabled`() = runTest { + val store = InMemoryPreferencesStore() + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.InRoomCalls, false) + } + val presenter = AdvancedSettingsPresenter(store, featureFlagService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.customElementCallBaseUrlState).isNull() + } + } + + @Test + fun `present - custom element call base url`() = runTest { + val store = InMemoryPreferencesStore() + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.InRoomCalls, true) + } + val presenter = AdvancedSettingsPresenter(store, featureFlagService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Initial state has a default `false` feature flag value, so the state will still be null + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.customElementCallBaseUrlState).isNotNull() + assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull() + + initialState.eventSink(AdvancedSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.dev")) + val updatedItem = awaitItem() + assertThat(updatedItem.customElementCallBaseUrlState?.baseUrl).isEqualTo("https://call.element.dev") + } + } + + @Test + fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest { + val store = InMemoryPreferencesStore() + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.InRoomCalls, true) + } + val presenter = AdvancedSettingsPresenter(store, featureFlagService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Initial state has a default `false` feature flag value, so the state will still be null + skipItems(1) + + val urlValidator = awaitItem().customElementCallBaseUrlState!!.validator + assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one + assertThat(urlValidator("test")).isFalse() + assertThat(urlValidator("http://")).isFalse() + assertThat(urlValidator("geo://test")).isFalse() + assertThat(urlValidator("https://call.element.io")).isTrue() + } + } } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt index e4a6d7ae7d..f8b6cee0b3 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt @@ -21,6 +21,7 @@ import android.content.ContextWrapper import com.bumble.appyx.core.node.Node import io.element.android.libraries.di.DaggerComponentOwner +inline fun Node.optionalBindings() = optionalBindings(T::class.java) inline fun Node.bindings() = bindings(T::class.java) inline fun Context.bindings() = bindings(T::class.java) @@ -36,7 +37,7 @@ fun Context.bindings(klass: Class): T { ?: error("Unable to find bindings for ${klass.name}") } -fun Node.bindings(klass: Class): T { +fun Node.optionalBindings(klass: Class): T? { // search dagger components in node hierarchy return generateSequence(this, Node::parent) .filterIsInstance() @@ -44,5 +45,8 @@ fun Node.bindings(klass: Class): T { .flatMap { if (it is Collection<*>) it else listOf(it) } .filterIsInstance(klass) .firstOrNull() - ?: error("Unable to find bindings for ${klass.name}") +} + +fun Node.bindings(klass: Class): T { + return optionalBindings(klass) ?: error("Unable to find bindings for ${klass.name}") } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt index e82ebfe532..42cd70cbbd 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt @@ -27,9 +27,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.airbnb.android.showkase.annotation.ShowkaseComposable import io.element.android.libraries.designsystem.components.list.TextFieldListItem -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.DialogPreview import io.element.android.libraries.designsystem.theme.components.ListSupportingText import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent @@ -45,6 +45,7 @@ fun ListDialog( subtitle: String? = null, cancelText: String = stringResource(CommonStrings.action_cancel), submitText: String = stringResource(CommonStrings.action_ok), + enabled: Boolean = true, listItems: LazyListScope.() -> Unit, ) { val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let { @@ -66,6 +67,7 @@ fun ListDialog( submitText = submitText, onDismissRequest = onDismissRequest, onSubmitClicked = onSubmit, + enabled = enabled, listItems = listItems, ) } @@ -80,6 +82,7 @@ private fun ListDialogContent( submitText: String, modifier: Modifier = Modifier, title: String? = null, + enabled: Boolean = true, subtitle: @Composable (() -> Unit)? = null, ) { SimpleAlertDialogContent( @@ -90,6 +93,7 @@ private fun ListDialogContent( submitText = submitText, onCancelClicked = onDismissRequest, onSubmitClicked = onSubmitClicked, + enabled = enabled, applyPaddingToContents = false, ) { LazyColumn( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt index 525d5e76b1..93268e25d7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt @@ -16,10 +16,13 @@ package io.element.android.libraries.designsystem.components.list +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup @@ -29,24 +32,68 @@ import io.element.android.libraries.theme.ElementTheme @Composable fun TextFieldListItem( - placeholder: String, + placeholder: String?, text: String, onTextChanged: (String) -> Unit, modifier: Modifier = Modifier, + error: String? = null, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, ) { val textFieldStyle = ElementTheme.materialTypography.bodyLarge OutlinedTextField( value = text, - onValueChange = onTextChanged, - placeholder = { Text(placeholder) }, + onValueChange = { onTextChanged(it) }, + placeholder = placeholder?.let { @Composable { Text(it) } }, colors = OutlinedTextFieldDefaults.colors( disabledBorderColor = Color.Transparent, errorBorderColor = Color.Transparent, focusedBorderColor = Color.Transparent, unfocusedBorderColor = Color.Transparent, ), + isError = error != null, + supportingText = error?.let { @Composable { Text(it) } }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, textStyle = textFieldStyle, + maxLines = maxLines, + singleLine = maxLines == 1, + modifier = modifier, + ) +} + +@Composable +fun TextFieldListItem( + placeholder: String?, + text: TextFieldValue, + onTextChanged: (TextFieldValue) -> Unit, + modifier: Modifier = Modifier, + error: String? = null, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, +) { + val textFieldStyle = ElementTheme.materialTypography.bodyLarge + + OutlinedTextField( + value = text, + onValueChange = { onTextChanged(it) }, + placeholder = placeholder?.let { @Composable { Text(it) } }, + colors = OutlinedTextFieldDefaults.colors( + disabledBorderColor = Color.Transparent, + errorBorderColor = Color.Transparent, + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + ), + isError = error != null, + supportingText = error?.let { @Composable { Text(it) } }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + textStyle = textFieldStyle, + maxLines = maxLines, + singleLine = maxLines == 1, modifier = modifier, ) } @@ -74,3 +121,15 @@ internal fun TextFieldListItemPreview() { ) } } + +@Preview("Text field List item - textfieldvalue", group = PreviewGroup.ListItems) +@Composable +internal fun TextFieldListItemTextFieldValuePreview() { + ElementThemedPreview { + TextFieldListItem( + placeholder = "Placeholder", + text = TextFieldValue("Text field value"), + onTextChanged = {}, + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt new file mode 100644 index 0000000000..648ea97434 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt @@ -0,0 +1,141 @@ +/* + * 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.libraries.designsystem.components.preferences + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import io.element.android.libraries.designsystem.components.dialogs.ListDialog +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.components.list.TextFieldListItem +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun PreferenceTextField( + headline: String, + onChange: (String?) -> Unit, + modifier: Modifier = Modifier, + placeholder: String? = null, + value: String? = null, + supportingText: String? = null, + displayValue: (String?) -> Boolean = { !it.isNullOrBlank() }, + trailingContent: ListItemContent? = null, + validation: (String?) -> Boolean = { true }, + onValidationErrorMessage: String? = null, + enabled: Boolean = true, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + style: ListItemStyle = ListItemStyle.Default, +) { + var displayTextFieldDialog by rememberSaveable { mutableStateOf(false) } + val valueToDisplay = if (displayValue(value)) { value } else supportingText + + ListItem( + modifier = modifier, + headlineContent = { Text(headline) }, + supportingContent = valueToDisplay?.let { @Composable { Text(it) } }, + trailingContent = trailingContent, + style = style, + enabled = enabled, + onClick = { displayTextFieldDialog = true } + ) + + if (displayTextFieldDialog) { + TextFieldDialog( + title = headline, + onSubmit = { + onChange(it.takeIf { it.isNotBlank() }) + displayTextFieldDialog = false + }, + onDismissRequest = { displayTextFieldDialog = false }, + placeholder = placeholder.orEmpty(), + value = value.orEmpty(), + validation = validation, + onValidationErrorMessage = onValidationErrorMessage, + keyboardOptions = keyboardOptions, + ) + } +} + +@Composable +private fun TextFieldDialog( + title: String, + onSubmit: (String) -> Unit, + onDismissRequest: () -> Unit, + value: String?, + placeholder: String?, + modifier: Modifier = Modifier, + validation: (String?) -> Boolean = { true }, + onValidationErrorMessage: String? = null, + autoSelectOnDisplay: Boolean = true, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, +) { + val focusRequester = remember { FocusRequester() } + + var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(value.orEmpty(), selection = TextRange(value.orEmpty().length))) + } + var error by rememberSaveable { mutableStateOf(null) } + val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } } + ListDialog( + title = title, + onSubmit = { onSubmit(textFieldContents.text) }, + onDismissRequest = onDismissRequest, + enabled = canSubmit, + modifier = modifier, + ) { + item { + TextFieldListItem( + placeholder = placeholder.orEmpty(), + text = textFieldContents, + onTextChanged = { + error = if (!validation(it.text)) onValidationErrorMessage else null + textFieldContents = it + }, + error = error, + keyboardOptions = keyboardOptions, + keyboardActions = KeyboardActions(onAny = { + if (validation(textFieldContents.text)) { + onSubmit(textFieldContents.text) + } + }), + maxLines = maxLines, + modifier = Modifier.focusRequester(focusRequester), + ) + } + } + + if (autoSelectOnDisplay) { + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt index abe744bdbc..2eb290dd4e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt @@ -96,6 +96,7 @@ internal fun SimpleAlertDialogContent( thirdButtonText: String? = null, onThirdButtonClicked: () -> Unit = {}, applyPaddingToContents: Boolean = true, + enabled: Boolean = true, icon: @Composable (() -> Unit)? = null, content: @Composable () -> Unit, ) { @@ -122,6 +123,7 @@ internal fun SimpleAlertDialogContent( if (submitText != null) { Button( text = submitText, + enabled = enabled, size = ButtonSize.Medium, onClick = onSubmitClicked, ) 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 121cf26271..e078b634d1 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 @@ -55,4 +55,10 @@ enum class FeatureFlags( description = "Allow user to lock/unlock the app with a pin code or biometrics", defaultValue = false, ), + InRoomCalls( + key = "feature.elementcall", + title = "Element call in rooms", + description = "Allow user to start or join a call in a room", + 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 48f159de83..87a797d13a 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 @@ -37,6 +37,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.NotificationSettings -> true FeatureFlags.VoiceMessages -> false FeatureFlags.PinUnlock -> false + FeatureFlags.InRoomCalls -> false } } else { false diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 5a430f7db5..4a083eacec 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -34,6 +34,7 @@ anvil { } dependencies { + implementation(projects.appconfig) implementation(projects.libraries.di) implementation(libs.dagger) implementation(projects.libraries.core) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt index e352dd5cfc..19e71db332 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt @@ -17,7 +17,7 @@ package io.element.android.libraries.matrix.api.permalink import android.net.Uri -import io.element.android.libraries.matrix.api.config.MatrixConfiguration +import io.element.android.appconfig.MatrixConfiguration /** * Mapping of an input URI to a matrix.to compliant URI. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt index c79ab36a7b..2a388ae580 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt @@ -16,7 +16,7 @@ package io.element.android.libraries.matrix.api.permalink -import io.element.android.libraries.matrix.api.config.MatrixConfiguration +import io.element.android.appconfig.MatrixConfiguration import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 746f8cead8..4b7e4e4470 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -30,6 +30,8 @@ import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import kotlinx.coroutines.flow.StateFlow import java.io.Closeable import java.io.File @@ -192,5 +194,27 @@ interface MatrixRoom : Closeable { progressCallback: ProgressCallback? ): Result + /** + * Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters. + * @param widgetSettings The widget settings to use. + * @param clientId The client id to use. It should be unique per app install. + * @param languageTag The language tag to use. If null, the default language will be used. + * @param theme The theme to use. If null, the default theme will be used. + * @return The resulting url, or a failure. + */ + suspend fun generateWidgetWebViewUrl( + widgetSettings: MatrixWidgetSettings, + clientId: String, + languageTag: String? = null, + theme: String? = null, + ): Result + + /** + * Get a [MatrixWidgetDriver] for the provided [widgetSettings]. + * @param widgetSettings The widget settings to use. + * @return The resulting [MatrixWidgetDriver], or a failure. + */ + fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result + override fun close() = destroy() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt new file mode 100644 index 0000000000..f0a22d0128 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.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.libraries.matrix.api.widget + +import java.util.UUID + +interface CallWidgetSettingsProvider { + fun provide( + baseUrl: String, + widgetId: String = UUID.randomUUID().toString() + ): MatrixWidgetSettings +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt new file mode 100644 index 0000000000..675adc1ad4 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.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.libraries.matrix.api.widget + +import kotlinx.coroutines.flow.Flow + +interface MatrixWidgetDriver : AutoCloseable { + val id: String + val incomingMessages: Flow + + suspend fun run() + suspend fun send(message: String) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetSettings.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetSettings.kt new file mode 100644 index 0000000000..022827898f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetSettings.kt @@ -0,0 +1,29 @@ +/* + * 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.libraries.matrix.api.widget + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +class MatrixWidgetSettings( + val id: String, + val initAfterContentLoad: Boolean, + val rawUrl: String, +) : Parcelable { + companion object +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 4dcdb6d88c..c778fa3ac5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -40,6 +40,8 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl import io.element.android.libraries.matrix.impl.media.map @@ -48,6 +50,8 @@ import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.util.destroyAll +import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver +import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CancellationException @@ -65,6 +69,8 @@ import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle +import org.matrix.rustcomponents.sdk.WidgetPermissions +import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import timber.log.Timber @@ -478,6 +484,27 @@ class RustMatrixRoom( ) } + override suspend fun generateWidgetWebViewUrl( + widgetSettings: MatrixWidgetSettings, + clientId: String, + languageTag: String?, + theme: String?, + ) = runCatching { + widgetSettings.generateWidgetWebViewUrl(innerRoom, clientId, languageTag, theme) + } + + override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result = runCatching { + RustWidgetDriver( + widgetSettings = widgetSettings, + room = innerRoom, + widgetPermissionsProvider = object : WidgetPermissionsProvider { + override fun acquirePermissions(permissions: WidgetPermissions): WidgetPermissions { + return permissions + } + }, + ) + } + private suspend fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { return runCatching { MediaUploadHandlerImpl(files, handle()) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt new file mode 100644 index 0000000000..a7f208e69d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt @@ -0,0 +1,46 @@ +/* + * 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.libraries.matrix.impl.widget + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import org.matrix.rustcomponents.sdk.VirtualElementCallWidgetOptions +import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettingsProvider { + override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings { + val options = VirtualElementCallWidgetOptions( + elementCallUrl = baseUrl, + widgetId = widgetId, + parentUrl = null, + hideHeader = null, + preload = null, + fontScale = null, + appPrompt = false, + skipLobby = true, + confineToRoom = true, + fonts = null, + analyticsId = null + ) + val rustWidgetSettings = newVirtualElementCallWidget(options) + return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt new file mode 100644 index 0000000000..65e6c8bc84 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt @@ -0,0 +1,50 @@ +/* + * 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.libraries.matrix.impl.widget + +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import org.matrix.rustcomponents.sdk.ClientProperties +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.WidgetSettings +import org.matrix.rustcomponents.sdk.generateWebviewUrl + +fun MatrixWidgetSettings.toRustWidgetSettings() = WidgetSettings( + id = this.id, + initAfterContentLoad = this.initAfterContentLoad, + rawUrl = this.rawUrl, +) + +fun MatrixWidgetSettings.Companion.fromRustWidgetSettings(widgetSettings: WidgetSettings) = MatrixWidgetSettings( + id = widgetSettings.id, + initAfterContentLoad = widgetSettings.initAfterContentLoad, + rawUrl = widgetSettings.rawUrl, +) + +suspend fun MatrixWidgetSettings.generateWidgetWebViewUrl( + room: Room, + clientId: String, + languageTag: String? = null, + theme: String? = null +) = generateWebviewUrl( + widgetSettings = this.toRustWidgetSettings(), + room = room, + props = ClientProperties( + clientId = clientId, + languageTag = languageTag, + theme = theme, + ) +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt new file mode 100644 index 0000000000..e385a34e0d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.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.libraries.matrix.impl.widget + +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider +import org.matrix.rustcomponents.sdk.makeWidgetDriver +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.coroutineContext + +class RustWidgetDriver( + widgetSettings: MatrixWidgetSettings, + private val room: Room, + private val widgetPermissionsProvider: WidgetPermissionsProvider, +): MatrixWidgetDriver { + + override val incomingMessages = MutableSharedFlow() + + private val driverAndHandle = makeWidgetDriver(widgetSettings.toRustWidgetSettings()) + private var receiveMessageJob: Job? = null + + private var isRunning = AtomicBoolean(false) + + override val id: String = widgetSettings.id + + override suspend fun run() { + // Don't run the driver if it's already running + if (!isRunning.compareAndSet(false, true)) { + return + } + + val coroutineScope = CoroutineScope(coroutineContext) + coroutineScope.launch { + // This call will suspend the coroutine while the driver is running, so it needs to be launched separately + driverAndHandle.driver.run(room, widgetPermissionsProvider) + } + receiveMessageJob = coroutineScope.launch(Dispatchers.IO) { + try { + while (isActive) { + driverAndHandle.handle.recv()?.let { incomingMessages.emit(it) } + } + } finally { + driverAndHandle.handle.close() + } + } + } + + override suspend fun send(message: String) { + driverAndHandle.handle.send(message) + } + + override fun close() { + receiveMessageJob?.cancel() + driverAndHandle.driver.close() + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 67a36f0db7..2332a37fdc 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -208,8 +208,12 @@ class FakeMatrixClient( findDmResult = result } - fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom) { - getRoomResults[roomId] = result + fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom?) { + if (result == null) { + getRoomResults.remove(roomId) + } else { + getRoomResults[roomId] = result + } } fun givenSearchUsersResult(searchTerm: String, result: Result) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt new file mode 100644 index 0000000000..80cdcff7ec --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.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.libraries.matrix.test + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.SessionId + +class FakeMatrixClientProvider( + private val getClient: (SessionId) -> Result = { Result.success(FakeMatrixClient()) } +) : MatrixClientProvider { + override suspend fun getOrRestore(sessionId: SessionId): Result = getClient(sessionId) +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 7549522c0c..c08a742391 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -36,11 +36,14 @@ import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -92,6 +95,8 @@ class FakeMatrixRoom( private var sendPollResponseResult = Result.success(Unit) private var endPollResult = Result.success(Unit) private var progressCallbackValues = emptyList>() + private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io") + private var getWidgetDriverResult: Result = Result.success(FakeWidgetDriver()) val editMessageCalls = mutableListOf>() var sendMediaCount = 0 @@ -368,6 +373,15 @@ class FakeMatrixRoom( progressCallback: ProgressCallback? ): Result = fakeSendMedia(progressCallback) + override suspend fun generateWidgetWebViewUrl( + widgetSettings: MatrixWidgetSettings, + clientId: String, + languageTag: String?, + theme: String?, + ): Result = generateWidgetWebViewUrlResult + + override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result = getWidgetDriverResult + fun givenLeaveRoomError(throwable: Throwable?) { this.leaveRoomError = throwable } @@ -475,6 +489,14 @@ class FakeMatrixRoom( fun givenProgressCallbackValues(values: List>) { progressCallbackValues = values } + + fun givenGenerateWidgetWebViewUrlResult(result: Result) { + generateWidgetWebViewUrlResult = result + } + + fun givenGetWidgetDriverResult(result: Result) { + getWidgetDriverResult = result + } } data class SendLocationInvocation( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt new file mode 100644 index 0000000000..74cf94e4ad --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt @@ -0,0 +1,32 @@ +/* + * 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.libraries.matrix.test.widget + +import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings + +class FakeCallWidgetSettingsProvider( + private val provideFn: (String, String) -> MatrixWidgetSettings = { _, _ -> MatrixWidgetSettings("id", true, "url") } +) : CallWidgetSettingsProvider { + + val providedBaseUrls = mutableListOf() + + override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings { + providedBaseUrls += baseUrl + return provideFn(baseUrl, widgetId) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeWidgetDriver.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeWidgetDriver.kt new file mode 100644 index 0000000000..f7fa2b494a --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeWidgetDriver.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.libraries.matrix.test.widget + +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import kotlinx.coroutines.flow.MutableSharedFlow +import java.util.UUID + +class FakeWidgetDriver( + override val id: String = UUID.randomUUID().toString(), +) : MatrixWidgetDriver { + + private val _sentMessages = mutableListOf() + val sentMessages: List = _sentMessages + + var runCalledCount = 0 + private set + var closeCalledCount = 0 + private set + + override val incomingMessages = MutableSharedFlow(extraBufferCapacity = 1) + + override suspend fun run() { + runCalledCount++ + } + + override suspend fun send(message: String) { + _sentMessages.add(message) + } + + override fun close() { + closeCalledCount++ + } + + fun givenIncomingMessage(message: String) { + incomingMessages.tryEmit(message) + } +} diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt index 8ad2c098f6..d62fb7e6cf 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt @@ -25,5 +25,8 @@ interface PreferencesStore { suspend fun setDeveloperModeEnabled(enabled: Boolean) fun isDeveloperModeEnabledFlow(): Flow + suspend fun setCustomElementCallBaseUrl(string: String?) + fun getCustomElementCallBaseUrlFlow(): Flow + suspend fun reset() } diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt index 337301f23e..66a46d1ca3 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt @@ -21,6 +21,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.preferences.api.store.PreferencesStore @@ -37,6 +38,7 @@ private val Context.dataStore: DataStore by preferencesDataStore(na private val richTextEditorKey = booleanPreferencesKey("richTextEditor") private val developerModeKey = booleanPreferencesKey("developerMode") +private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl") @ContributesBinding(AppScope::class) class DefaultPreferencesStore @Inject constructor( @@ -71,6 +73,22 @@ class DefaultPreferencesStore @Inject constructor( } } + override suspend fun setCustomElementCallBaseUrl(string: String?) { + store.edit { prefs -> + if (string != null) { + prefs[customElementCallBaseUrlKey] = string + } else { + prefs.remove(customElementCallBaseUrlKey) + } + } + } + + override fun getCustomElementCallBaseUrlFlow(): Flow { + return store.data.map { prefs -> + prefs[customElementCallBaseUrlKey] + } + } + override suspend fun reset() { store.edit { it.clear() } } diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt index a2a9fdaa3f..6dea8910ed 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt @@ -23,9 +23,11 @@ import kotlinx.coroutines.flow.MutableStateFlow class InMemoryPreferencesStore( isRichTextEditorEnabled: Boolean = false, isDeveloperModeEnabled: Boolean = false, + customElementCallBaseUrl: String? = null, ) : PreferencesStore { private var _isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled) private var _isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) + private var _customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl) override suspend fun setRichTextEditorEnabled(enabled: Boolean) { _isRichTextEditorEnabled.value = enabled @@ -43,6 +45,14 @@ class InMemoryPreferencesStore( return _isDeveloperModeEnabled } + override suspend fun setCustomElementCallBaseUrl(string: String?) { + _customElementCallBaseUrl.tryEmit(string) + } + + override fun getCustomElementCallBaseUrlFlow(): Flow { + return _customElementCallBaseUrl + } + override suspend fun reset() { // No op } diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index f28095763c..7ada336f28 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -131,7 +131,6 @@ "např. název vašeho projektu" "Hledat někoho" "Výsledky hledání" - "Zabezpečená záloha" "Zabezpečení" "Odesílání…" "Server není podporován" @@ -208,9 +207,9 @@ "Další nastavení" "Halsové a video hovory" "Neshoda konfigurace" - "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. + "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. -Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. +Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. Pokud budete pokračovat, některá nastavení se mohou změnit." "Přímé zprávy" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 7401685482..ada42316b1 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -126,7 +126,6 @@ "например, название вашего проекта" "Поиск человека" "Результаты поиска" - "Безопасное резервное копирование" "Безопасность" "Отправка…" "Сервер не поддерживается" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 536db8c501..c6cf324b75 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -131,7 +131,6 @@ "napr. názov vášho projektu" "Vyhľadať niekoho" "Výsledky hľadania" - "Bezpečné zálohovanie" "Bezpečnosť" "Odosiela sa…" "Server nie je podporovaný" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index b9a6460bec..1ff0ac1bc0 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -69,6 +69,8 @@ "Share" "Share link" "Sign in again" + "Sign out" + "Sign out anyway" "Skip" "Start" "Start chat" @@ -84,6 +86,7 @@ "Analytics" "Audio" "Bubbles" + "Chat backup" "Copyright" "Creating room…" "Left room" @@ -122,6 +125,7 @@ "Privacy policy" "Reaction" "Reactions" + "Recovery key" "Refreshing…" "Replying to %1$s" "Report a bug" @@ -131,7 +135,6 @@ "e.g. your project name" "Search for someone" "Search results" - "Secure backup" "Security" "Sending…" "Server not supported" @@ -200,6 +203,22 @@ "This is the beginning of this conversation." "New" "Share analytics data" + "Turn off backup" + "Turn on backup" + "Backup ensures that you don\'t lose your message history." + "Backup" + "Change recovery key" + "Confirm recovery key" + "Your chat backup is currently out of sync." + "Set up recovery" + "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere." + "Turn off" + "You will lose your encrypted messages if you are signed out of all devices." + "Are you sure you want to turn off backup?" + "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:" + "Not have encrypted message history on new devices" + "Lose access to your encrypted messages if you are signed out of %1$@ everywhere" + "Are you sure you want to turn off backup?" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." @@ -228,6 +247,27 @@ If you proceed, some of your settings may change." "system settings" "System notifications turned off" "Notifications" + "Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work." + "Generate a new recovery key" + "Make sure you can store your recovery key somewhere safe" + "Recovery key changed" + "Change recovery key?" + "Enter your recovery key to confirm access to your chat backup." + "Enter the 48 character code." + "Enter…" + "Recovery key confirmed" + "Confirm your recovery key" + "Save recovery key" + "Write down your recovery key somewhere safe or save it in a password manager." + "Tap to copy recovery key" + "Save your recovery key" + "You will not be able to access your new recovery key after this step." + "Have you saved your recovery key?" + "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’." + "Generate your recovery key" + "Make sure you can store your recovery key somewhere safe" + "Recovery setup successful" + "Set up recovery" "Check if you want to hide all current and future messages from this user" "Share location" "Share my location" diff --git a/settings.gradle.kts b/settings.gradle.kts index 105befcd04..9ac2c96dde 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,6 +52,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "ElementX" include(":app") include(":appnav") +include(":appconfig") include(":tests:konsist") include(":tests:uitests") include(":tests:testutils") diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f44d866ff8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a28b7969455f17784f060291ce58b3720324baa67e9d93c2aa59f6d979268678 +size 14429 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..232a3e752a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8a811677a50035a361f65ece4a1281346423226db6a1b8b3b8611f6b2f1d23d +size 13099 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 4e99b44510..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0264d691ec2946cda4d5860f02079dd9f3e69ddd30a2e5c2f9c701253fd659c -size 10499 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 5301f939c3..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20ec46c4c66a68d93c45a17eafd945536c9c137b18e66a82e83eced674708d98 -size 9732 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png index 72056b5ed9..6f1f867a7c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88bdef3999877e5017bfe0e0ead1514e4e6a58abcde0b0167d4b0ad9d4abd1e0 -size 54020 +oid sha256:5f219bd9b9363f237e15bb73655dd53b2ec143e18c8544c11efbfa90390e091c +size 54312 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png index 90b4f4652f..062dfb77f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35f420b550029d7f8b22d73ea0349d2794cc2e5c5f3080799f496774fad7d2ff -size 55440 +oid sha256:6065f5330e1b3638719c6743db098bf6576fcffc6ccf8215f7f600a0b981144b +size 55731 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png index c99c22634e..1de7fbc725 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9518f3e4809856f3787bcd076f1a4f33067ea911e66cd6692790451309e6e192 -size 55769 +oid sha256:f4bdafdfa50665f05ba5cd8749252e7e5d65bea8a8c6bfc1c46eb1acd1570b52 +size 56086 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png index 1ae57f1414..d348a6f13f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f62a8a4eded0b742e911970837fe1003228834dd07a216a9bdc4acef37aa468 -size 55800 +oid sha256:2bfcc4dcaa8980cfc9c724e64a93116ac1fb81a1bf4421532cb196d4e07db7c5 +size 56024 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png index 31edf89ccf..224cef4b67 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1314aaf5394d03b5d08eccdb29783ce8d949f44d3eea28ec8ce434b830515304 -size 51662 +oid sha256:c86a6da3d45b767a41f65e0cf4e8cacefb33e0409386a6233140891deb2258f6 +size 51965 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png index 617d8da89b..82636b3e21 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecdc26ae1b8943734a8ec1020d7ca9ad4a8e570f5295eb568f80ed314585a9a9 -size 51981 +oid sha256:5a1fc20759ff45eeef547621853b9684a663fe9dcdf78b316454dcae26d078f9 +size 52286 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png index 58b944edce..094ab2536a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32ae9c61f8a01bd54b9fb51af5f0dff222f4df0be1003a9c6ad680d20877448b -size 52275 +oid sha256:7b9bc4654b6911d7f50b4b0242b650a529cb22c13a58caf5eae50366a2d27b37 +size 52532 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png index e28209550e..32de8d07f9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1785f0fe49a5afd9b152f6ddb51acad7ba1700b2711cf302ef3a490d948fcd96 -size 53618 +oid sha256:0e051668beb9030ff184d6e75831a00202854f6448880f399a7ee2d5be187740 +size 53880 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png index 5113ea50e1..009ddeadaa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8907587eb26b29d273fcce14fe4f17acd3ab8ae2fffa83d2c5d5c2c0d8c29bc6 -size 54270 +oid sha256:c6c6603452db218811f3f5a2baa934ca640755b969e81ca8cbf6b3dcf663aee9 +size 54551 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png index 6162f39468..d2f2f56d9b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:455b414e5da5d5174a8d87ef37219c9754e3558b39f025d7aab226b041c51096 -size 51305 +oid sha256:e953bc91e9959f1635d12416f6afde79427aeac4edef44fb38f1512672e544bd +size 51552 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png index 3b7c0855e3..9437874ffd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0b83b1d37b34cdf0769e83f625d66479638ce402d5c8e76ef40548414d34400 -size 49862 +oid sha256:0157713ae934c0771a5d3d4d064ce6dc9be11b1d8011f2360740d7f4a20f6f04 +size 50162 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png index 500b83a53e..af4dae6214 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b09d4a60c3d9944cd6d50a8f0a06d5b3522eba7884f3b5e1e6889f93e8dd1794 -size 50026 +oid sha256:24f1d30bb62ed620671e87062a338fd53f409bce6e08fb7b116249decf74e409 +size 50295 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2c48d3e55d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4e1af15c571d1f087005849b627d79387f8f5557bbc4233768bb3c2d940d628 +size 48510 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..519d9d4d10 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa25ebf20fe62af56a548c3e962ae2e76e6e8e1b7e685d021306b733613e49eb +size 45462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_TextfieldListitem-textfieldvalue_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_TextfieldListitem-textfieldvalue_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c7b2473e20 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_TextfieldListitem-textfieldvalue_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17fa588278f61269982bda072f4a67f9cc6f39f6f1ebcf4da59c7c3006808e14 +size 11262 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 7e07d269c0..0525eb834d 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -145,6 +145,7 @@ "name": ":features:preferences:impl", "includeRegex": [ "screen_advanced_settings_.*", + "screen\\.advanced_settings\\..*", "screen_edit_profile_.*" ] }, From 753a80fc29e4fc76c67f9e412e549298c151ba30 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 21:58:12 +0200 Subject: [PATCH 16/70] Pin auth : simple first iteration on ui --- .../impl/auth/PinAuthenticationView.kt | 115 +++++++++-- .../lockscreen/impl/auth/numpad/PinKeypad.kt | 182 ++++++++++++++++++ .../impl/auth/numpad/PinKeypadModel.kt | 26 +++ 3 files changed, 304 insertions(+), 19 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt index 2b62e46800..740d85adec 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt @@ -16,22 +16,40 @@ package io.element.android.features.lockscreen.impl.auth -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign 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.features.lockscreen.impl.auth.numpad.PinKeypad 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.Icon import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme @Composable fun PinAuthenticationView( @@ -40,27 +58,23 @@ fun PinAuthenticationView( ) { Surface(modifier) { HeaderFooterPage( - modifier = Modifier - .systemBarsPadding() - .fillMaxSize(), header = { PinAuthenticationHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, - footer = { PinAuthenticationFooter(state) }, + content = { + Box( + modifier = Modifier + .padding(top = 40.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + PinKeypad( + onClick = {} + ) + } + } ) } } -@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( @@ -72,6 +86,69 @@ private fun PinAuthenticationFooter(state: PinAuthenticationState) { ) } +@Composable +private fun PinDotsRow( + modifier: Modifier = Modifier, +) { + Row(modifier, horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + PinDot(isFilled = true) + PinDot(isFilled = true) + PinDot(isFilled = false) + PinDot(isFilled = false) + } +} + +@Composable +private fun PinDot( + isFilled: Boolean, + modifier: Modifier = Modifier, +) { + val backgroundColor = if (isFilled) { + ElementTheme.colors.iconPrimary + } else { + ElementTheme.colors.bgSubtlePrimary + } + Box( + modifier = modifier + .size(14.dp) + .background(backgroundColor, CircleShape) + ) +} + +@Composable +private fun PinAuthenticationHeader( + modifier: Modifier = Modifier, +) { + Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + modifier = Modifier + .size(32.dp), + tint = ElementTheme.colors.iconPrimary, + imageVector = Icons.Filled.Lock, + contentDescription = "", + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Enter your PIN", + modifier = Modifier + .fillMaxWidth(), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontHeadingMdBold, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = "You have 3 attempts to unlock", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.secondary, + ) + Spacer(Modifier.height(24.dp)) + PinDotsRow() + } +} + @Composable @PreviewsDayNight internal fun PinAuthenticationViewPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt new file mode 100644 index 0000000000..43abc24002 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt @@ -0,0 +1,182 @@ +/* + * 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.lockscreen.impl.auth.numpad + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Backspace +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.toSp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun PinKeypad( + onClick: (PinKeypadModel) -> Unit, + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + verticalAlignment: Alignment.Vertical = Alignment.Top, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, +) { + Column( + modifier = modifier, + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment, + ) { + PinKeypadRow( + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = listOf(PinKeypadModel.Number("1"), PinKeypadModel.Number("2"), PinKeypadModel.Number("3")), + onClick = onClick, + ) + PinKeypadRow( + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = listOf(PinKeypadModel.Number("4"), PinKeypadModel.Number("5"), PinKeypadModel.Number("6")), + onClick = onClick, + ) + PinKeypadRow( + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = listOf(PinKeypadModel.Number("7"), PinKeypadModel.Number("8"), PinKeypadModel.Number("9")), + onClick = onClick, + ) + PinKeypadRow( + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number("0"), PinKeypadModel.Back), + onClick = onClick, + ) + } +} + +@Composable +private fun PinKeypadRow( + models: List, + onClick: (PinKeypadModel) -> Unit, + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.Top, +) { + Row( + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment, + modifier = modifier, + ) { + val commonModifier = Modifier.size(80.dp) + for (model in models) { + when (model) { + is PinKeypadModel.Empty -> { + Spacer(modifier = commonModifier) + } + is PinKeypadModel.Back -> { + PinKeypadBackButton( + modifier = commonModifier, + onClick = { onClick(model) }, + ) + } + is PinKeypadModel.Number -> { + PinKeyBadDigitButton( + size = 80.dp, + modifier = commonModifier, + digit = model.number, + onClick = { onClick(model) }, + ) + } + } + } + } +} + +@Composable +private fun PinKeyBadDigitButton( + digit: String, + size: Dp, + onClick: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = ElementTheme.colors.bgSubtlePrimary, + contentColor = Color.Transparent, + ), + shape = CircleShape, + contentPadding = PaddingValues(0.dp), + modifier = modifier + .clip(CircleShape), + onClick = { onClick(digit) } + ) { + val fontSize = 80.dp.toSp() / 2 + val originalFont = ElementTheme.typography.fontHeadingXlBold + val ratio = fontSize.value / originalFont.fontSize.value + val lineHeight = originalFont.lineHeight * ratio + Text( + text = digit, + color = ElementTheme.colors.textPrimary, + style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp), + ) + } +} + +@Composable +private fun PinKeypadBackButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .clip(CircleShape) + .background(color = Color.Transparent, shape = CircleShape), + onClick = onClick, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Backspace, + contentDescription = null, + ) + } +} + +@Composable +@PreviewsDayNight +fun PinKeypad() { + ElementPreview { + PinKeypad(onClick = {}) + } +} + + diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt new file mode 100644 index 0000000000..108486e400 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.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.lockscreen.impl.auth.numpad + +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface PinKeypadModel { + data object Empty : PinKeypadModel + data object Back : PinKeypadModel + data class Number(val number: String) : PinKeypadModel +} From 6dfba0f0c4d47ac531df97464e485fd50bf9cfeb Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 22:23:19 +0200 Subject: [PATCH 17/70] Create pin : fix some spacing --- .../lockscreen/impl/create/CreatePinView.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 4e01de8c2f..cddadbbae9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -25,12 +25,16 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api @@ -49,7 +53,6 @@ import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview @@ -77,13 +80,18 @@ fun CreatePinView( ) }, content = { padding -> - HeaderFooterPage( + val scrollState = rememberScrollState() + Column( modifier = Modifier + .imePadding() .padding(padding) - .consumeWindowInsets(padding), - header = { CreatePinHeader(state.isConfirmationStep, state.pinSize) }, - content = { CreatePinContent(state) } - ) + .consumeWindowInsets(padding) + .verticalScroll(state = scrollState) + .padding(vertical = 16.dp, horizontal = 20.dp), + ) { + CreatePinHeader(state.isConfirmationStep, state.pinSize) + CreatePinContent(state) + } } ) } @@ -99,7 +107,6 @@ private fun CreatePinHeader( horizontalAlignment = Alignment.CenterHorizontally, ) { IconTitleSubtitleMolecule( - modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = if (isValidationStep) { stringResource(id = R.string.screen_app_lock_setup_confirm_pin) } else { @@ -110,7 +117,7 @@ private fun CreatePinHeader( ) Text( text = stringResource(id = R.string.screen_app_lock_setup_pin_context_warning), - modifier = Modifier.padding(8.dp), + modifier = Modifier.padding(top = 24.dp), textAlign = TextAlign.Center, style = ElementTheme.typography.fontBodyMdRegular, color = MaterialTheme.colorScheme.secondary, From e38ce7d9e0733971685ff137c7feba858e38af57 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 19 Oct 2023 20:39:07 +0000 Subject: [PATCH 18/70] Update screenshots --- ...create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png index f110f617f8..6d36e3b71b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e2d58ea747cf87fd6e4f70b3ebb1f1e638a4ec3f7ac8cb712566a889167c3e1 -size 35045 +oid sha256:8ddeca63feb6f81e2db0da909657f1f90cfb13facb8ce854cc79e4eb33e329ee +size 34635 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png index e5a3fe28af..3981b8e841 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4be560206e4fc9101d8a793ad5f5f9cc2057fb47e60f37f91ce6dae8e2b995e2 -size 35147 +oid sha256:28dbde9da91754c0377667bc852b8df3f17d2c6d91f0be4b04a5762cda7995e6 +size 34703 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png index 87cefa723f..a054b68cf1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da83f8b634c8217636b08792498704d9ffc1623c8d4aedfb15265d2f5e085e86 -size 32805 +oid sha256:e06d34c115e1e3ecadf73a78701f8e565f0bc9cd38d1eb3f007c0ec4486cbf3e +size 32319 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png index d3d3ba4038..fede94e2a3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:598214e07c2545ac4dd061f6237a737e91e9c47eeea01a63ab064b3398e39b68 -size 24046 +oid sha256:9368e0cae34cde6e8559702c8de4cb42d684cea5f13cbd62d9ab0b7de918e684 +size 28624 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png index 7476004b1f..9f40f229df 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ec4a6f05bfb15e018c19205cb919f6619440e25392ed0a74fb2896ddb1decb6 -size 28266 +oid sha256:3a948fedb7df6d6cd25793d9aab2a96487bd1a7bd31ba8bacb44026d0c97582c +size 36925 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png index 41a349426e..e1cff5f4ef 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7c84953e8c1c3b2384078be4ca8cd5c763c032bfa22bb2ba034dcbee8ef9c72 -size 33594 +oid sha256:e17eaf4585825f04fb121296d4b1370d652fc64d28149c285ac28849267ea9f0 +size 33051 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png index 8f6c3fdbba..b67c6d82ed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0917206ffe964311097f05de3fb838e0cc23f6fdbb2cd106d3015a751b378cfc -size 33864 +oid sha256:9b0b7a756d5195a5397ef4eeba5da8b1cbdc8a4a182e935d03ef9d258d921b5f +size 33278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png index 64bc9a11cd..f6c53f382a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32b2c6514d9a23da09c932e198f29ec112b5afa872a0fa42cd5de85689660891 -size 31658 +oid sha256:d42ed3cb545fc96e1840db73c8efdc6782a545572539855ccceceda50663f284 +size 31445 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png index 6d10aeb87d..2047696c67 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bdedf444797479efa73d6ca7d3d6545f4bd01faa80d50980b0ad6ea427b09ee -size 20828 +oid sha256:75785d23eafb006cbfbbab3e6824364a419f56d0a7d2d2a734b6f0947ca7843b +size 25358 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png index 7c2736059e..21e905b2df 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51774321b68ee4d1f5a605edf1ce630e0f7651191e93cab6a885fc90f1639061 -size 24493 +oid sha256:624d62ed7dd52617d8c02bcf5be4ded2707500e930d56f4362c834226e6108bd +size 32514 From 177a5bc78ad0f557a1e9733806659e98dd338db6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 08:56:10 +0200 Subject: [PATCH 19/70] Update dependency org.jsoup:jsoup to v1.16.2 (#1613) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b2e91fce99..9c7e956569 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,7 +38,7 @@ coil = "2.4.0" datetime = "0.4.1" serialization_json = "1.6.0" showkase = "1.0.0-beta18" -jsoup = "1.16.1" +jsoup = "1.16.2" appyx = "1.4.0" dependencycheck = "8.4.0" dependencyanalysis = "1.25.0" From 7386da257c8745ee7c31a3e97bfe5cfe61857474 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 10:41:39 +0200 Subject: [PATCH 20/70] Remove AppNameProvider, we have buildMeta now. --- .../libraries/push/impl/PushersManager.kt | 6 +-- .../toolbox/api/appname/AppNameProvider.kt | 21 --------- .../impl/appname/DefaultAppNameProvider.kt | 47 ------------------- 3 files changed, 3 insertions(+), 71 deletions(-) delete mode 100644 services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt delete mode 100644 services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index 7f4ee63004..b49f8f4299 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId @@ -28,7 +29,6 @@ import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyReque import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret -import io.element.android.services.toolbox.api.appname.AppNameProvider import timber.log.Timber import javax.inject.Inject @@ -39,7 +39,7 @@ private val loggerTag = LoggerTag("PushersManager", LoggerTag.PushLoggerTag) @ContributesBinding(AppScope::class) class PushersManager @Inject constructor( // private val localeProvider: LocaleProvider, - private val appNameProvider: AppNameProvider, + private val buildMeta: BuildMeta, // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, private val pushGatewayNotifyRequest: PushGatewayNotifyRequest, private val pushClientSecret: PushClientSecret, @@ -88,7 +88,7 @@ class PushersManager @Inject constructor( appId = PushConfig.pusher_app_id, profileTag = DEFAULT_PUSHER_FILE_TAG + "_" /* TODO + abs(activeSessionHolder.getActiveSession().myUserId.hashCode())*/, lang = "en", // TODO localeProvider.current().language, - appDisplayName = appNameProvider.getAppName(), + appDisplayName = buildMeta.applicationName, deviceDisplayName = "MyDevice", // TODO getDeviceInfoUseCase.execute().displayName().orEmpty(), url = gateway, defaultPayload = createDefaultPayload(pushClientSecret.getSecretForUser(userId)) diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt deleted file mode 100644 index 414c9b632e..0000000000 --- a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.services.toolbox.api.appname - -interface AppNameProvider { - fun getAppName(): String -} diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt deleted file mode 100644 index 7a5cbd46f0..0000000000 --- a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.services.toolbox.impl.appname - -import android.content.Context -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.androidutils.system.getApplicationLabel -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext -import io.element.android.services.toolbox.api.appname.AppNameProvider -import timber.log.Timber -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultAppNameProvider @Inject constructor(@ApplicationContext private val context: Context) : - AppNameProvider { - - override fun getAppName(): String { - return try { - val appPackageName = context.packageName - var appName = context.getApplicationLabel(appPackageName) - - // Use appPackageName instead of appName if appName contains any non-ASCII character - if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { - appName = appPackageName - } - appName - } catch (e: Exception) { - Timber.e(e, "## AppNameProvider() : failed") - "ElementAndroid" - } - } -} From 623ac05644d021c22b910aa355080135f1873d99 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 10:52:13 +0200 Subject: [PATCH 21/70] Fix small issue in the script. --- tools/sdk/build_rust_sdk.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sdk/build_rust_sdk.sh b/tools/sdk/build_rust_sdk.sh index 6853642218..3d7dc17059 100755 --- a/tools/sdk/build_rust_sdk.sh +++ b/tools/sdk/build_rust_sdk.sh @@ -64,7 +64,7 @@ printf "\nBuilding the SDK for aarch64-linux-android...\n\n" cd ../element-x-android mv ./libraries/rustsdk/sdk-android-debug.aar ./libraries/rustsdk/matrix-rust-sdk.aar mkdir -p ./libraries/rustsdk/sdks -cp ./libraries/rustsdk/matrix-rust-sdk.aar ./libraries/rustsdk/matrix-rust-sdk-${date}.aar +cp ./libraries/rustsdk/matrix-rust-sdk.aar ./libraries/rustsdk/sdks/matrix-rust-sdk-${date}.aar if [ ${buildApp} == "yes" ]; then From 78d264c6fc51d3ab4331f9cc7d9977ec1165821f Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 10:54:59 +0200 Subject: [PATCH 22/70] Create PIN : hopefully fix remaining issues --- .../impl/create/CreatePinPresenter.kt | 3 +++ .../lockscreen/impl/create/CreatePinState.kt | 1 + .../impl/create/CreatePinStateProvider.kt | 1 + .../lockscreen/impl/create/CreatePinView.kt | 18 ++++-------------- .../impl/src/main/res/values/localazy.xml | 7 ++++--- .../impl/create/CreatePinPresenterTest.kt | 3 ++- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index e72e636ed4..957594c0f7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -25,12 +25,14 @@ import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.create.validation.PinValidator import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta import javax.inject.Inject private const val PIN_SIZE = 4 class CreatePinPresenter @Inject constructor( private val pinValidator: PinValidator, + private val buildMeta: BuildMeta, ) : Presenter { @Composable @@ -94,6 +96,7 @@ class CreatePinPresenter @Inject constructor( confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, createPinFailure = createPinFailure, + appName = buildMeta.applicationName, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 5bb632f04e..020076a2ab 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -24,6 +24,7 @@ data class CreatePinState( val confirmPinEntry: PinEntry, val isConfirmationStep: Boolean, val createPinFailure: CreatePinFailure?, + val appName: String, val eventSink: (CreatePinEvents) -> Unit ) { val pinSize = choosePinEntry.size diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index 543360f91e..c9dcce018d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -55,6 +55,7 @@ fun aCreatePinState( confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, createPinFailure = creationFailure, + appName = "Element", eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index cddadbbae9..063d65f41f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding @@ -38,14 +37,12 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.R @@ -89,7 +86,7 @@ fun CreatePinView( .verticalScroll(state = scrollState) .padding(vertical = 16.dp, horizontal = 20.dp), ) { - CreatePinHeader(state.isConfirmationStep, state.pinSize) + CreatePinHeader(state.isConfirmationStep, state.appName) CreatePinContent(state) } } @@ -99,7 +96,7 @@ fun CreatePinView( @Composable private fun CreatePinHeader( isValidationStep: Boolean, - pinSize: Int, + appName: String, modifier: Modifier = Modifier, ) { Column( @@ -110,18 +107,11 @@ private fun CreatePinHeader( title = if (isValidationStep) { stringResource(id = R.string.screen_app_lock_setup_confirm_pin) } else { - stringResource(id = R.string.screen_app_lock_setup_choose_pin, pinSize) + stringResource(id = R.string.screen_app_lock_setup_choose_pin) }, - subTitle = stringResource(id = R.string.screen_app_lock_setup_pin_context), + subTitle = stringResource(id = R.string.screen_app_lock_setup_pin_context, appName), iconImageVector = Icons.Filled.Lock, ) - Text( - text = stringResource(id = R.string.screen_app_lock_setup_pin_context_warning), - modifier = Modifier.padding(top = 24.dp), - textAlign = TextAlign.Center, - style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, - ) } } diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml index fb5c2be73c..6b12eac427 100644 --- a/features/lockscreen/impl/src/main/res/values/localazy.xml +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -10,12 +10,13 @@ "Remove PIN" "Are you sure you want to remove PIN?" "Remove PIN?" - "Choose %1$d digit PIN" + "Choose PIN" "Confirm PIN" "You cannot choose this as your PIN code for security reasons" "Choose a different PIN" - "Lock Element to add extra security to your chats." - "Choose something memorable. If you forget this PIN, you will be logged out of the app." + "Lock %1$s to add extra security to your chats. + +Choose something memorable. If you forget this PIN, you will be logged out of the app." "Please enter the same PIN twice" "PINs don\'t match" "You’ll need to re-login and create a new PIN to proceed" diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt index c1af14f519..78536bb693 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest import org.junit.Test @@ -108,6 +109,6 @@ class CreatePinPresenterTest { } private fun createCreatePinPresenter(): CreatePinPresenter { - return CreatePinPresenter(PinValidator()) + return CreatePinPresenter(PinValidator(), aBuildMeta()) } } From 1b9cc7ded53d17ad8a384842057bb37b161cccb6 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 20 Oct 2023 09:12:23 +0000 Subject: [PATCH 23/70] Update screenshots --- ...create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png index 6d36e3b71b..c3d918b1c2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ddeca63feb6f81e2db0da909657f1f90cfb13facb8ce854cc79e4eb33e329ee -size 34635 +oid sha256:c9f1f9f900cea024cdf777867c33c1d1bbe4744a5b91cd994a03aa6cbf4f8815 +size 33186 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png index 3981b8e841..0c72d54e99 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28dbde9da91754c0377667bc852b8df3f17d2c6d91f0be4b04a5762cda7995e6 -size 34703 +oid sha256:d25ab8d3f53a5139b265a1ddef43ef3755b539a695aa8f43615e9197bd93471d +size 33231 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png index a054b68cf1..61c45c675c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e06d34c115e1e3ecadf73a78701f8e565f0bc9cd38d1eb3f007c0ec4486cbf3e -size 32319 +oid sha256:13f2b0ac39e3e5448916beea6f7a7183c8a205811343d3ff5e8a21a4d2d3f641 +size 32447 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png index fede94e2a3..f96a3743f3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9368e0cae34cde6e8559702c8de4cb42d684cea5f13cbd62d9ab0b7de918e684 -size 28624 +oid sha256:f031423cd98572a729f54e0bc4cdb50778aadd732aa93cf48d063075e9da00be +size 28636 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png index 9f40f229df..8a290a2303 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a948fedb7df6d6cd25793d9aab2a96487bd1a7bd31ba8bacb44026d0c97582c -size 36925 +oid sha256:2818a043ea06af263b1442c6e90727509da2ce2da036d68dce89de31fa8b013f +size 35614 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png index e1cff5f4ef..c1fb8f128f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e17eaf4585825f04fb121296d4b1370d652fc64d28149c285ac28849267ea9f0 -size 33051 +oid sha256:75145d74e0bebbad427626711d5bc86911bd89350e73c968c8ca9448e939cdbb +size 31954 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png index b67c6d82ed..b22bfb46dd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b0b7a756d5195a5397ef4eeba5da8b1cbdc8a4a182e935d03ef9d258d921b5f -size 33278 +oid sha256:2d5037348c8b27714e54ebec53aac8a546f14b11e3195434f681aad95e416847 +size 32181 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png index f6c53f382a..4b0a1988c5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d42ed3cb545fc96e1840db73c8efdc6782a545572539855ccceceda50663f284 -size 31445 +oid sha256:d0a34e1c3c047806e4304791664c25cd8d7c9e7850c24b2bb17119e0d83fd5d4 +size 31492 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png index 2047696c67..d0c1ab3c32 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75785d23eafb006cbfbbab3e6824364a419f56d0a7d2d2a734b6f0947ca7843b -size 25358 +oid sha256:d8c09be0dbf07d8593fd2f93881febdf3494bd439cb6f62adb78beee34b6c7e7 +size 25357 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png index 21e905b2df..05e4d69148 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:624d62ed7dd52617d8c02bcf5be4ded2707500e930d56f4362c834226e6108bd -size 32514 +oid sha256:68a54676b1f82425df6db8cb56b161950584ce9d433fc59630a16ac6288fc7d8 +size 31204 From dd623ceb638bb1ace30257ef0ec6e9491b371747 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 12:05:11 +0200 Subject: [PATCH 24/70] Maestro No need to close the keyboard after fix from #1593 --- .maestro/tests/roomList/searchRoomList.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml index 6c31acd4db..b4f44c8b27 100644 --- a/.maestro/tests/roomList/searchRoomList.yaml +++ b/.maestro/tests/roomList/searchRoomList.yaml @@ -8,8 +8,6 @@ appId: ${APP_ID} # Back from timeline - back - assertVisible: "MyR" -# Close keyboard -- hideKeyboard # Back from search - back - runFlow: ../assertions/assertHomeDisplayed.yaml From 72eb1dca0a3866bffe8ce466e9d90c2a09c1cbc6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 12:05:11 +0200 Subject: [PATCH 25/70] Maestro No need to close the keyboard after fix from #1593 --- .maestro/tests/roomList/searchRoomList.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml index 6c31acd4db..b4f44c8b27 100644 --- a/.maestro/tests/roomList/searchRoomList.yaml +++ b/.maestro/tests/roomList/searchRoomList.yaml @@ -8,8 +8,6 @@ appId: ${APP_ID} # Back from timeline - back - assertVisible: "MyR" -# Close keyboard -- hideKeyboard # Back from search - back - runFlow: ../assertions/assertHomeDisplayed.yaml From ee6074453078a093f36bc7c98125a16dca420cba Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 12:09:19 +0200 Subject: [PATCH 26/70] Revert "Maestro No need to close the keyboard after fix from #1593" This reverts commit dd623ceb638bb1ace30257ef0ec6e9491b371747. --- .maestro/tests/roomList/searchRoomList.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml index b4f44c8b27..6c31acd4db 100644 --- a/.maestro/tests/roomList/searchRoomList.yaml +++ b/.maestro/tests/roomList/searchRoomList.yaml @@ -8,6 +8,8 @@ appId: ${APP_ID} # Back from timeline - back - assertVisible: "MyR" +# Close keyboard +- hideKeyboard # Back from search - back - runFlow: ../assertions/assertHomeDisplayed.yaml From 7e8f78c05d4ea107d0d753f1420ab4d3b3448e91 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 20 Oct 2023 14:25:20 +0200 Subject: [PATCH 27/70] Document the nuances in `UserId` and `SessionId` types. (#1616) --- .../element/android/libraries/matrix/api/core/SessionId.kt | 3 +++ .../io/element/android/libraries/matrix/api/core/UserId.kt | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt index 6009aa5c03..7bb6e27a5e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt @@ -16,4 +16,7 @@ package io.element.android.libraries.matrix.api.core +/** + * The [UserId] of the currently logged in user. + */ typealias SessionId = UserId diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index e153834501..e72af8596a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -19,6 +19,11 @@ package io.element.android.libraries.matrix.api.core import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable +/** + * A [String] holding a valid Matrix user ID. + * + * https://spec.matrix.org/v1.8/appendices/#user-identifiers + */ @JvmInline value class UserId(val value: String) : Serializable { From fbced52fee3a76da1a06447a1f1255709bc98ae2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 15:15:19 +0200 Subject: [PATCH 28/70] Lockscreen: renaming --- build.gradle.kts | 3 +- .../lockscreen/impl/LockScreenFlowNode.kt | 18 +++---- .../impl/pin/DefaultPinCodeManager.kt | 2 +- .../lockscreen/impl/pin/PinCodeManager.kt | 2 +- .../SetupPinEvents.kt} | 8 ++-- .../SetupPinNode.kt} | 8 ++-- .../SetupPinPresenter.kt} | 40 ++++++++-------- .../SetupPinState.kt} | 12 ++--- .../SetupPinStateProvider.kt} | 32 ++++++------- .../SetupPinView.kt} | 48 +++++++++---------- .../impl/{create => setup}/model/PinDigit.kt | 2 +- .../impl/{create => setup}/model/PinEntry.kt | 2 +- .../validation/CreatePinFailure.kt | 8 ++-- .../validation/PinValidator.kt | 8 ++-- .../PinUnlockEvents.kt} | 6 +-- .../PinUnlockNode.kt} | 8 ++-- .../PinUnlockPresenter.kt} | 14 +++--- .../PinUnlockState.kt} | 6 +-- .../PinUnlockStateProvider.kt} | 10 ++-- .../PinUnlockView.kt} | 25 ++++------ .../impl/{auth => unlock}/numpad/PinKeypad.kt | 2 +- .../{auth => unlock}/numpad/PinKeypadModel.kt | 2 +- .../CreatePinPresenterTest.kt | 10 ++-- 23 files changed, 135 insertions(+), 141 deletions(-) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinEvents.kt => setup/SetupPinEvents.kt} (73%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinNode.kt => setup/SetupPinNode.kt} (88%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinPresenter.kt => setup/SetupPinPresenter.kt} (73%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinState.kt => setup/SetupPinState.kt} (72%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinStateProvider.kt => setup/SetupPinStateProvider.kt} (66%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinView.kt => setup/SetupPinView.kt} (80%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/model/PinDigit.kt (92%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/model/PinEntry.kt (96%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/validation/CreatePinFailure.kt (74%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/validation/PinValidator.kt (80%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationEvents.kt => unlock/PinUnlockEvents.kt} (80%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationNode.kt => unlock/PinUnlockNode.kt} (86%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationPresenter.kt => unlock/PinUnlockPresenter.kt} (73%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationState.kt => unlock/PinUnlockState.kt} (80%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationStateProvider.kt => unlock/PinUnlockStateProvider.kt} (70%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationView.kt => unlock/PinUnlockView.kt} (83%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth => unlock}/numpad/PinKeypad.kt (98%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth => unlock}/numpad/PinKeypadModel.kt (92%) rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/CreatePinPresenterTest.kt (92%) diff --git a/build.gradle.kts b/build.gradle.kts index e14ad71981..391313fe2d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -251,8 +251,7 @@ koverMerged { // 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.lockscreen.impl.auth.PinAuthenticationPresenter" - excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter$*" + excludes += "io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter$*" } bound { minValue = 85 diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index d2989d53cc..fa3b88e18a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -27,8 +27,8 @@ 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.lockscreen.impl.auth.PinAuthenticationNode -import io.element.android.features.lockscreen.impl.create.CreatePinNode +import io.element.android.features.lockscreen.impl.setup.SetupPinNode +import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -41,7 +41,7 @@ class LockScreenFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Auth, + initialElement = NavTarget.Unlock, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -50,19 +50,19 @@ class LockScreenFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - data object Auth : NavTarget + data object Unlock : NavTarget @Parcelize - data object Create : NavTarget + data object Setup : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Auth -> { - createNode(buildContext) + NavTarget.Unlock -> { + createNode(buildContext) } - NavTarget.Create -> { - createNode(buildContext) + NavTarget.Setup -> { + createNode(buildContext) } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index e7529e9280..1f7439301c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -37,7 +37,7 @@ class DefaultPinCodeManager @Inject constructor( return pinCodeStore.hasPinCode() } - override suspend fun createPinCode(pinCode: String) { + override suspend fun SetupPinCode(pinCode: String) { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() pinCodeStore.saveEncryptedPinCode(encryptedPinCode) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 5f84f5296d..49b6141665 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -30,7 +30,7 @@ interface PinCodeManager { * Creates a new encrypted pin code. * @param pinCode the clear pin code to create */ - suspend fun createPinCode(pinCode: String) + suspend fun SetupPinCode(pinCode: String) /** * @return true if the pin code is correct. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt similarity index 73% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt index 78ce529325..45c5b034b0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup -sealed interface CreatePinEvents { - data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents - data object ClearFailure : CreatePinEvents +sealed interface SetupPinEvents { + data class OnPinEntryChanged(val entryAsText: String) : SetupPinEvents + data object ClearFailure : SetupPinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt similarity index 88% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt index 331d6ada84..7474289f1e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,16 +27,16 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class CreatePinNode @AssistedInject constructor( +class SetupPinNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: CreatePinPresenter, + private val presenter: SetupPinPresenter, ) : Node(buildContext, plugins = plugins) { @Composable override fun View(modifier: Modifier) { val state = presenter.present() - CreatePinView( + SetupPinView( state = state, onBackClicked = { }, modifier = modifier diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt similarity index 73% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 957594c0f7..747af1bfd4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -14,29 +14,29 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure -import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure +import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import javax.inject.Inject private const val PIN_SIZE = 4 -class CreatePinPresenter @Inject constructor( +class SetupPinPresenter @Inject constructor( private val pinValidator: PinValidator, private val buildMeta: BuildMeta, -) : Presenter { +) : Presenter { @Composable - override fun present(): CreatePinState { + override fun present(): SetupPinState { var choosePinEntry by remember { mutableStateOf(PinEntry.empty(PIN_SIZE)) } @@ -46,20 +46,20 @@ class CreatePinPresenter @Inject constructor( var isConfirmationStep by remember { mutableStateOf(false) } - var createPinFailure by remember { - mutableStateOf(null) + var setupPinFailure by remember { + mutableStateOf(null) } - fun handleEvents(event: CreatePinEvents) { + fun handleEvents(event: SetupPinEvents) { when (event) { - is CreatePinEvents.OnPinEntryChanged -> { + is SetupPinEvents.OnPinEntryChanged -> { if (isConfirmationStep) { confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) if (confirmPinEntry.isPinComplete()) { if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { - createPinFailure = CreatePinFailure.PinsDontMatch + setupPinFailure = SetupPinFailure.PinsDontMatch } } } else { @@ -67,35 +67,35 @@ class CreatePinPresenter @Inject constructor( if (choosePinEntry.isPinComplete()) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { is PinValidator.Result.Invalid -> { - createPinFailure = pinValidationResult.failure + setupPinFailure = pinValidationResult.failure } PinValidator.Result.Valid -> isConfirmationStep = true } } } } - CreatePinEvents.ClearFailure -> { - when (createPinFailure) { - is CreatePinFailure.PinsDontMatch -> { + SetupPinEvents.ClearFailure -> { + when (setupPinFailure) { + is SetupPinFailure.PinsDontMatch -> { choosePinEntry = PinEntry.empty(PIN_SIZE) confirmPinEntry = PinEntry.empty(PIN_SIZE) } - is CreatePinFailure.PinBlacklisted -> { + is SetupPinFailure.PinBlacklisted -> { choosePinEntry = PinEntry.empty(PIN_SIZE) } null -> Unit } isConfirmationStep = false - createPinFailure = null + setupPinFailure = null } } } - return CreatePinState( + return SetupPinState( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - createPinFailure = createPinFailure, + SetupPinFailure = setupPinFailure, appName = buildMeta.applicationName, eventSink = ::handleEvents ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt similarity index 72% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt index 020076a2ab..f2b1f50e5c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt @@ -14,18 +14,18 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure -data class CreatePinState( +data class SetupPinState( val choosePinEntry: PinEntry, val confirmPinEntry: PinEntry, val isConfirmationStep: Boolean, - val createPinFailure: CreatePinFailure?, + val SetupPinFailure: SetupPinFailure?, val appName: String, - val eventSink: (CreatePinEvents) -> Unit + val eventSink: (SetupPinEvents) -> Unit ) { val pinSize = choosePinEntry.size val activePinEntry = if (isConfirmationStep) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt similarity index 66% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt index c9dcce018d..05df16f591 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt @@ -14,47 +14,47 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure -open class CreatePinStateProvider : PreviewParameterProvider { - override val values: Sequence +open class SetupPinStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aCreatePinState(), - aCreatePinState( + aSetupPinState(), + aSetupPinState( choosePinEntry = PinEntry.empty(4).fillWith("12") ), - aCreatePinState( + aSetupPinState( choosePinEntry = PinEntry.empty(4).fillWith("1789"), isConfirmationStep = true, ), - aCreatePinState( + aSetupPinState( choosePinEntry = PinEntry.empty(4).fillWith("1789"), confirmPinEntry = PinEntry.empty(4).fillWith("1788"), isConfirmationStep = true, - creationFailure = CreatePinFailure.PinsDontMatch + creationFailure = SetupPinFailure.PinsDontMatch ), - aCreatePinState( + aSetupPinState( choosePinEntry = PinEntry.empty(4).fillWith("1111"), - creationFailure = CreatePinFailure.PinBlacklisted + creationFailure = SetupPinFailure.PinBlacklisted ), ) } -fun aCreatePinState( +fun aSetupPinState( choosePinEntry: PinEntry = PinEntry.empty(4), confirmPinEntry: PinEntry = PinEntry.empty(4), isConfirmationStep: Boolean = false, - creationFailure: CreatePinFailure? = null, -) = CreatePinState( + creationFailure: SetupPinFailure? = null, +) = SetupPinState( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - createPinFailure = creationFailure, + SetupPinFailure = creationFailure, appName = "Element", eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt index 063d65f41f..3b84e1d889 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalMaterial3Api::class) -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -46,9 +46,9 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.R -import io.element.android.features.lockscreen.impl.create.model.PinDigit -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.setup.model.PinDigit +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog @@ -61,8 +61,8 @@ import io.element.android.libraries.designsystem.theme.pinDigitBg import io.element.android.libraries.theme.ElementTheme @Composable -fun CreatePinView( - state: CreatePinState, +fun SetupPinView( + state: SetupPinState, onBackClicked: () -> Unit, modifier: Modifier = Modifier, ) { @@ -86,15 +86,15 @@ fun CreatePinView( .verticalScroll(state = scrollState) .padding(vertical = 16.dp, horizontal = 20.dp), ) { - CreatePinHeader(state.isConfirmationStep, state.appName) - CreatePinContent(state) + SetupPinHeader(state.isConfirmationStep, state.appName) + SetupPinContent(state) } } ) } @Composable -private fun CreatePinHeader( +private fun SetupPinHeader( isValidationStep: Boolean, appName: String, modifier: Modifier = Modifier, @@ -116,44 +116,44 @@ private fun CreatePinHeader( } @Composable -private fun CreatePinContent( - state: CreatePinState, +private fun SetupPinContent( + state: SetupPinState, modifier: Modifier = Modifier, ) { PinEntryTextField( state.activePinEntry, onValueChange = { - state.eventSink(CreatePinEvents.OnPinEntryChanged(it)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(it)) }, modifier = modifier .padding(top = 36.dp) .fillMaxWidth() ) - if (state.createPinFailure != null) { + if (state.SetupPinFailure != null) { ErrorDialog( modifier = modifier, - title = state.createPinFailure.title(), - content = state.createPinFailure.content(), + title = state.SetupPinFailure.title(), + content = state.SetupPinFailure.content(), onDismiss = { - state.eventSink(CreatePinEvents.ClearFailure) + state.eventSink(SetupPinEvents.ClearFailure) } ) } } @Composable -private fun CreatePinFailure.content(): String { +private fun SetupPinFailure.content(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_content) - CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_content) + SetupPinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_content) + SetupPinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_content) } } @Composable -private fun CreatePinFailure.title(): String { +private fun SetupPinFailure.title(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_title) - CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_title) + SetupPinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_title) + SetupPinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_title) } } @@ -225,9 +225,9 @@ private fun PinDigitView( @Composable @PreviewsDayNight -internal fun CreatePinViewPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) { +internal fun SetupPinViewPreview(@PreviewParameter(SetupPinStateProvider::class) state: SetupPinState) { ElementPreview { - CreatePinView( + SetupPinView( state = state, onBackClicked = {}, ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt similarity index 92% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt index 741a61cafe..10a4832866 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.model +package io.element.android.features.lockscreen.impl.setup.model sealed interface PinDigit { data object Empty : PinDigit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt similarity index 96% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt index a97315f2e8..9f802a989c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.model +package io.element.android.features.lockscreen.impl.setup.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt similarity index 74% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt index 8c0cb78921..3bb21cb9e6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.validation +package io.element.android.features.lockscreen.impl.setup.validation -sealed interface CreatePinFailure { - data object PinBlacklisted : CreatePinFailure - data object PinsDontMatch : CreatePinFailure +sealed interface SetupPinFailure { + data object PinBlacklisted : SetupPinFailure + data object PinsDontMatch : SetupPinFailure } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index 7353ec47d0..0ddf86d887 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.validation +package io.element.android.features.lockscreen.impl.setup.validation import androidx.annotation.VisibleForTesting -import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.model.PinEntry import javax.inject.Inject class PinValidator @Inject constructor() { @@ -29,14 +29,14 @@ class PinValidator @Inject constructor() { sealed interface Result { data object Valid : Result - data class Invalid(val failure: CreatePinFailure) : Result + data class Invalid(val failure: SetupPinFailure) : Result } fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() val isBlacklisted = BLACKLIST.any { it == pinAsText } return if (isBlacklisted) { - Result.Invalid(CreatePinFailure.PinBlacklisted) + Result.Invalid(SetupPinFailure.PinBlacklisted) } else { Result.Valid } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index f9f46c430a..ba35f3045c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -14,8 +14,8 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock -sealed interface PinAuthenticationEvents { - data object Unlock : PinAuthenticationEvents +sealed interface PinUnlockEvents { + data object Unlock : PinUnlockEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt similarity index 86% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt index d236d40cf1..0fba55c17b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,16 +27,16 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class PinAuthenticationNode @AssistedInject constructor( +class PinUnlockNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: PinAuthenticationPresenter, + private val presenter: PinUnlockPresenter, ) : Node(buildContext, plugins = plugins) { @Composable override fun View(modifier: Modifier) { val state = presenter.present() - PinAuthenticationView( + PinUnlockView( state = state, modifier = modifier ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt similarity index 73% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index ecc82f421c..24cba574af 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable import io.element.android.features.lockscreen.api.LockScreenStateService @@ -23,20 +23,20 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject -class PinAuthenticationPresenter @Inject constructor( +class PinUnlockPresenter @Inject constructor( private val pinStateService: LockScreenStateService, private val coroutineScope: CoroutineScope, -) : Presenter { +) : Presenter { @Composable - override fun present(): PinAuthenticationState { + override fun present(): PinUnlockState { - fun handleEvents(event: PinAuthenticationEvents) { + fun handleEvents(event: PinUnlockEvents) { when (event) { - PinAuthenticationEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } + PinUnlockEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } } } - return PinAuthenticationState( + return PinUnlockState( eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 387467534f..dec731c040 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -14,8 +14,8 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock -data class PinAuthenticationState( - val eventSink: (PinAuthenticationEvents) -> Unit +data class PinUnlockState( + val eventSink: (PinUnlockEvents) -> Unit ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt similarity index 70% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index a2612ed858..f5afb44fa9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -14,17 +14,17 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class PinAuthenticationStateProvider : PreviewParameterProvider { - override val values: Sequence +open class PinUnlockStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aPinAuthenticationState(), + aPinUnlockState(), ) } -fun aPinAuthenticationState() = PinAuthenticationState( +fun aPinUnlockState() = PinUnlockState( eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt similarity index 83% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 740d85adec..47027853b4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -14,23 +14,18 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock @@ -41,7 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.features.lockscreen.impl.auth.numpad.PinKeypad +import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad 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 @@ -52,13 +47,13 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme @Composable -fun PinAuthenticationView( - state: PinAuthenticationState, +fun PinUnlockView( + state: PinUnlockState, modifier: Modifier = Modifier, ) { Surface(modifier) { HeaderFooterPage( - header = { PinAuthenticationHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, + header = { PinUnlockHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, content = { Box( modifier = Modifier @@ -76,12 +71,12 @@ fun PinAuthenticationView( } @Composable -private fun PinAuthenticationFooter(state: PinAuthenticationState) { +private fun PinUnlockFooter(state: PinUnlockState) { Button( modifier = Modifier.fillMaxWidth(), text = "Unlock", onClick = { - state.eventSink(PinAuthenticationEvents.Unlock) + state.eventSink(PinUnlockEvents.Unlock) } ) } @@ -116,7 +111,7 @@ private fun PinDot( } @Composable -private fun PinAuthenticationHeader( +private fun PinUnlockHeader( modifier: Modifier = Modifier, ) { Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { @@ -151,9 +146,9 @@ private fun PinAuthenticationHeader( @Composable @PreviewsDayNight -internal fun PinAuthenticationViewPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) { +internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) { ElementPreview { - PinAuthenticationView( + PinUnlockView( state = state, ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt similarity index 98% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index 43abc24002..44a09ed08f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth.numpad +package io.element.android.features.lockscreen.impl.unlock.numpad import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt similarity index 92% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt index 108486e400..4ea42e7f42 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth.numpad +package io.element.android.features.lockscreen.impl.unlock.numpad import androidx.compose.runtime.Immutable diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt similarity index 92% rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt index 78536bb693..85f54fd149 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.create.model.PinDigit -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure -import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.features.lockscreen.impl.setup.model.PinDigit +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest From 56a612e1c57c59014f803fd3622c541c6fef6e4a Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 15:23:10 +0200 Subject: [PATCH 29/70] Pin : move some classes around --- .../impl/components/PinEntryTextField.kt | 116 ++++++++++++++++++ .../impl/{setup => pin}/model/PinDigit.kt | 2 +- .../impl/{setup => pin}/model/PinEntry.kt | 2 +- .../impl/setup/SetupPinPresenter.kt | 2 +- .../lockscreen/impl/setup/SetupPinState.kt | 2 +- .../impl/setup/SetupPinStateProvider.kt | 2 +- .../lockscreen/impl/setup/SetupPinView.kt | 83 +------------ .../impl/setup/validation/PinValidator.kt | 2 +- .../impl/setup/CreatePinPresenterTest.kt | 4 +- 9 files changed, 125 insertions(+), 90 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{setup => pin}/model/PinDigit.kt (93%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{setup => pin}/model/PinEntry.kt (96%) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt new file mode 100644 index 0000000000..5682e594fe --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -0,0 +1,116 @@ +/* + * 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.lockscreen.impl.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.pin.model.PinDigit +import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.pinDigitBg +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun PinEntryTextField( + pinEntry: PinEntry, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + BasicTextField( + modifier = modifier, + value = TextFieldValue(pinEntry.toText()), + onValueChange = { + onValueChange(it.text) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + decorationBox = { + PinEntryRow(pinEntry = pinEntry) + } + ) +} + +@Composable +private fun PinEntryRow( + pinEntry: PinEntry, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + for (digit in pinEntry.digits) { + PinDigitView(digit = digit) + } + } +} + +@Composable +private fun PinDigitView( + digit: PinDigit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(8.dp) + val appearanceModifier = when (digit) { + PinDigit.Empty -> { + Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) + } + is PinDigit.Filled -> { + Modifier.background(ElementTheme.colors.pinDigitBg, shape) + } + } + Box( + modifier = modifier + .size(48.dp) + .then(appearanceModifier), + contentAlignment = Alignment.Center, + + ) { + if (digit is PinDigit.Filled) { + Text( + text = digit.toText(), + style = ElementTheme.typography.fontHeadingMdBold + ) + } + + } +} + +@PreviewsDayNight +@Composable +fun PinEntryTextFieldPreview() { + ElementTheme { + PinEntryTextField( + pinEntry = PinEntry.empty(4).fillWith("12"), + onValueChange = {}, + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt similarity index 93% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt index 10a4832866..aa3c45e02e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.setup.model +package io.element.android.features.lockscreen.impl.pin.model sealed interface PinDigit { data object Empty : PinDigit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt similarity index 96% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index 9f802a989c..d14b71ffe2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.setup.model +package io.element.android.features.lockscreen.impl.pin.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 747af1bfd4..af34c387d2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -21,7 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.libraries.architecture.Presenter diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt index f2b1f50e5c..7823b1e39f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt @@ -16,7 +16,7 @@ package io.element.android.features.lockscreen.impl.setup -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure data class SetupPinState( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt index 05df16f591..37a232dc11 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.lockscreen.impl.setup import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure open class SetupPinStateProvider : PreviewParameterProvider { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt index 3b84e1d889..18be5e14b4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt @@ -18,21 +18,12 @@ package io.element.android.features.lockscreen.impl.setup -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock @@ -41,13 +32,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.R -import io.element.android.features.lockscreen.impl.setup.model.PinDigit -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.components.PinEntryTextField import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.components.button.BackButton @@ -55,10 +43,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.designsystem.theme.pinDigitBg -import io.element.android.libraries.theme.ElementTheme @Composable fun SetupPinView( @@ -157,72 +142,6 @@ private fun SetupPinFailure.title(): String { } } -@Composable -private fun PinEntryTextField( - pinEntry: PinEntry, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, -) { - BasicTextField( - modifier = modifier, - value = TextFieldValue(pinEntry.toText()), - onValueChange = { - onValueChange(it.text) - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - decorationBox = { - PinEntryRow(pinEntry = pinEntry) - } - ) -} - -@Composable -private fun PinEntryRow( - pinEntry: PinEntry, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, - ) { - for (digit in pinEntry.digits) { - PinDigitView(digit = digit) - } - } -} - -@Composable -private fun PinDigitView( - digit: PinDigit, - modifier: Modifier = Modifier, -) { - val shape = RoundedCornerShape(8.dp) - val appearanceModifier = when (digit) { - PinDigit.Empty -> { - Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) - } - is PinDigit.Filled -> { - Modifier.background(ElementTheme.colors.pinDigitBg, shape) - } - } - Box( - modifier = modifier - .size(48.dp) - .then(appearanceModifier), - contentAlignment = Alignment.Center, - - ) { - if (digit is PinDigit.Filled) { - Text( - text = digit.toText(), - style = ElementTheme.typography.fontHeadingMdBold - ) - } - - } -} - @Composable @PreviewsDayNight internal fun SetupPinViewPreview(@PreviewParameter(SetupPinStateProvider::class) state: SetupPinState) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index 0ddf86d887..c7435120aa 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -17,7 +17,7 @@ package io.element.android.features.lockscreen.impl.setup.validation import androidx.annotation.VisibleForTesting -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject class PinValidator @Inject constructor() { diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt index 85f54fd149..723b8e8e6b 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt @@ -20,8 +20,8 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.setup.model.PinDigit -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinDigit +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.libraries.matrix.test.core.aBuildMeta From 9822d8825efdab17e31a1fee0ca55c6bbb69caa2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 15:43:55 +0200 Subject: [PATCH 30/70] Pin unlock : start branching logic --- .../lockscreen/impl/pin/model/PinEntry.kt | 24 ++++++++++++- .../impl/setup/SetupPinPresenter.kt | 4 +-- .../state/DefaultLockScreenStateService.kt | 3 +- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 3 ++ .../impl/unlock/PinUnlockPresenter.kt | 25 +++++++++++++ .../lockscreen/impl/unlock/PinUnlockState.kt | 3 ++ .../impl/unlock/PinUnlockStateProvider.kt | 6 +++- .../lockscreen/impl/unlock/PinUnlockView.kt | 36 +++++++++---------- .../impl/unlock/numpad/PinKeypad.kt | 10 +++--- .../impl/unlock/numpad/PinKeypadModel.kt | 2 +- 10 files changed, 85 insertions(+), 31 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index d14b71ffe2..92dda869a6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -50,14 +50,36 @@ data class PinEntry( return copy(digits = newDigits.toPersistentList()) } + fun deleteLast(): PinEntry { + if (isEmpty()) return this + val newDigits = digits.toMutableList() + newDigits.indexOfLast { it is PinDigit.Filled }.also { lastFilled -> + newDigits[lastFilled] = PinDigit.Empty + } + return copy(digits = newDigits.toPersistentList()) + } + + fun addDigit(digit: Char): PinEntry { + if (isComplete()) return this + val newDigits = digits.toMutableList() + newDigits.indexOfFirst { it is PinDigit.Empty }.also { firstEmpty -> + newDigits[firstEmpty] = PinDigit.Filled(digit) + } + return copy(digits = newDigits.toPersistentList()) + } + fun clear(): PinEntry { return fillWith("") } - fun isPinComplete(): Boolean { + fun isComplete(): Boolean { return digits.all { it is PinDigit.Filled } } + fun isEmpty(): Boolean { + return digits.all { it is PinDigit.Empty } + } + fun toText(): String { return digits.joinToString("") { it.toText() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index af34c387d2..8561e5333c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -55,7 +55,7 @@ class SetupPinPresenter @Inject constructor( is SetupPinEvents.OnPinEntryChanged -> { if (isConfirmationStep) { confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) - if (confirmPinEntry.isPinComplete()) { + if (confirmPinEntry.isComplete()) { if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { @@ -64,7 +64,7 @@ class SetupPinPresenter @Inject constructor( } } else { choosePinEntry = choosePinEntry.fillWith(event.entryAsText) - if (choosePinEntry.isPinComplete()) { + if (choosePinEntry.isComplete()) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { is PinValidator.Result.Invalid -> { setupPinFailure = pinValidationResult.failure diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt index dbfeca2c6a..a071de06e9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt @@ -25,7 +25,6 @@ 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 @@ -57,7 +56,7 @@ class DefaultLockScreenStateService @Inject constructor( override suspend fun entersBackground() = coroutineScope { lockJob = launch { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - delay(GRACE_PERIOD_IN_MILLIS) + //delay(GRACE_PERIOD_IN_MILLIS) _lockScreenState.value = LockScreenState.Locked } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index ba35f3045c..8dddd40e8a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -16,6 +16,9 @@ package io.element.android.features.lockscreen.impl.unlock +import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel + sealed interface PinUnlockEvents { + data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents data object Unlock : PinUnlockEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 24cba574af..d673ad7966 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -17,7 +17,13 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.api.LockScreenStateService +import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -31,13 +37,32 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { + var pinEntry by remember { + mutableStateOf(PinEntry.empty(4)) + } + fun handleEvents(event: PinUnlockEvents) { when (event) { PinUnlockEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } + is PinUnlockEvents.OnPinKeypadPressed -> { + pinEntry = pinEntry.process(event.pinKeypadModel) + if (pinEntry.isComplete()) { + coroutineScope.launch { pinStateService.unlock() } + } + } } } return PinUnlockState( + pinEntry = pinEntry, eventSink = ::handleEvents ) } + + private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry { + return when (pinKeypadModel) { + PinKeypadModel.Back -> deleteLast() + is PinKeypadModel.Number -> addDigit(pinKeypadModel.number) + PinKeypadModel.Empty -> this + } + } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index dec731c040..69d3213c7e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -16,6 +16,9 @@ package io.element.android.features.lockscreen.impl.unlock +import io.element.android.features.lockscreen.impl.pin.model.PinEntry + data class PinUnlockState( + val pinEntry: PinEntry, val eventSink: (PinUnlockEvents) -> Unit ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index f5afb44fa9..5120818316 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.lockscreen.impl.pin.model.PinEntry open class PinUnlockStateProvider : PreviewParameterProvider { override val values: Sequence @@ -25,6 +26,9 @@ open class PinUnlockStateProvider : PreviewParameterProvider { ) } -fun aPinUnlockState() = PinUnlockState( +fun aPinUnlockState( + pinEntry: PinEntry = PinEntry.empty(4), +) = PinUnlockState( + pinEntry = pinEntry, eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 47027853b4..5631f33e32 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -36,11 +36,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.pin.model.PinDigit +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad 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.Icon import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text @@ -53,7 +54,12 @@ fun PinUnlockView( ) { Surface(modifier) { HeaderFooterPage( - header = { PinUnlockHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, + header = { + PinUnlockHeader( + state = state, + modifier = Modifier.padding(top = 60.dp, bottom = 12.dp) + ) + }, content = { Box( modifier = Modifier @@ -62,7 +68,9 @@ fun PinUnlockView( contentAlignment = Alignment.Center, ) { PinKeypad( - onClick = {} + onClick = { + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) + } ) } } @@ -70,26 +78,15 @@ fun PinUnlockView( } } -@Composable -private fun PinUnlockFooter(state: PinUnlockState) { - Button( - modifier = Modifier.fillMaxWidth(), - text = "Unlock", - onClick = { - state.eventSink(PinUnlockEvents.Unlock) - } - ) -} - @Composable private fun PinDotsRow( + pinEntry: PinEntry, modifier: Modifier = Modifier, ) { Row(modifier, horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { - PinDot(isFilled = true) - PinDot(isFilled = true) - PinDot(isFilled = false) - PinDot(isFilled = false) + for (digit in pinEntry.digits) { + PinDot(isFilled = digit is PinDigit.Filled) + } } } @@ -112,6 +109,7 @@ private fun PinDot( @Composable private fun PinUnlockHeader( + state: PinUnlockState, modifier: Modifier = Modifier, ) { Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { @@ -140,7 +138,7 @@ private fun PinUnlockHeader( color = MaterialTheme.colorScheme.secondary, ) Spacer(Modifier.height(24.dp)) - PinDotsRow() + PinDotsRow(state.pinEntry) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index 44a09ed08f..d22a34b732 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -61,25 +61,25 @@ fun PinKeypad( PinKeypadRow( verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number("1"), PinKeypadModel.Number("2"), PinKeypadModel.Number("3")), + models = listOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')), onClick = onClick, ) PinKeypadRow( verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number("4"), PinKeypadModel.Number("5"), PinKeypadModel.Number("6")), + models = listOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')), onClick = onClick, ) PinKeypadRow( verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number("7"), PinKeypadModel.Number("8"), PinKeypadModel.Number("9")), + models = listOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')), onClick = onClick, ) PinKeypadRow( verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number("0"), PinKeypadModel.Back), + models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back), onClick = onClick, ) } @@ -114,7 +114,7 @@ private fun PinKeypadRow( PinKeyBadDigitButton( size = 80.dp, modifier = commonModifier, - digit = model.number, + digit = model.number.toString(), onClick = { onClick(model) }, ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt index 4ea42e7f42..f1430dcaa5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt @@ -22,5 +22,5 @@ import androidx.compose.runtime.Immutable sealed interface PinKeypadModel { data object Empty : PinKeypadModel data object Back : PinKeypadModel - data class Number(val number: String) : PinKeypadModel + data class Number(val number: Char) : PinKeypadModel } From 223942fda9f1b961ccf5f001ede0f3a09603ca22 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 16:35:54 +0200 Subject: [PATCH 31/70] Pin unlock : fix some ui --- .../lockscreen/impl/unlock/PinUnlockView.kt | 8 +++- .../impl/unlock/numpad/PinKeypad.kt | 40 ++++++++++++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 5631f33e32..686790afcb 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -65,12 +66,15 @@ fun PinUnlockView( modifier = Modifier .padding(top = 40.dp) .fillMaxWidth(), - contentAlignment = Alignment.Center, ) { PinKeypad( onClick = { state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) - } + }, + horizontalArrangement = spacedBy(24.dp, Alignment.CenterHorizontally), + verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, ) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index d22a34b732..39656a8d56 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -17,18 +17,21 @@ package io.element.android.features.lockscreen.impl.unlock.numpad import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Backspace import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -96,7 +99,7 @@ private fun PinKeypadRow( Row( horizontalArrangement = horizontalArrangement, verticalAlignment = verticalAlignment, - modifier = modifier, + modifier = modifier.fillMaxWidth(), ) { val commonModifier = Modifier.size(80.dp) for (model in models) { @@ -123,6 +126,22 @@ private fun PinKeypadRow( } } +@Composable +private fun PinKeypadButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(CircleShape) + .background(color = ElementTheme.colors.bgSubtlePrimary) + .clickable(onClick = onClick), + content = content + ) +} + @Composable private fun PinKeyBadDigitButton( digit: String, @@ -130,15 +149,8 @@ private fun PinKeyBadDigitButton( onClick: (String) -> Unit, modifier: Modifier = Modifier, ) { - Button( - colors = ButtonDefaults.buttonColors( - containerColor = ElementTheme.colors.bgSubtlePrimary, - contentColor = Color.Transparent, - ), - shape = CircleShape, - contentPadding = PaddingValues(0.dp), - modifier = modifier - .clip(CircleShape), + PinKeypadButton( + modifier = modifier, onClick = { onClick(digit) } ) { val fontSize = 80.dp.toSp() / 2 @@ -158,10 +170,8 @@ private fun PinKeypadBackButton( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - IconButton( - modifier = modifier - .clip(CircleShape) - .background(color = Color.Transparent, shape = CircleShape), + PinKeypadButton( + modifier = modifier, onClick = onClick, ) { Icon( From 02c5873fc9ee0e4541c7e7b1a187226231fc14d3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 17:57:29 +0200 Subject: [PATCH 32/70] Pin unlock : best effort for small height --- .../lockscreen/impl/unlock/PinUnlockView.kt | 122 +++++++++++++++--- 1 file changed, 101 insertions(+), 21 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 686790afcb..4f4f9be2df 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -17,16 +17,20 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock @@ -34,18 +38,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad -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.Icon import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.theme.ElementTheme @Composable @@ -54,31 +60,95 @@ fun PinUnlockView( modifier: Modifier = Modifier, ) { Surface(modifier) { - HeaderFooterPage( - header = { + BoxWithConstraints { + val commonModifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .padding(all = 20.dp) + + val header = @Composable { PinUnlockHeader( state = state, modifier = Modifier.padding(top = 60.dp, bottom = 12.dp) ) - }, - content = { - Box( - modifier = Modifier - .padding(top = 40.dp) - .fillMaxWidth(), - ) { - PinKeypad( - onClick = { - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) - }, - horizontalArrangement = spacedBy(24.dp, Alignment.CenterHorizontally), - verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, - verticalAlignment = Alignment.CenterVertically, - ) - } } - ) + val footer = @Composable { + PinUnlockFooter() + } + val content = @Composable { + PinKeypad( + onClick = { + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) + }, + horizontalArrangement = spacedBy(24.dp, Alignment.CenterHorizontally), + verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + ) + } + if (maxHeight < 600.dp) { + PinUnlockCompactView( + header = header, + footer = footer, + content = content, + modifier = commonModifier, + ) + } else { + PinUnlockExpandedView( + header = header, + footer = footer, + content = content, + modifier = commonModifier, + ) + } + } + } +} + +@Composable +fun PinUnlockCompactView( + modifier: Modifier = Modifier, + header: @Composable () -> Unit, + footer: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + Row(modifier = modifier) { + Column(Modifier.weight(1f)) { + header() + Spacer(modifier = Modifier.height(24.dp)) + footer() + } + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + contentAlignment = Alignment.Center, + ) { + content() + } + } +} + +@Composable +fun PinUnlockExpandedView( + modifier: Modifier = Modifier, + header: @Composable () -> Unit, + footer: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier, + ) { + header() + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(top = 40.dp), + ) { + content() + } + footer() } } @@ -146,6 +216,16 @@ private fun PinUnlockHeader( } } +@Composable +private fun PinUnlockFooter( + modifier: Modifier = Modifier, +) { + Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { + TextButton(text = "Use biometric", onClick = { }) + TextButton(text = stringResource(id = R.string.screen_app_lock_forgot_pin), onClick = { }) + } +} + @Composable @PreviewsDayNight internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) { From d12fa5c8fafee6aaccd82e40eec3aa9403cea1bc Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 18:52:56 +0200 Subject: [PATCH 33/70] Pin unlock : add signout prompt --- features/lockscreen/impl/build.gradle.kts | 1 + .../impl/pin/DefaultPinCodeManager.kt | 2 +- .../lockscreen/impl/pin/PinCodeManager.kt | 2 +- .../lockscreen/impl/pin/model/PinEntry.kt | 3 +- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 1 + .../impl/unlock/PinUnlockPresenter.kt | 19 ++++++++-- .../lockscreen/impl/unlock/PinUnlockState.kt | 7 +++- .../impl/unlock/PinUnlockStateProvider.kt | 10 ++++++ .../lockscreen/impl/unlock/PinUnlockView.kt | 36 +++++++++++++++++-- .../impl/PreferencesFeatureFlagProvider.kt | 5 +-- 10 files changed, 74 insertions(+), 12 deletions(-) diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 028d8bee3c..8daeb2178c 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.cryptography.api) + implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index 1f7439301c..f5848a9d40 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -37,7 +37,7 @@ class DefaultPinCodeManager @Inject constructor( return pinCodeStore.hasPinCode() } - override suspend fun SetupPinCode(pinCode: String) { + override suspend fun setupPinCode(pinCode: String) { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() pinCodeStore.saveEncryptedPinCode(encryptedPinCode) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 49b6141665..09197c3eb1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -30,7 +30,7 @@ interface PinCodeManager { * Creates a new encrypted pin code. * @param pinCode the clear pin code to create */ - suspend fun SetupPinCode(pinCode: String) + suspend fun setupPinCode(pinCode: String) /** * @return true if the pin code is correct. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index 92dda869a6..76331bd38f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -18,10 +18,11 @@ package io.element.android.features.lockscreen.impl.pin.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList +import java.io.Serializable data class PinEntry( val digits: ImmutableList, -) { +): Serializable { companion object { fun empty(size: Int): PinEntry { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index 8dddd40e8a..a90b6cb702 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -21,4 +21,5 @@ import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel sealed interface PinUnlockEvents { data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents data object Unlock : PinUnlockEvents + data object OnForgetPin : PinUnlockEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index d673ad7966..d6b77f799c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -18,8 +18,9 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.features.lockscreen.impl.pin.model.PinEntry @@ -36,10 +37,18 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { - - var pinEntry by remember { + var pinEntry by rememberSaveable { mutableStateOf(PinEntry.empty(4)) } + var remainingAttempts by rememberSaveable { + mutableIntStateOf(3) + } + var showWrongPinTitle by rememberSaveable { + mutableStateOf(false) + } + var showSignOutPrompt by rememberSaveable { + mutableStateOf(false) + } fun handleEvents(event: PinUnlockEvents) { when (event) { @@ -50,10 +59,14 @@ class PinUnlockPresenter @Inject constructor( coroutineScope.launch { pinStateService.unlock() } } } + PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true } } return PinUnlockState( pinEntry = pinEntry, + showWrongPinTitle = showWrongPinTitle, + remainingAttempts = remainingAttempts, + showSignOutPrompt = showSignOutPrompt, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 69d3213c7e..1787fb8e8b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -20,5 +20,10 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry data class PinUnlockState( val pinEntry: PinEntry, + val showWrongPinTitle: Boolean, + val remainingAttempts: Int, + val showSignOutPrompt: Boolean, val eventSink: (PinUnlockEvents) -> Unit -) +) { + val isSignOutPromptCancellable = remainingAttempts > 0 +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 5120818316..4f269d2f5a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -23,12 +23,22 @@ open class PinUnlockStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPinUnlockState(), + aPinUnlockState(pinEntry = PinEntry.empty(4).fillWith("12")), + aPinUnlockState(showWrongPinTitle = true), + aPinUnlockState(showSignOutPrompt = true), + aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), ) } fun aPinUnlockState( pinEntry: PinEntry = PinEntry.empty(4), + remainingAttempts: Int = 3, + showWrongPinTitle: Boolean = false, + showSignOutPrompt: Boolean = false, ) = PinUnlockState( pinEntry = pinEntry, + showWrongPinTitle = showWrongPinTitle, + remainingAttempts = remainingAttempts, + showSignOutPrompt = showSignOutPrompt, eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 4f4f9be2df..19cb866a5c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter @@ -46,6 +47,8 @@ import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon @@ -53,6 +56,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun PinUnlockView( @@ -101,6 +105,22 @@ fun PinUnlockView( modifier = commonModifier, ) } + if (state.showSignOutPrompt) { + if (state.isSignOutPromptCancellable) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_app_lock_signout_alert_title), + content = stringResource(id = R.string.screen_app_lock_signout_alert_message), + onSubmitClicked = {}, + onDismiss = {}, + ) + } else { + ErrorDialog( + title = stringResource(id = R.string.screen_app_lock_signout_alert_title), + content = stringResource(id = R.string.screen_app_lock_signout_alert_message), + onDismiss = {}, + ) + } + } } } } @@ -196,7 +216,7 @@ private fun PinUnlockHeader( ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Enter your PIN", + text = stringResource(id = CommonStrings.common_enter_your_pin), modifier = Modifier .fillMaxWidth(), textAlign = TextAlign.Center, @@ -204,12 +224,22 @@ private fun PinUnlockHeader( color = MaterialTheme.colorScheme.primary, ) Spacer(Modifier.height(8.dp)) + val subtitle = if (state.showWrongPinTitle) { + pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = state.remainingAttempts, state.remainingAttempts) + } else { + stringResource(id = R.string.screen_app_lock_subtitle) + } + val subtitleColor = if (state.showWrongPinTitle) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.secondary + } Text( - text = "You have 3 attempts to unlock", + text = subtitle, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, + color = subtitleColor, ) Spacer(Modifier.height(24.dp)) PinDotsRow(state.pinEntry) diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt index ddffdebd34..23ff977da2 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -24,6 +24,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.featureflag.api.Feature +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -44,10 +45,10 @@ class PreferencesFeatureFlagProvider @Inject constructor(@ApplicationContext con } } - override suspend fun isFeatureEnabled(feature: Feature): Boolean { + override fun isFeatureEnabled(feature: Feature): Flow { return store.data.map { prefs -> prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue - }.first() + } } override fun hasFeature(feature: Feature): Boolean { From 9104e7d5857aaa26ee18a02d426cfd682a46801a Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 20:25:54 +0200 Subject: [PATCH 34/70] Pin unlock : better PinKeypad management --- .../lockscreen/impl/unlock/PinUnlockView.kt | 16 +++---- .../impl/unlock/numpad/PinKeypad.kt | 42 ++++++++++++++----- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 19cb866a5c..2fff711131 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -79,15 +80,14 @@ fun PinUnlockView( val footer = @Composable { PinUnlockFooter() } - val content = @Composable { + val content = @Composable { constraints: BoxWithConstraintsScope -> PinKeypad( onClick = { state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) }, - horizontalArrangement = spacedBy(24.dp, Alignment.CenterHorizontally), - verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), + maxWidth = constraints.maxWidth, + maxHeight = constraints.maxHeight, horizontalAlignment = Alignment.CenterHorizontally, - verticalAlignment = Alignment.CenterVertically, ) } if (maxHeight < 600.dp) { @@ -130,7 +130,7 @@ fun PinUnlockCompactView( modifier: Modifier = Modifier, header: @Composable () -> Unit, footer: @Composable () -> Unit, - content: @Composable () -> Unit, + content: @Composable BoxWithConstraintsScope.() -> Unit, ) { Row(modifier = modifier) { Column(Modifier.weight(1f)) { @@ -138,7 +138,7 @@ fun PinUnlockCompactView( Spacer(modifier = Modifier.height(24.dp)) footer() } - Box( + BoxWithConstraints( modifier = Modifier .weight(1f) .fillMaxHeight(), @@ -154,13 +154,13 @@ fun PinUnlockExpandedView( modifier: Modifier = Modifier, header: @Composable () -> Unit, footer: @Composable () -> Unit, - content: @Composable () -> Unit, + content: @Composable BoxWithConstraintsScope.() -> Unit, ) { Column( modifier = modifier, ) { header() - Box( + BoxWithConstraints( modifier = Modifier .weight(1f) .fillMaxWidth() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index 39656a8d56..ee8adb19b2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -19,10 +19,11 @@ package io.element.android.features.lockscreen.impl.unlock.numpad import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -30,56 +31,68 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Backspace -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.coerceAtMost import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.times import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toSp import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.theme.ElementTheme +private val spaceBetweenPinKey = 8.dp +private val maxSizePinKey = 80.dp + @Composable fun PinKeypad( onClick: (PinKeypadModel) -> Unit, + maxWidth: Dp, + maxHeight: Dp, modifier: Modifier = Modifier, - verticalArrangement: Arrangement.Vertical = Arrangement.Top, verticalAlignment: Alignment.Vertical = Alignment.Top, - horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, horizontalAlignment: Alignment.Horizontal = Alignment.Start, ) { + val pinKeyMaxWidth = ((maxWidth - 2 * spaceBetweenPinKey) / 3).coerceAtMost(maxSizePinKey) + val pinKeyMaxHeight = ((maxHeight - 3 * spaceBetweenPinKey) / 4).coerceAtMost(maxSizePinKey) + val pinKeySize = if (pinKeyMaxWidth < pinKeyMaxHeight) pinKeyMaxWidth else pinKeyMaxHeight + + val horizontalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterHorizontally) + val verticalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterVertically) Column( modifier = modifier, verticalArrangement = verticalArrangement, horizontalAlignment = horizontalAlignment, ) { PinKeypadRow( + pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, models = listOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')), onClick = onClick, ) PinKeypadRow( + pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, models = listOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')), onClick = onClick, ) PinKeypadRow( + pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, models = listOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')), onClick = onClick, ) PinKeypadRow( + pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back), @@ -92,6 +105,7 @@ fun PinKeypad( private fun PinKeypadRow( models: List, onClick: (PinKeypadModel) -> Unit, + pinKeySize: Dp, modifier: Modifier = Modifier, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, verticalAlignment: Alignment.Vertical = Alignment.Top, @@ -101,7 +115,7 @@ private fun PinKeypadRow( verticalAlignment = verticalAlignment, modifier = modifier.fillMaxWidth(), ) { - val commonModifier = Modifier.size(80.dp) + val commonModifier = Modifier.size(pinKeySize) for (model in models) { when (model) { is PinKeypadModel.Empty -> { @@ -115,7 +129,7 @@ private fun PinKeypadRow( } is PinKeypadModel.Number -> { PinKeyBadDigitButton( - size = 80.dp, + size = pinKeySize, modifier = commonModifier, digit = model.number.toString(), onClick = { onClick(model) }, @@ -153,7 +167,7 @@ private fun PinKeyBadDigitButton( modifier = modifier, onClick = { onClick(digit) } ) { - val fontSize = 80.dp.toSp() / 2 + val fontSize = size.toSp() / 2 val originalFont = ElementTheme.typography.fontHeadingXlBold val ratio = fontSize.value / originalFont.fontSize.value val lineHeight = originalFont.lineHeight * ratio @@ -183,9 +197,15 @@ private fun PinKeypadBackButton( @Composable @PreviewsDayNight -fun PinKeypad() { +fun PinKeypadPreview() { ElementPreview { - PinKeypad(onClick = {}) + BoxWithConstraints { + PinKeypad( + maxWidth = maxWidth, + maxHeight = maxHeight, + onClick = {} + ) + } } } From 5fc04bd0795fb442dcd76e67818a79dcfc0129e9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 20:26:04 +0200 Subject: [PATCH 35/70] Fix compilation --- .../android/features/lockscreen/impl/pin/model/PinEntry.kt | 3 +-- .../features/lockscreen/impl/unlock/PinUnlockPresenter.kt | 3 ++- .../featureflag/impl/PreferencesFeatureFlagProvider.kt | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index 76331bd38f..92dda869a6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -18,11 +18,10 @@ package io.element.android.features.lockscreen.impl.pin.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList -import java.io.Serializable data class PinEntry( val digits: ImmutableList, -): Serializable { +) { companion object { fun empty(size: Int): PinEntry { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index d6b77f799c..a6f06158d4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.api.LockScreenStateService @@ -37,7 +38,7 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { - var pinEntry by rememberSaveable { + var pinEntry by remember { mutableStateOf(PinEntry.empty(4)) } var remainingAttempts by rememberSaveable { diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt index 23ff977da2..ddffdebd34 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -24,7 +24,6 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.featureflag.api.Feature -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -45,10 +44,10 @@ class PreferencesFeatureFlagProvider @Inject constructor(@ApplicationContext con } } - override fun isFeatureEnabled(feature: Feature): Flow { + override suspend fun isFeatureEnabled(feature: Feature): Boolean { return store.data.map { prefs -> prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue - } + }.first() } override fun hasFeature(feature: Feature): Boolean { From 710b2c52f1536fb9d707a4cb0219982fb1a781f0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 20:38:27 +0200 Subject: [PATCH 36/70] Fix some warnings --- .../lockscreen/impl/setup/SetupPinPresenter.kt | 2 +- .../features/lockscreen/impl/setup/SetupPinState.kt | 3 +-- .../lockscreen/impl/setup/SetupPinStateProvider.kt | 2 +- .../features/lockscreen/impl/setup/SetupPinView.kt | 6 +++--- .../{CreatePinFailure.kt => SetupPinFailure.kt} | 0 .../impl/state/DefaultLockScreenStateService.kt | 2 +- .../features/lockscreen/impl/unlock/PinUnlockView.kt | 4 ++-- .../lockscreen/impl/unlock/numpad/PinKeypad.kt | 12 +++++++----- 8 files changed, 16 insertions(+), 15 deletions(-) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/{CreatePinFailure.kt => SetupPinFailure.kt} (100%) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 8561e5333c..6a880a6967 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -95,7 +95,7 @@ class SetupPinPresenter @Inject constructor( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - SetupPinFailure = setupPinFailure, + setupPinFailure = setupPinFailure, appName = buildMeta.applicationName, eventSink = ::handleEvents ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt index 7823b1e39f..3ae4a2c85b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt @@ -23,11 +23,10 @@ data class SetupPinState( val choosePinEntry: PinEntry, val confirmPinEntry: PinEntry, val isConfirmationStep: Boolean, - val SetupPinFailure: SetupPinFailure?, + val setupPinFailure: SetupPinFailure?, val appName: String, val eventSink: (SetupPinEvents) -> Unit ) { - val pinSize = choosePinEntry.size val activePinEntry = if (isConfirmationStep) { confirmPinEntry } else { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt index 37a232dc11..1a177b4a83 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt @@ -54,7 +54,7 @@ fun aSetupPinState( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - SetupPinFailure = creationFailure, + setupPinFailure = creationFailure, appName = "Element", eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt index 18be5e14b4..b8f40b06d0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt @@ -114,11 +114,11 @@ private fun SetupPinContent( .padding(top = 36.dp) .fillMaxWidth() ) - if (state.SetupPinFailure != null) { + if (state.setupPinFailure != null) { ErrorDialog( modifier = modifier, - title = state.SetupPinFailure.title(), - content = state.SetupPinFailure.content(), + title = state.setupPinFailure.title(), + content = state.setupPinFailure.content(), onDismiss = { state.eventSink(SetupPinEvents.ClearFailure) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt similarity index 100% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt index a071de06e9..f2e037b111 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject -private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L +//private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 2fff711131..be43d6cf32 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -127,9 +127,9 @@ fun PinUnlockView( @Composable fun PinUnlockCompactView( - modifier: Modifier = Modifier, header: @Composable () -> Unit, footer: @Composable () -> Unit, + modifier: Modifier = Modifier, content: @Composable BoxWithConstraintsScope.() -> Unit, ) { Row(modifier = modifier) { @@ -151,9 +151,9 @@ fun PinUnlockCompactView( @Composable fun PinUnlockExpandedView( - modifier: Modifier = Modifier, header: @Composable () -> Unit, footer: @Composable () -> Unit, + modifier: Modifier = Modifier, content: @Composable BoxWithConstraintsScope.() -> Unit, ) { Column( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index ee8adb19b2..8a6230c725 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -46,6 +46,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toSp import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.theme.ElementTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf private val spaceBetweenPinKey = 8.dp private val maxSizePinKey = 80.dp @@ -74,28 +76,28 @@ fun PinKeypad( pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')), + models = persistentListOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')), onClick = onClick, ) PinKeypadRow( pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')), + models = persistentListOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')), onClick = onClick, ) PinKeypadRow( pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')), + models = persistentListOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')), onClick = onClick, ) PinKeypadRow( pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back), + models = persistentListOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back), onClick = onClick, ) } @@ -103,7 +105,7 @@ fun PinKeypad( @Composable private fun PinKeypadRow( - models: List, + models: ImmutableList, onClick: (PinKeypadModel) -> Unit, pinKeySize: Dp, modifier: Modifier = Modifier, From 7c5fab732bc4a72731fb38721146b11ff2d4d728 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 22:32:59 +0200 Subject: [PATCH 37/70] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.63 (#1619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update dependency org.matrix.rustcomponents:sdk-android to v0.1.63 * Update Element Call integrated APIs * Take into account the new `MessageType.Other` from the SDK --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín Co-authored-by: Benoit Marty --- .../event/TimelineItemContentMessageFactory.kt | 9 ++++++++- gradle/libs.versions.toml | 2 +- .../impl/DefaultRoomLastMessageFormatter.kt | 7 ++++++- .../impl/DefaultRoomLastMessageFormatterTest.kt | 4 ++++ .../matrix/api/timeline/item/event/MessageType.kt | 5 +++++ .../libraries/matrix/impl/room/RustMatrixRoom.kt | 10 +++++----- .../impl/timeline/item/event/EventMessageMapper.kt | 5 +++++ .../impl/widget/DefaultCallWidgetSettingsProvider.kt | 2 +- .../matrix/impl/widget/MatrixWidgetSettings.kt | 4 ++-- .../libraries/matrix/impl/widget/RustWidgetDriver.kt | 6 +++--- .../push/impl/notifications/NotifiableEventResolver.kt | 2 ++ 11 files changed, 42 insertions(+), 14 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 323f110f47..f39c1e0989 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageT import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType @@ -131,8 +132,14 @@ class TimelineItemContentMessageFactory @Inject constructor( htmlDocument = messageType.formatted?.toHtmlDocument(), isEdited = content.isEdited, ) + is OtherMessageType -> TimelineItemTextContent( + body = messageType.body, + htmlDocument = null, + isEdited = content.isEdited, + ) UnknownMessageType -> TimelineItemTextContent( - // Display the body as a fallback + // Display the body as a fallback, but should not happen anymore + // (we have `OtherMessageType` now) body = content.body, htmlDocument = null, isEdited = content.isEdited, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 23aaa4327b..807adf020c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -148,7 +148,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.62" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.63" matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt index d041078051..80e7a7155b 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails @@ -129,8 +130,12 @@ class DefaultRoomLastMessageFormatter @Inject constructor( is AudioMessageType -> { sp.getString(CommonStrings.common_audio) } + is OtherMessageType -> { + messageType.body + } UnknownMessageType -> { - // Display the body as a fallback + // Display the body as a fallback, but should not happen anymore + // (we have `OtherMessageType` now) messageContent.body } is NoticeMessageType -> { diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt index 50d313f132..bc58aa1d48 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent @@ -204,6 +205,7 @@ class DefaultRoomLastMessageFormatterTest { is EmoteMessageType -> "* $senderName ${type.body}" is TextMessageType, is NoticeMessageType, + is OtherMessageType, UnknownMessageType -> body } Truth.assertWithMessage("$type was not properly handled for DM").that(result).isEqualTo(expectedResult) @@ -220,6 +222,7 @@ class DefaultRoomLastMessageFormatterTest { is LocationMessageType -> "$senderName: Shared location" is TextMessageType, is NoticeMessageType, + is OtherMessageType, UnknownMessageType -> "$senderName: $body" is EmoteMessageType -> "* $senderName ${type.body}" } @@ -231,6 +234,7 @@ class DefaultRoomLastMessageFormatterTest { is LocationMessageType -> false is EmoteMessageType -> false is TextMessageType, is NoticeMessageType -> true + is OtherMessageType -> true UnknownMessageType -> true } if (shouldCreateAnnotatedString) { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt index ba6eeca819..09f0c00a7c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt @@ -73,3 +73,8 @@ data class TextMessageType( val body: String, val formatted: FormattedBody? ) : MessageType + +data class OtherMessageType( + val msgType: String, + val body: String, +) : MessageType diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index c778fa3ac5..8dd0c02321 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -69,8 +69,8 @@ import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle -import org.matrix.rustcomponents.sdk.WidgetPermissions -import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider +import org.matrix.rustcomponents.sdk.WidgetCapabilities +import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import timber.log.Timber @@ -497,9 +497,9 @@ class RustMatrixRoom( RustWidgetDriver( widgetSettings = widgetSettings, room = innerRoom, - widgetPermissionsProvider = object : WidgetPermissionsProvider { - override fun acquirePermissions(permissions: WidgetPermissions): WidgetPermissions { - return permissions + widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider { + override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities { + return capabilities } }, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 18d2e1bdeb..521ff3bd5a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -28,11 +28,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.impl.media.map import org.matrix.rustcomponents.sdk.Message +import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.ProfileDetails import org.matrix.rustcomponents.sdk.RepliedToEventDetails import org.matrix.rustcomponents.sdk.use @@ -104,6 +106,9 @@ class EventMessageMapper { is RustMessageType.Location -> { LocationMessageType(type.content.body, type.content.geoUri, type.content.description) } + is MessageType.Other -> { + OtherMessageType(type.msgtype, type.body) + } null -> UnknownMessageType } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt index a7f208e69d..1a34a31167 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt @@ -37,7 +37,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettin appPrompt = false, skipLobby = true, confineToRoom = true, - fonts = null, + font = null, analyticsId = null ) val rustWidgetSettings = newVirtualElementCallWidget(options) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt index 65e6c8bc84..018e02816c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt @@ -23,13 +23,13 @@ import org.matrix.rustcomponents.sdk.WidgetSettings import org.matrix.rustcomponents.sdk.generateWebviewUrl fun MatrixWidgetSettings.toRustWidgetSettings() = WidgetSettings( - id = this.id, + widgetId = this.id, initAfterContentLoad = this.initAfterContentLoad, rawUrl = this.rawUrl, ) fun MatrixWidgetSettings.Companion.fromRustWidgetSettings(widgetSettings: WidgetSettings) = MatrixWidgetSettings( - id = widgetSettings.id, + id = widgetSettings.widgetId, initAfterContentLoad = widgetSettings.initAfterContentLoad, rawUrl = widgetSettings.rawUrl, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt index e385a34e0d..2764cecfdc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider +import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.makeWidgetDriver import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.coroutineContext @@ -33,7 +33,7 @@ import kotlin.coroutines.coroutineContext class RustWidgetDriver( widgetSettings: MatrixWidgetSettings, private val room: Room, - private val widgetPermissionsProvider: WidgetPermissionsProvider, + private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider, ): MatrixWidgetDriver { override val incomingMessages = MutableSharedFlow() @@ -54,7 +54,7 @@ class RustWidgetDriver( val coroutineScope = CoroutineScope(coroutineContext) coroutineScope.launch { // This call will suspend the coroutine while the driver is running, so it needs to be launched separately - driverAndHandle.driver.run(room, widgetPermissionsProvider) + driverAndHandle.driver.run(room, widgetCapabilitiesProvider) } receiveMessageJob = coroutineScope.launch(Dispatchers.IO) { try { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 113a323611..aabc58befb 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType @@ -216,6 +217,7 @@ class NotifiableEventResolver @Inject constructor( is TextMessageType -> messageType.body is VideoMessageType -> messageType.body is LocationMessageType -> messageType.body + is OtherMessageType -> messageType.body is UnknownMessageType -> stringProvider.getString(CommonStrings.common_unsupported_event) } } From 45ea74bbfde9d8e4298b8ab339c4aff6cc9bebd7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:36:15 +0200 Subject: [PATCH 38/70] Update dependency org.owasp.dependencycheck to v8.4.2 (#1622) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 807adf020c..50de2438c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ serialization_json = "1.6.0" showkase = "1.0.0-beta18" jsoup = "1.16.2" appyx = "1.4.0" -dependencycheck = "8.4.0" +dependencycheck = "8.4.2" dependencyanalysis = "1.25.0" stem = "2.3.0" sqldelight = "2.0.0" From 68ff7c5e7b8b1539789bbaa6ed22f54689bd90d8 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 23 Oct 2023 08:15:26 +0000 Subject: [PATCH 39/70] Update screenshots --- ...null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png | 3 --- ...nents_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png | 3 +++ ...nents_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png | 3 +++ ...l.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png} | 0 ...unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png | 3 +++ ...unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png | 3 +++ 26 files changed, 42 insertions(+), 6 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.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.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index f1992899f6..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -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.lockscreen.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.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index f7a21c8025..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -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.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b2f7ce4747 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de1d44f62edd3f2d30421e49ad435971b165711b8bfe6d4a9475c5fb2f9f83ed +size 8506 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a4c7739624 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d58d2d25d8f2c07c976bcda0eea7ec101f993ad1ef733fae4c713a67650e33e2 +size 8498 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fc54ab5f7e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b14ff41b74e36809f359a7b3b73ffdb9c0c2cd97ec786333bcce9412f05825e6 +size 30608 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..62b7c73a26 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1521d9fecaaadcfc7ed4670d997b36a20bd2a84f564794a06cb10bfe7d1ad64e +size 28812 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e81bbf1c1d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c369763c4be676d6727a6b0f5cd74d56a9de09b9c3780e1806874d19ba7ffd3f +size 42705 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bcb90d6b4a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a63439f320a1b6ad124cb943f95651f85d2e9cfc34597cb450c803b6dafaefa7 +size 43149 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bf2823f6a4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:165e37ef78a7b09bbd49e8d692ccb27836e35d8535b182d9837a5c3daa5cf3a6 +size 43978 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b3ca08978d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecaba43c72a890d85f9894eb987f291967c8ea15bdc09db88d6a72384098d827 +size 48308 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5b8c62dcbf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25175242be4133a74541f0bb2d05db95484a65da952031fb8f68a2fdef88a995 +size 45880 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fc34314f30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:699c96cc0d06c85eeda3a3b408877d37445bebf1873a86d6255ba8626ba410b0 +size 39654 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6d029a3034 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:233bb8cb261f752ac868c492d075fea9e5af656fcc8ad0d12fbf05b2dd690788 +size 40113 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af181f44e5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:920b9f28c3d2f5e726ad4beeb668e75c285b0e731e6d7eaaf87ca3b2a8b9980b +size 40652 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2616989dcd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28197831dacc61f25d6b3a0080db331ef317283e8032fa5cc4ed457709da0f89 +size 42980 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ce7df11d83 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652154d82c5f43f91cface3f2e5650ee5a98cf1faed08a80589ab42225bd8120 +size 40557 From 46bfbc06fabd97bde2877a5d423ce94b74ed9a05 Mon Sep 17 00:00:00 2001 From: ElementBot <110224175+ElementBot@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:20:49 +0100 Subject: [PATCH 40/70] Sync Strings (#1623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync Strings from Localazy * Fix strings * Update screenshots --------- Co-authored-by: bmarty Co-authored-by: Jorge Martín --- .../src/main/res/values-sk/translations.xml | 26 +++++++++ .../src/main/res/values-sk/translations.xml | 4 ++ .../api/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-ru/translations.xml | 1 + .../src/main/res/values-sk/translations.xml | 3 ++ .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 4 +- .../src/main/res/values-ru/translations.xml | 5 ++ .../src/main/res/values-sk/translations.xml | 53 +++++++++++++++++++ .../main/res/values-zh-rTW/translations.xml | 1 + .../src/main/res/values/localazy.xml | 2 + ...ationHeader-D-3_3_null,NEXUS_5,1.0,en].png | 4 +- ...ationHeader-N-3_4_null,NEXUS_5,1.0,en].png | 4 +- ...mListView-D-2_2_null_1,NEXUS_5,1.0,en].png | 4 +- ...mListView-N-2_3_null_1,NEXUS_5,1.0,en].png | 4 +- 15 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 features/lockscreen/impl/src/main/res/values-sk/translations.xml diff --git a/features/lockscreen/impl/src/main/res/values-sk/translations.xml b/features/lockscreen/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..593542d81e --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,26 @@ + + + + "Nesprávny PIN kód. Máte ešte %1$d pokus" + "Nesprávny PIN kód. Máte ešte %1$d pokusy" + "Nesprávny PIN kód. Máte ešte %1$d pokusov" + + "Zabudli ste PIN?" + "Zmeniť PIN kód" + "Povoliť biometrické odomknutie" + "Odstrániť PIN" + "Ste si istí, že chcete odstrániť PIN?" + "Odstrániť PIN?" + "Vyberte PIN" + "Potvrdiť PIN" + "Z bezpečnostných dôvodov si nemôžete toto zvoliť ako svoj PIN kód." + "Vyberte iný PIN" + "Uzamknite %1$s, aby ste zvýšili bezpečnosť svojich konverzácií. + +Vyberte si niečo zapamätateľné. Ak tento kód PIN zabudnete, budete z aplikácie odhlásení." + "Zadajte prosím ten istý PIN dvakrát" + "PIN kódy sa nezhodujú" + "Ak chcete pokračovať, musíte sa znovu prihlásiť a vytvoriť nový PIN kód." + "Prebieha odhlasovanie" + "Máte 3 pokusy na odomknutie" + diff --git a/features/logout/api/src/main/res/values-sk/translations.xml b/features/logout/api/src/main/res/values-sk/translations.xml index 212f11ccbc..2c334e9c3b 100644 --- a/features/logout/api/src/main/res/values-sk/translations.xml +++ b/features/logout/api/src/main/res/values-sk/translations.xml @@ -1,8 +1,12 @@ + "Prosím, počkajte na dokončenie tohto kroku a až potom sa odhláste." + "Vaše kľúče sa ešte stále zálohujú" "Ste si istí, že sa chcete odhlásiť?" "Odhlásiť sa" "Prebieha odhlasovanie…" + "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam." + "Uložili ste kľúč na obnovenie?" "Odhlásiť sa" "Odhlásiť sa" diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/api/src/main/res/values/localazy.xml index 5a5c9c64ca..c695309194 100644 --- a/features/logout/api/src/main/res/values/localazy.xml +++ b/features/logout/api/src/main/res/values/localazy.xml @@ -6,7 +6,7 @@ "Sign out" "Signing out…" "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." - "Have you saved your recovery key?" + "Recovery not set up" "Sign out" "Sign out" diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index e54b92ff08..4bb44db372 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -38,5 +38,6 @@ "Не удалось отправить ваше сообщение" "Добавить эмодзи" "Показать меньше" + "Удерживайте для записи" "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." diff --git a/features/preferences/impl/src/main/res/values-sk/translations.xml b/features/preferences/impl/src/main/res/values-sk/translations.xml index 392999558e..2e1dc22b93 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,8 @@ + "Vlastná Element Call základná URL adresa" + "Nastaviť vlastnú základnú URL adresu pre Element Call." + "Neplatná adresa URL, uistite sa, že ste uviedli protokol (http/https) a správnu adresu." "Vývojársky režim" "Umožniť prístup k možnostiam a funkciám pre vývojárov." "Vypnite rozšírený textový editor na ručné písanie Markdown." diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index d63f96d07c..26b0bbee9e 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -4,6 +4,6 @@ "Get started by messaging someone." "No chats yet." "All Chats" - "Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards." + "Looks like you’re using a new device. Verify with another device to access your encrypted messages." "Verify it’s you" diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 7ada336f28..e5f46af0e6 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -207,9 +207,9 @@ "Další nastavení" "Halsové a video hovory" "Neshoda konfigurace" - "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. + "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. -Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. +Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. Pokud budete pokračovat, některá nastavení se mohou změnit." "Přímé zprávy" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index ada42316b1..64d233e598 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -1,13 +1,17 @@ + "Удалить" "Скрыть пароль" "Только упоминания" "Звук отключен" + "Приостановить" + "Воспроизвести" "Опрос" "Опрос завершен" "Отправить файлы" "Показать пароль" "Меню пользователя" + "Запишите голосовое сообщение. Дважды нажмите и удерживайте, чтобы записать. Отпустите, чтобы закончить запись." "Разрешить" "Добавить в хронологию" "Назад" @@ -90,6 +94,7 @@ "%1$s%2$s" "Шифрование включено" "Ошибка" + "Для всех" "Файл" "Файл сохранен в «Загрузки»" "Переслать сообщение" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index c6cf324b75..5cb20f8e22 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -5,6 +5,7 @@ "Iba zmienky" "Stlmené" "Pozastaviť" + "Pole PIN" "Prehrať" "Anketa" "Ukončená anketa" @@ -69,12 +70,15 @@ "Zdieľať" "Zdieľať odkaz" "Prihláste sa znova" + "Odhlásiť sa" + "Napriek tomu sa odhlásiť" "Preskočiť" "Spustiť" "Začať konverzáciu" "Spustiť overovanie" "Ťuknutím načítate mapu" "Urobiť fotku" + "Skúste to znova" "Zobraziť zdroj" "Áno" "Upraviť anketu" @@ -84,6 +88,7 @@ "Analytika" "Zvuk" "Bubliny" + "Záloha konverzácie" "Autorské práva" "Vytváranie miestnosti…" "Opustil/a miestnosť" @@ -93,6 +98,7 @@ "Upravuje sa" "* %1$s %2$s" "Šifrovanie zapnuté" + "Zadajte svoj PIN" "Chyba" "Všetci" "Súbor" @@ -122,6 +128,7 @@ "Zásady ochrany osobných údajov" "Reakcia" "Reakcie" + "Kľúč na obnovenie" "Obnovuje sa…" "Odpoveď na %1$s" "Nahlásiť chybu" @@ -129,6 +136,7 @@ "Rozšírený textový editor" "Názov miestnosti" "napr. názov vášho projektu" + "Zámok obrazovky" "Vyhľadať niekoho" "Výsledky hľadania" "Bezpečnosť" @@ -150,6 +158,7 @@ "Nie je možné dešifrovať" "Pozvánky nebolo možné odoslať jednému alebo viacerým používateľom." "Nie je možné odoslať pozvánku/ky" + "Odomknúť" "Zrušiť stlmenie zvuku" "Nepodporovaná udalosť" "Používateľské meno" @@ -174,8 +183,10 @@ "%1$s nedokázal načítať mapu. Skúste to prosím neskôr." "Načítanie správ zlyhalo" "%1$s nemohol získať prístup k vašej polohe. Skúste to prosím neskôr." + "Nepodarilo sa nahrať hlasovú správu." "%1$s nemá povolenie na prístup k vašej polohe. Prístup môžete zapnúť v Nastaveniach." "%1$s nemá povolenie na prístup k vašej polohe. Povoľte prístup nižšie." + "%1$s nemá povolenie na prístup k vášmu mikrofónu. Povoľte prístup na nahrávanie hlasovej správy." "Niektoré správy neboli odoslané" "Prepáčte, vyskytla sa chyba" "🔐️ Pripojte sa ku mne na %1$s" @@ -184,6 +195,11 @@ "Ste si istí, že chcete opustiť túto miestnosť? Táto miestnosť nie je verejná a bez pozvania sa do nej nebudete môcť vrátiť." "Ste si istí, že chcete opustiť miestnosť?" "%1$s Android" + + "%1$d zadaná číslica" + "%1$d zadané číslice" + "%1$d zadaných číslic" + "%1$d člen" "%1$d členovia" @@ -201,6 +217,22 @@ "Toto je začiatok tejto konverzácie." "Nové" "Zdieľať analytické údaje" + "Vypnúť zálohovanie" + "Zapnúť zálohovanie" + "Zálohovanie zaisťuje, že nestratíte históriu správ. %1$s." + "Zálohovanie" + "Zmeniť kľúč na obnovenie" + "Potvrdiť kľúč na obnovenie" + "Vaša záloha konverzácie nie je momentálne synchronizovaná." + "Nastaviť obnovovanie" + "Získajte prístup k vašim šifrovaným správam aj keď stratíte všetky svoje zariadenia alebo sa odhlásite zo všetkých %1$s zariadení." + "Vypnúť" + "Stratíte prístup k svojim zašifrovaným správam, ak sa odhlásite zo všetkých zariadení" + "Ste si istí, že chcete vypnúť zálohovanie?" + "Vypnutím zálohovania sa odstráni aktuálna záloha šifrovacích kľúčov a vypnú sa ďalšie bezpečnostné funkcie. V tomto prípade:" + "Na nových zariadeniach nebudete mať zašifrovanú históriu správ" + "Stratíte prístup k svojim zašifrovaným správam, ak sa odhlásite zo všetkých %1$s zariadení" + "Ste si istí, že chcete vypnúť zálohovanie?" "Nepodarilo sa vybrať médium, skúste to prosím znova." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa nahrať médiá, skúste to prosím znova." @@ -231,6 +263,27 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""nastavenia systému" "Systémové oznámenia sú vypnuté" "Oznámenia" + "Získajte nový kľúč na obnovenie, ak ste stratili svoj existujúci. Po zmene kľúča na obnovenie už starý kľúč nebude fungovať." + "Vygenerovať nový kľúč na obnovenie" + "Uistite sa, že kľúč na obnovenie môžete uložiť niekde v bezpečí" + "Kľúč na obnovenie bol zmenený" + "Zmeniť kľúč na obnovenie?" + "Zadajte kľúč na obnovenie a potvrďte prístup k zálohe konverzácie." + "Zadajte 48-znakový kód." + "Zadať…" + "Kľúč na obnovu potvrdený" + "Potvrďte kľúč na obnovenie" + "Uložiť kľúč na obnovenie" + "Zapíšte si kľúč na obnovenie na bezpečné miesto alebo ho uložte do správcu hesiel." + "Ťuknutím skopírujte kľúč na obnovenie" + "Uložte svoj kľúč na obnovenie" + "Po tomto kroku nebudete mať prístup k novému kľúču na obnovenie." + "Uložili ste kľúč na obnovenie?" + "Vaša záloha konverzácie je chránená kľúčom na obnovenie. Ak potrebujete nový kľúč na obnovenie, po nastavení si ho môžete znova vytvoriť výberom položky „Zmeniť kľúč na obnovenie“." + "Vygenerujte si váš kľúč na obnovenie" + "Uistite sa, že kľúč na obnovenie môžete uložiť niekde v bezpečí" + "Úspešné nastavenie obnovy" + "Nastaviť obnovenie" "Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa" "Zdieľať polohu" "Zdieľať moju polohu" diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index 71aa40e8fb..a87c7b50f3 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -25,6 +25,7 @@ "複製連結" "建立" "建立聊天室" + "拒絕" "停用" "完成" "編輯" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 2e2668c837..c70eceb21f 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -78,6 +78,7 @@ "Start verification" "Tap to load map" "Take photo" + "Try again" "View source" "Yes" "Edit poll" @@ -182,6 +183,7 @@ "%1$s could not load the map. Please try again later." "Failed loading messages" "%1$s could not access your location. Please try again later." + "Failed to upload your voice message." "%1$s does not have permission to access your location. You can enable access in Settings." "%1$s does not have permission to access your location. Enable access below." "%1$s does not have permission to access your microphone. Enable access to record a voice message." diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png index 5b4c6141ef..db9a827165 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf2f1ba80fa68e1a1277e00a90efd157b38f367cf264b1f75969067f0b71bd65 -size 28736 +oid sha256:dca845bd952e92a37f50aced5dd4d1dbe7dcaeccecade64cd1164a20efc65200 +size 26875 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png index d0b1ef9ac2..d11cf5060a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66111b25139ada1f03833b8c3532fcb138aed48644bd7ff8a6b7e9cca3d99f35 -size 28127 +oid sha256:a9c45cd3b849a04aba6813003632b8706d0e433ad6178b7eec616647f1b950ba +size 26039 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png index 0cdf38dc42..9561e126dc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e8a1cabb49befa77b2a89c20e83e2f8355242e8868ae075e7967c7a14ca870 -size 89054 +oid sha256:c02883f4a671a5c27da1fd5f3f635d7e23ed077aec2d47dc2ac6e0e2ced72f59 +size 86748 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png index 222acb9e53..10de4f6b65 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e4fdc7a1af79a5c2113f7c8f27f1c22463fbb3a3dbdddf83f7258ba513ea06a -size 90775 +oid sha256:2ad2f44c3fb1915ad6e226a973cecc3662c02bdf4dd8faf1eb3f056346fa1aca +size 88667 From 3a39e747fd038ca02b757b5f17dc59949853465a Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 23 Oct 2023 09:31:32 +0100 Subject: [PATCH 41/70] Fix issue where text is cleared when cancelling a reply (#1617) --- changelog.d/1617.bugfix | 1 + .../messages/impl/MessagesStateProvider.kt | 2 +- .../MessageComposerContextImpl.kt | 2 +- .../MessageComposerPresenter.kt | 10 +++--- .../MessageComposerStateProvider.kt | 2 +- .../MessageComposerPresenterTest.kt | 33 ++++++++++++++++--- .../test/MessageComposerContextFake.kt | 2 +- .../libraries/textcomposer/TextComposer.kt | 14 ++++---- .../textcomposer/components/SendButton.kt | 2 +- .../textcomposer/model/MessageComposerMode.kt | 2 +- 10 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 changelog.d/1617.bugfix diff --git a/changelog.d/1617.bugfix b/changelog.d/1617.bugfix new file mode 100644 index 0000000000..8beee812e5 --- /dev/null +++ b/changelog.d/1617.bugfix @@ -0,0 +1 @@ +Fix issue where text is cleared when cancelling a reply \ No newline at end of file diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 4222a0889d..249c4b487e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -59,7 +59,7 @@ fun aMessagesState() = MessagesState( composerState = aMessageComposerState().copy( richTextEditorState = RichTextEditorState("Hello", initialFocus = true), isFullScreen = false, - mode = MessageComposerMode.Normal("Hello"), + mode = MessageComposerMode.Normal, ), voiceMessageComposerState = aVoiceMessageComposerState(), timelineState = aTimelineState().copy( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt index c9a491d8a3..2353285499 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt @@ -29,6 +29,6 @@ import javax.inject.Inject @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class) class MessageComposerContextImpl @Inject constructor() : MessageComposerContext { - override var composerMode: MessageComposerMode by mutableStateOf(MessageComposerMode.Normal("")) + override var composerMode: MessageComposerMode by mutableStateOf(MessageComposerMode.Normal) internal set } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index e4a8d50cb8..3857600e58 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -155,10 +155,12 @@ class MessageComposerPresenter @Inject constructor( when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value MessageComposerEvents.CloseSpecialMode -> { - localCoroutineScope.launch { - richTextEditorState.setHtml("") + if (messageComposerContext.composerMode is MessageComposerMode.Edit) { + localCoroutineScope.launch { + richTextEditorState.setHtml("") + } } - messageComposerContext.composerMode = MessageComposerMode.Normal("") + messageComposerContext.composerMode = MessageComposerMode.Normal } is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage( message = event.message, @@ -253,7 +255,7 @@ class MessageComposerPresenter @Inject constructor( val capturedMode = messageComposerContext.composerMode // Reset composer right away richTextEditorState.setHtml("") - updateComposerMode(MessageComposerMode.Normal("")) + updateComposerMode(MessageComposerMode.Normal) when (capturedMode) { is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html) is MessageComposerMode.Edit -> { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index 7dbe413e83..76f40a1969 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -30,7 +30,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) { + private suspend fun ReceiveTurbine.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState { state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode) skipItems(skipCount) val normalState = awaitItem() - assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal("")) - assertThat(normalState.richTextEditorState.messageHtml).isEqualTo("") + assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal) + return normalState } private fun createPresenter( diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt index 0ae604004d..03af64e071 100644 --- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt @@ -20,5 +20,5 @@ import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.textcomposer.model.MessageComposerMode class MessageComposerContextFake( - override var composerMode: MessageComposerMode = MessageComposerMode.Normal(null) + override var composerMode: MessageComposerMode = MessageComposerMode.Normal ) : MessageComposerContext diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 18246f1ac4..911cebb142 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -483,7 +483,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { RichTextEditorState("", initialFocus = true), voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, onResetComposerMode = {}, enableTextFormatting = true, enableVoiceMessages = true, @@ -493,7 +493,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { RichTextEditorState("A message", initialFocus = true), voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, onResetComposerMode = {}, enableTextFormatting = true, enableVoiceMessages = true, @@ -506,7 +506,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { ), voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, onResetComposerMode = {}, enableTextFormatting = true, enableVoiceMessages = true, @@ -516,7 +516,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { RichTextEditorState("A message without focus", initialFocus = false), voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, onResetComposerMode = {}, enableTextFormatting = true, enableVoiceMessages = true, @@ -533,7 +533,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview { RichTextEditorState("", initialFocus = false), voiceMessageState = VoiceMessageState.Idle, showTextFormatting = true, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, enableTextFormatting = true, enableVoiceMessages = true, ) @@ -542,7 +542,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview { RichTextEditorState("A message", initialFocus = false), voiceMessageState = VoiceMessageState.Idle, showTextFormatting = true, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, enableTextFormatting = true, enableVoiceMessages = true, ) @@ -551,7 +551,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview { RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", initialFocus = false), voiceMessageState = VoiceMessageState.Idle, showTextFormatting = true, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, enableTextFormatting = true, enableVoiceMessages = true, ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt index 8dc1a4706b..be258f07de 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt @@ -93,7 +93,7 @@ internal fun SendButton( @PreviewsDayNight @Composable internal fun SendButtonPreview() = ElementPreview { - val normalMode = MessageComposerMode.Normal("") + val normalMode = MessageComposerMode.Normal val editMode = MessageComposerMode.Edit(null, "", null) Row { SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index 49ce0ddb6e..34ab1641f2 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -24,7 +24,7 @@ import kotlinx.parcelize.Parcelize sealed interface MessageComposerMode : Parcelable { @Parcelize - data class Normal(val content: CharSequence?) : MessageComposerMode + data object Normal: MessageComposerMode sealed class Special(open val eventId: EventId?, open val defaultContent: String) : MessageComposerMode From 32f9ddc44b54df3be355e1a6691bf2fa91d46b82 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 10:32:43 +0200 Subject: [PATCH 42/70] Pin : fix tests after rename --- .../impl/pin/DefaultPinCodeManager.kt | 2 +- .../lockscreen/impl/pin/PinCodeManager.kt | 2 +- ...senterTest.kt => SetupPinPresenterTest.kt} | 38 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/{CreatePinPresenterTest.kt => SetupPinPresenterTest.kt} (74%) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index f5848a9d40..e7529e9280 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -37,7 +37,7 @@ class DefaultPinCodeManager @Inject constructor( return pinCodeStore.hasPinCode() } - override suspend fun setupPinCode(pinCode: String) { + override suspend fun createPinCode(pinCode: String) { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() pinCodeStore.saveEncryptedPinCode(encryptedPinCode) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 09197c3eb1..5f84f5296d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -30,7 +30,7 @@ interface PinCodeManager { * Creates a new encrypted pin code. * @param pinCode the clear pin code to create */ - suspend fun setupPinCode(pinCode: String) + suspend fun createPinCode(pinCode: String) /** * @return true if the pin code is correct. diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt similarity index 74% rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 723b8e8e6b..d7529be243 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -22,14 +22,14 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry -import io.element.android.features.lockscreen.impl.setup.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.setup.validation.PinValidator +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest import org.junit.Test -class CreatePinPresenterTest { +class SetupPinPresenterTest { private val blacklistedPin = PinValidator.BLACKLIST.first() private val halfCompletePin = "12" @@ -39,58 +39,58 @@ class CreatePinPresenterTest { @Test fun `present - complete flow`() = runTest { - val presenter = createCreatePinPresenter() + val presenter = createSetupPinPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().also { state -> state.choosePinEntry.assertEmpty() state.confirmPinEntry.assertEmpty() - assertThat(state.createPinFailure).isNull() + assertThat(state.setupPinFailure).isNull() assertThat(state.isConfirmationStep).isFalse() - state.eventSink(CreatePinEvents.OnPinEntryChanged(halfCompletePin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(halfCompletePin)) } awaitItem().also { state -> state.choosePinEntry.assertText(halfCompletePin) state.confirmPinEntry.assertEmpty() - assertThat(state.createPinFailure).isNull() + assertThat(state.setupPinFailure).isNull() assertThat(state.isConfirmationStep).isFalse() - state.eventSink(CreatePinEvents.OnPinEntryChanged(blacklistedPin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(blacklistedPin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(blacklistedPin) - assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinBlacklisted) - state.eventSink(CreatePinEvents.ClearFailure) + assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.PinBlacklisted) + state.eventSink(SetupPinEvents.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() - assertThat(state.createPinFailure).isNull() - state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + assertThat(state.setupPinFailure).isNull() + state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() - state.eventSink(CreatePinEvents.OnPinEntryChanged(mismatchedPin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(mismatchedPin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertText(mismatchedPin) - assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinsDontMatch) - state.eventSink(CreatePinEvents.ClearFailure) + assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.PinsDontMatch) + state.eventSink(SetupPinEvents.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isFalse() - assertThat(state.createPinFailure).isNull() - state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + assertThat(state.setupPinFailure).isNull() + state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() - state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } awaitItem().also { state -> state.choosePinEntry.assertText(completePin) @@ -108,7 +108,7 @@ class CreatePinPresenterTest { assertThat(isEmpty).isTrue() } - private fun createCreatePinPresenter(): CreatePinPresenter { - return CreatePinPresenter(PinValidator(), aBuildMeta()) + private fun createSetupPinPresenter(): SetupPinPresenter { + return SetupPinPresenter(PinValidator(), aBuildMeta()) } } From ff56a51be54a8c7a22867981568424bb014fe947 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 10:34:49 +0200 Subject: [PATCH 43/70] PIN : fix warning --- .../features/lockscreen/impl/components/PinEntryTextField.kt | 2 +- .../android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index 5682e594fe..e4b6e6ac47 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -106,7 +106,7 @@ private fun PinDigitView( @PreviewsDayNight @Composable -fun PinEntryTextFieldPreview() { +internal fun PinEntryTextFieldPreview() { ElementTheme { PinEntryTextField( pinEntry = PinEntry.empty(4).fillWith("12"), diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index 8a6230c725..46b9ca1b82 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -199,7 +199,7 @@ private fun PinKeypadBackButton( @Composable @PreviewsDayNight -fun PinKeypadPreview() { +internal fun PinKeypadPreview() { ElementPreview { BoxWithConstraints { PinKeypad( From f6b9a8be9aa0af11a2b6d8f7d50fee1d43b5a4d2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 11:00:47 +0200 Subject: [PATCH 44/70] PIN: fix konsist --- .../android/features/lockscreen/impl/unlock/PinUnlockView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index be43d6cf32..e2cf522a07 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -126,7 +126,7 @@ fun PinUnlockView( } @Composable -fun PinUnlockCompactView( +private fun PinUnlockCompactView( header: @Composable () -> Unit, footer: @Composable () -> Unit, modifier: Modifier = Modifier, @@ -150,7 +150,7 @@ fun PinUnlockCompactView( } @Composable -fun PinUnlockExpandedView( +private fun PinUnlockExpandedView( header: @Composable () -> Unit, footer: @Composable () -> Unit, modifier: Modifier = Modifier, From f283594fbdf23cde9624dfa1d4b1fa85064938a9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 11:41:08 +0200 Subject: [PATCH 45/70] PIN : add test for SetupPinPresenter --- build.gradle.kts | 2 - features/lockscreen/impl/build.gradle.kts | 2 + .../lockscreen/impl/unlock/PinUnlockEvents.kt | 2 +- .../impl/unlock/PinUnlockPresenter.kt | 2 +- .../impl/pin/model/PinEntryAssertions.kt | 28 ++++++ .../impl/setup/SetupPinPresenterTest.kt | 11 +-- .../impl/unlock/PinUnlockPresenterTest.kt | 89 +++++++++++++++++++ 7 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 391313fe2d..f08c023b1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -250,8 +250,6 @@ 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.lockscreen.impl.unlock.PinUnlockPresenter$*" } bound { minValue = 85 diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 8daeb2178c..a3f5b27949 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -51,6 +51,8 @@ dependencies { testImplementation(projects.tests.testutils) testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) + testImplementation(projects.libraries.featureflag.test) + ksp(libs.showkase.processor) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index a90b6cb702..5560dedd24 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -20,6 +20,6 @@ import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel sealed interface PinUnlockEvents { data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents - data object Unlock : PinUnlockEvents data object OnForgetPin : PinUnlockEvents + data object ClearSignOutPrompt : PinUnlockEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index a6f06158d4..783e03c92a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -53,7 +53,6 @@ class PinUnlockPresenter @Inject constructor( fun handleEvents(event: PinUnlockEvents) { when (event) { - PinUnlockEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } is PinUnlockEvents.OnPinKeypadPressed -> { pinEntry = pinEntry.process(event.pinKeypadModel) if (pinEntry.isComplete()) { @@ -61,6 +60,7 @@ class PinUnlockPresenter @Inject constructor( } } PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true + PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false } } return PinUnlockState( diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt new file mode 100644 index 0000000000..37d54677a1 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt @@ -0,0 +1,28 @@ +/* + * 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.lockscreen.impl.pin.model + +import com.google.common.truth.Truth.assertThat + +fun PinEntry.assertText(text: String) { + assertThat(toText()).isEqualTo(text) +} + +fun PinEntry.assertEmpty() { + val isEmpty = digits.all { it is PinDigit.Empty } + assertThat(isEmpty).isTrue() +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index d7529be243..5d9901a9f1 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -22,6 +22,8 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.assertEmpty +import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.matrix.test.core.aBuildMeta @@ -99,15 +101,6 @@ class SetupPinPresenterTest { } } - private fun PinEntry.assertText(text: String) { - assertThat(toText()).isEqualTo(text) - } - - private fun PinEntry.assertEmpty() { - val isEmpty = digits.all { it is PinDigit.Empty } - assertThat(isEmpty).isTrue() - } - private fun createSetupPinPresenter(): SetupPinPresenter { return SetupPinPresenter(PinValidator(), aBuildMeta()) } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt new file mode 100644 index 0000000000..6839fe65c7 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -0,0 +1,89 @@ +/* + * 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.lockscreen.impl.unlock + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.pin.model.assertEmpty +import io.element.android.features.lockscreen.impl.pin.model.assertText +import io.element.android.features.lockscreen.impl.state.DefaultLockScreenStateService +import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PinUnlockPresenterTest { + + private val halfCompletePin = "12" + private val completePin = "1235" + + @Test + fun `present - complete flow`() = runTest { + val presenter = createPinUnlockPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().also { state -> + state.pinEntry.assertEmpty() + assertThat(state.showWrongPinTitle).isFalse() + assertThat(state.showSignOutPrompt).isFalse() + assertThat(state.remainingAttempts).isEqualTo(3) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) + } + awaitLastSequentialItem().also { state -> + state.pinEntry.assertText(halfCompletePin) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back)) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Empty)) + } + awaitLastSequentialItem().also { state -> + state.pinEntry.assertText(halfCompletePin) + state.eventSink(PinUnlockEvents.OnForgetPin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showSignOutPrompt).isEqualTo(true) + assertThat(state.isSignOutPromptCancellable).isEqualTo(true) + state.eventSink(PinUnlockEvents.ClearSignOutPrompt) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showSignOutPrompt).isEqualTo(false) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) + } + awaitLastSequentialItem().also { state -> + state.pinEntry.assertText(completePin) + } + } + } + + private suspend fun createPinUnlockPresenter(scope: CoroutineScope): PinUnlockPresenter { + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.PinUnlock, true) + } + val lockScreenStateService = DefaultLockScreenStateService(featureFlagService) + return PinUnlockPresenter( + lockScreenStateService, + scope, + ) + } +} From 577527902f3e9322236bff332478502cdfec0105 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 23 Oct 2023 11:57:04 +0200 Subject: [PATCH 46/70] Konsist: add test to ensure that functions with `@PreviewsDayNight` contain `ElementTheme` composable, and fix existing issues. --- .../lockscreen/impl/components/PinEntryTextField.kt | 3 ++- .../virtual/TimelineEncryptedHistoryBannerView.kt | 3 ++- .../libraries/textcomposer/TextComposerLinkDialog.kt | 7 ++++--- .../textcomposer/components/RecordingProgress.kt | 7 +++---- .../android/tests/konsist/KonsistPreviewTest.kt | 11 +++++++++++ 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index e4b6e6ac47..fceaf59f2a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.pinDigitBg import io.element.android.libraries.theme.ElementTheme @@ -107,7 +108,7 @@ private fun PinDigitView( @PreviewsDayNight @Composable internal fun PinEntryTextFieldPreview() { - ElementTheme { + ElementPreview { PinEntryTextField( pinEntry = PinEntry.empty(4).fillWith("12"), onValueChange = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt index 0317a68a00..2ef6c6580c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.R +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.utils.CommonDrawables @@ -61,7 +62,7 @@ fun TimelineEncryptedHistoryBannerView(modifier: Modifier = Modifier) { @PreviewsDayNight @Composable internal fun TimelineEncryptedHistoryBannerViewPreview() { - ElementTheme { + ElementPreview { TimelineEncryptedHistoryBannerView() } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt index f36e2ca11c..232e9ab0c1 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import io.element.android.libraries.designsystem.components.dialogs.ListDialog import io.element.android.libraries.designsystem.components.list.TextFieldListItem +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text @@ -200,7 +201,7 @@ private fun EditLinkDialog( @PreviewsDayNight @Composable -internal fun TextComposerLinkDialogCreateLinkPreview() { +internal fun TextComposerLinkDialogCreateLinkPreview() = ElementPreview { TextComposerLinkDialog( onDismissRequest = {}, linkAction = LinkAction.InsertLink, @@ -212,7 +213,7 @@ internal fun TextComposerLinkDialogCreateLinkPreview() { @PreviewsDayNight @Composable -internal fun TextComposerLinkDialogCreateLinkWithoutTextPreview() { +internal fun TextComposerLinkDialogCreateLinkWithoutTextPreview() = ElementPreview { TextComposerLinkDialog( onDismissRequest = {}, linkAction = LinkAction.SetLink(null), @@ -224,7 +225,7 @@ internal fun TextComposerLinkDialogCreateLinkWithoutTextPreview() { @PreviewsDayNight @Composable -internal fun TextComposerLinkDialogEditLinkPreview() { +internal fun TextComposerLinkDialogEditLinkPreview() = ElementPreview { TextComposerLinkDialog( onDismissRequest = {}, linkAction = LinkAction.SetLink("https://element.io"), diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt index db4f59342c..2fc0420e05 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +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 @@ -46,9 +47,7 @@ internal fun RecordingProgress( shape = MaterialTheme.shapes.medium, ) .padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp) - .heightIn(26.dp) - - , + .heightIn(26.dp), verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -69,6 +68,6 @@ internal fun RecordingProgress( @PreviewsDayNight @Composable -internal fun RecordingProgressPreview() { +internal fun RecordingProgressPreview() = ElementPreview { RecordingProgress() } diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 40c73c8eaa..7061d4ba55 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -35,4 +35,15 @@ class KonsistPreviewTest { it.hasNameEndingWith("DarkPreview").not() } } + + @Test + fun `Functions with '@PreviewsDayNight' annotation should contain 'ElementPreview' composable`() { + Konsist + .scopeFromProject() + .functions() + .withAllAnnotationsOf(PreviewsDayNight::class) + .assertTrue { + it.text.contains("ElementPreview") + } + } } From e49c0c46ebd77fd56157b90d53c8ffac65cd550a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 23 Oct 2023 12:00:01 +0200 Subject: [PATCH 47/70] Konsist: add test to ensure that functions with `@PreviewsDayNight` are internal, and fix existing issues. --- .../android/tests/konsist/KonsistPreviewTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 7061d4ba55..f82f4dac7c 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -46,4 +46,15 @@ class KonsistPreviewTest { it.text.contains("ElementPreview") } } + + @Test + fun `Functions with '@PreviewsDayNight' are internal`() { + Konsist + .scopeFromProject() + .functions() + .withAllAnnotationsOf(PreviewsDayNight::class) + .assertTrue { + it.hasInternalModifier + } + } } From 3763593a668c766856626c48e3c7ecd321bdafe6 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 23 Oct 2023 12:06:52 +0100 Subject: [PATCH 48/70] Fix emoji shortcut values --- .../android/features/messages/impl/actionlist/ActionListView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index 05db97d3be..34d87201a2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -348,7 +348,7 @@ private fun EmojiReactionsRow( ) { // TODO use most recently used emojis here when available from the Rust SDK val defaultEmojis = sequenceOf( - "👍", "👎", "🔥", "❤️", "👏" + "👍️", "👎️", "🔥", "❤️", "👏" ) for (emoji in defaultEmojis) { val isHighlighted = highlightedEmojis.contains(emoji) From bf8cf00c51f916c6a696a8b3ba3cc576e113f633 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 23 Oct 2023 14:01:23 +0200 Subject: [PATCH 49/70] Remove unused import --- .../features/lockscreen/impl/setup/SetupPinPresenterTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 5d9901a9f1..94877fbff5 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,8 +20,6 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.pin.model.PinDigit -import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator From 6de6b6a8be911acf65cd9f3e1f4137a07d884c60 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 23 Oct 2023 12:11:23 +0000 Subject: [PATCH 50/70] Update screenshots --- ...ionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png index ac13aa8b52..385f874f07 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e7684929f93f437c50549b309e1ba75b95cddb506ad9c8ef97373d0552bddfb -size 39017 +oid sha256:1997fc442ccd0abe76d0fdcde639e8224e339f8b93cc3703543b39ec641edcf8 +size 37486 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png index 18aaab9fd1..44fb2cdf99 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56f71535fd676a86e79b0e987701d34c85c96276e5338f77117b13e1b3a05144 -size 45715 +oid sha256:72976810fdc5da4e2182db9ddcd1972a3d5f58510a2083a58513441cce02579e +size 43960 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png index 9098eef49b..cbf0e90093 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5554661aeb29ec0d95464942da03df5e83dc78762bc08e2f57851a8f7e811a5a -size 39438 +oid sha256:fc9f27f9d0068e1010eccbc776af4106bdb95811f6c3bdd3d13de26eea254b22 +size 37902 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png index 0c6cb3114f..483ae0043a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d398070330d5c66358b7ac17d4e1b93795fb9001ffc6eabe533d3bae86de7d58 -size 39810 +oid sha256:0e271ef60b51973bdb283f656f6c2bbfbb8bbfc723f15128e04e0ff1123dc763 +size 38256 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png index 74448d6967..d65f7aaa8a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:979138753000ada021a67f4bc14a89b912430c4331ac69aa702b2e50c050d7cb -size 41200 +oid sha256:207ee896047bed185f6110dd032924c9ed3ffa40a58ca6c2e829797e3074d460 +size 39600 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png index 2e051be73c..fd436c9971 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb4be22a56036d53f7c394c3829a9f8f09820276d7529ace0d3731965a8a92f9 -size 37556 +oid sha256:9a82276d1fa57bbed2850244886071df37b6441d7842acef4ecefd6a161404d7 +size 36106 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png index ea837fc372..125bcd6f79 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:369e8b4e3f741476cfa5ff7e3f9d5d1e1399fcf3494fadb95540129436ef4d68 -size 43744 +oid sha256:440aa47c7c00d1957a800b245de79b9b9b9a570de7024a0da211df681effc5f3 +size 42340 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png index 1c028e2b60..8aecf8543a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cd8fe02340aeb0edb116c0f3d8f07efc1d331849688718782db25e17bb6e1ab -size 37766 +oid sha256:6c595a14f273e1ffbda9ecbe499acccb49d4ee5ff32aaa3f8d5cfee0f7cf86d2 +size 36277 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png index cb01f9579d..23ebba41cf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b2aaa54bf065e6496957c3a60d61336f65e6b76f97e5dbc773284ee49fac91d -size 37988 +oid sha256:babf4bbf8494b67b000177fca06fb8817f35ba5b5851cd85e729ad1d70a9bd95 +size 36515 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png index 8d45efac8c..ac459d9d58 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:752abe37a810cc8e4b36ae5f4c4aa38c9f22757b6458f272ba991050c69aee53 -size 39409 +oid sha256:1845e027e45d15aadc4be9c579568a791623adf25ff166ffb1595333b4535833 +size 37953 From d21623e523967b752c064c0abac6480b81545bbf Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 15:30:17 +0200 Subject: [PATCH 51/70] Pin: add LockScreenConfig and address PR reviews --- .../android/appconfig/LockScreenConfig.kt | 35 +++++++++++++++++++ features/lockscreen/impl/build.gradle.kts | 5 ++- .../impl/components/PinEntryTextField.kt | 2 +- .../lockscreen/impl/pin/model/PinEntry.kt | 4 +-- .../impl/setup/SetupPinPresenter.kt | 15 ++++---- .../impl/setup/SetupPinStateProvider.kt | 14 ++++---- .../impl/setup/validation/PinValidator.kt | 9 ++--- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 2 +- .../impl/unlock/PinUnlockPresenter.kt | 8 +++-- .../impl/unlock/PinUnlockStateProvider.kt | 4 +-- .../lockscreen/impl/unlock/PinUnlockView.kt | 2 +- .../unlock/{numpad => keypad}/PinKeypad.kt | 2 +- .../{numpad => keypad}/PinKeypadModel.kt | 2 +- .../impl/setup/SetupPinPresenterTest.kt | 3 +- .../impl/unlock/PinUnlockPresenterTest.kt | 2 +- 15 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/{numpad => keypad}/PinKeypad.kt (99%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/{numpad => keypad}/PinKeypadModel.kt (92%) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt new file mode 100644 index 0000000000..5930f53428 --- /dev/null +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -0,0 +1,35 @@ +/* + * 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.appconfig + +object LockScreenConfig { + + /** + * Whether the LockScreen is mandatory or not. + */ + const val IS_MANDATORY: Boolean = false + + /** + * Some PINs are blacklisted. + */ + val PIN_BLACKLIST = listOf("0000", "1234") + + /** + * The size of the PIN. + */ + const val PIN_SIZE = 4 +} diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index a3f5b27949..a3657ccff3 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -30,9 +30,11 @@ anvil { } dependencies { + ksp(libs.showkase.processor) implementation(projects.anvilannotations) anvil(projects.anvilcodegen) api(projects.features.lockscreen.api) + implementation(projects.appconfig) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) @@ -52,7 +54,4 @@ dependencies { testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) testImplementation(projects.libraries.featureflag.test) - - - ksp(libs.showkase.processor) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index fceaf59f2a..91f6d435c5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -110,7 +110,7 @@ private fun PinDigitView( internal fun PinEntryTextFieldPreview() { ElementPreview { PinEntryTextField( - pinEntry = PinEntry.empty(4).fillWith("12"), + pinEntry = PinEntry.createEmpty(4).fillWith("12"), onValueChange = {}, ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index 92dda869a6..eaca592de9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -24,7 +24,7 @@ data class PinEntry( ) { companion object { - fun empty(size: Int): PinEntry { + fun createEmpty(size: Int): PinEntry { val digits = List(size) { PinDigit.Empty } return PinEntry( digits = digits.toPersistentList() @@ -69,7 +69,7 @@ data class PinEntry( } fun clear(): PinEntry { - return fillWith("") + return createEmpty(size) } fun isComplete(): Boolean { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 6a880a6967..3c380e6be7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -21,15 +21,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry -import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.features.lockscreen.impl.setup.validation.PinValidator +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import javax.inject.Inject -private const val PIN_SIZE = 4 - class SetupPinPresenter @Inject constructor( private val pinValidator: PinValidator, private val buildMeta: BuildMeta, @@ -38,10 +37,10 @@ class SetupPinPresenter @Inject constructor( @Composable override fun present(): SetupPinState { var choosePinEntry by remember { - mutableStateOf(PinEntry.empty(PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) } var confirmPinEntry by remember { - mutableStateOf(PinEntry.empty(PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) } var isConfirmationStep by remember { mutableStateOf(false) @@ -77,11 +76,11 @@ class SetupPinPresenter @Inject constructor( SetupPinEvents.ClearFailure -> { when (setupPinFailure) { is SetupPinFailure.PinsDontMatch -> { - choosePinEntry = PinEntry.empty(PIN_SIZE) - confirmPinEntry = PinEntry.empty(PIN_SIZE) + choosePinEntry = choosePinEntry.clear() + confirmPinEntry = confirmPinEntry.clear() } is SetupPinFailure.PinBlacklisted -> { - choosePinEntry = PinEntry.empty(PIN_SIZE) + choosePinEntry = choosePinEntry.clear() } null -> Unit } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt index 1a177b4a83..bb0a46d10c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt @@ -25,20 +25,20 @@ open class SetupPinStateProvider : PreviewParameterProvider { get() = sequenceOf( aSetupPinState(), aSetupPinState( - choosePinEntry = PinEntry.empty(4).fillWith("12") + choosePinEntry = PinEntry.createEmpty(4).fillWith("12") ), aSetupPinState( - choosePinEntry = PinEntry.empty(4).fillWith("1789"), + choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"), isConfirmationStep = true, ), aSetupPinState( - choosePinEntry = PinEntry.empty(4).fillWith("1789"), - confirmPinEntry = PinEntry.empty(4).fillWith("1788"), + choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"), + confirmPinEntry = PinEntry.createEmpty(4).fillWith("1788"), isConfirmationStep = true, creationFailure = SetupPinFailure.PinsDontMatch ), aSetupPinState( - choosePinEntry = PinEntry.empty(4).fillWith("1111"), + choosePinEntry = PinEntry.createEmpty(4).fillWith("1111"), creationFailure = SetupPinFailure.PinBlacklisted ), @@ -46,8 +46,8 @@ open class SetupPinStateProvider : PreviewParameterProvider { } fun aSetupPinState( - choosePinEntry: PinEntry = PinEntry.empty(4), - confirmPinEntry: PinEntry = PinEntry.empty(4), + choosePinEntry: PinEntry = PinEntry.createEmpty(4), + confirmPinEntry: PinEntry = PinEntry.createEmpty(4), isConfirmationStep: Boolean = false, creationFailure: SetupPinFailure? = null, ) = SetupPinState( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index c7435120aa..20ad023b1c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -16,17 +16,12 @@ package io.element.android.features.lockscreen.impl.setup.validation -import androidx.annotation.VisibleForTesting +import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject class PinValidator @Inject constructor() { - companion object { - @VisibleForTesting - val BLACKLIST = listOf("0000", "1234") - } - sealed interface Result { data object Valid : Result data class Invalid(val failure: SetupPinFailure) : Result @@ -34,7 +29,7 @@ class PinValidator @Inject constructor() { fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() - val isBlacklisted = BLACKLIST.any { it == pinAsText } + val isBlacklisted = LockScreenConfig.PIN_BLACKLIST.any { it == pinAsText } return if (isBlacklisted) { Result.Invalid(SetupPinFailure.PinBlacklisted) } else { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index 5560dedd24..30ee16df02 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -16,7 +16,7 @@ package io.element.android.features.lockscreen.impl.unlock -import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel sealed interface PinUnlockEvents { data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 783e03c92a..e189a2ab39 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -23,9 +23,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.features.lockscreen.impl.pin.model.PinEntry -import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -39,9 +40,11 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { var pinEntry by remember { - mutableStateOf(PinEntry.empty(4)) + //TODO fetch size from db + mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) } var remainingAttempts by rememberSaveable { + //TODO fetch from db mutableIntStateOf(3) } var showWrongPinTitle by rememberSaveable { @@ -56,6 +59,7 @@ class PinUnlockPresenter @Inject constructor( is PinUnlockEvents.OnPinKeypadPressed -> { pinEntry = pinEntry.process(event.pinKeypadModel) if (pinEntry.isComplete()) { + //TODO check pin with PinCodeManager coroutineScope.launch { pinStateService.unlock() } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 4f269d2f5a..8ddc942e25 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -23,7 +23,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPinUnlockState(), - aPinUnlockState(pinEntry = PinEntry.empty(4).fillWith("12")), + aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")), aPinUnlockState(showWrongPinTitle = true), aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), @@ -31,7 +31,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider { } fun aPinUnlockState( - pinEntry: PinEntry = PinEntry.empty(4), + pinEntry: PinEntry = PinEntry.createEmpty(4), remainingAttempts: Int = 3, showWrongPinTitle: Boolean = false, showSignOutPrompt: Boolean = false, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index e2cf522a07..37cb591007 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry -import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt similarity index 99% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt index 46b9ca1b82..1056718dd0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.unlock.numpad +package io.element.android.features.lockscreen.impl.unlock.keypad import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt similarity index 92% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt index f1430dcaa5..8d232cb21b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.unlock.numpad +package io.element.android.features.lockscreen.impl.unlock.keypad import androidx.compose.runtime.Immutable diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 94877fbff5..21005cc722 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator @@ -31,7 +32,7 @@ import org.junit.Test class SetupPinPresenterTest { - private val blacklistedPin = PinValidator.BLACKLIST.first() + private val blacklistedPin = LockScreenConfig.PIN_BLACKLIST private val halfCompletePin = "12" private val completePin = "1235" private val mismatchedPin = "1236" diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index 6839fe65c7..02919edce0 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.state.DefaultLockScreenStateService -import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.tests.testutils.awaitLastSequentialItem From d66bd4a4599930fad96a51db4e5d9cc54a8c9e9a Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 15:38:24 +0200 Subject: [PATCH 52/70] PIN unlock : adjust ui a bit --- .../android/features/lockscreen/impl/unlock/PinUnlockView.kt | 4 +++- .../features/lockscreen/impl/unlock/keypad/PinKeypad.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 37cb591007..5769f42b35 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -78,7 +78,9 @@ fun PinUnlockView( ) } val footer = @Composable { - PinUnlockFooter() + PinUnlockFooter( + modifier = Modifier.padding(top = 24.dp) + ) } val content = @Composable { constraints: BoxWithConstraintsScope -> PinKeypad( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt index 1056718dd0..9db5cfe11a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt @@ -49,7 +49,7 @@ import io.element.android.libraries.theme.ElementTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -private val spaceBetweenPinKey = 8.dp +private val spaceBetweenPinKey = 16.dp private val maxSizePinKey = 80.dp @Composable From 733b9c4ab1838044a83ea455172cde5e6b8ab236 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 23 Oct 2023 13:49:14 +0000 Subject: [PATCH 53/70] Update screenshots --- ...nlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png | 3 +++ ...nlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png | 3 +++ ...nlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png | 3 --- ...nlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png | 3 --- ...unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png | 4 ++-- 14 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dbb5213ce6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbb48906ee7f1f18034cd7c64766790689f95f4ea4c0310a98430921b3792aee +size 30854 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4a018f0927 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ec32849337493132ab88ebc7270728e3c568896e40b0f451b633c924591cbaa +size 28579 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index fc54ab5f7e..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b14ff41b74e36809f359a7b3b73ffdb9c0c2cd97ec786333bcce9412f05825e6 -size 30608 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 62b7c73a26..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1521d9fecaaadcfc7ed4670d997b36a20bd2a84f564794a06cb10bfe7d1ad64e -size 28812 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png index e81bbf1c1d..c508164493 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c369763c4be676d6727a6b0f5cd74d56a9de09b9c3780e1806874d19ba7ffd3f -size 42705 +oid sha256:c94f10441359bdb901ee1f8be9c307312586d6ca54495e88ec231c1e931eb36b +size 38994 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png index bcb90d6b4a..07c06a033c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a63439f320a1b6ad124cb943f95651f85d2e9cfc34597cb450c803b6dafaefa7 -size 43149 +oid sha256:38456a511fbc72e3d544c8f88138be4f24b6adb3a10629989c037141213a9e19 +size 39441 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png index bf2823f6a4..4a8df974cd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:165e37ef78a7b09bbd49e8d692ccb27836e35d8535b182d9837a5c3daa5cf3a6 -size 43978 +oid sha256:083c3972e5d019ce1d8565a655bd6f6fb6689e17792ddf7ed282d5eee529a7b6 +size 40292 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png index b3ca08978d..02570ec381 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecaba43c72a890d85f9894eb987f291967c8ea15bdc09db88d6a72384098d827 -size 48308 +oid sha256:bda49a721fbd19b4cbe0f4dc1b4013154a55e17f78e099f411d1caaef82cfa6c +size 46711 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png index 5b8c62dcbf..ee9f61a453 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25175242be4133a74541f0bb2d05db95484a65da952031fb8f68a2fdef88a995 -size 45880 +oid sha256:28d2e1634cc0bbf01f6efc2b260625abb26f2105bf1e858f2bd196830d70854c +size 44278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png index fc34314f30..c7864da5cc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:699c96cc0d06c85eeda3a3b408877d37445bebf1873a86d6255ba8626ba410b0 -size 39654 +oid sha256:ab8c04689414b5183fa6ab5a479063b478b3d02dfb6bf2df72b1b606c77392ad +size 36278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png index 6d029a3034..5d49a0e78a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:233bb8cb261f752ac868c492d075fea9e5af656fcc8ad0d12fbf05b2dd690788 -size 40113 +oid sha256:fda961a2240c1458ad561c8f4e7fa265bbe6c70cf9b0fbfcd1ae0879036a3343 +size 36714 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png index af181f44e5..b386f26669 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:920b9f28c3d2f5e726ad4beeb668e75c285b0e731e6d7eaaf87ca3b2a8b9980b -size 40652 +oid sha256:0cb889338b21dd8e1a913348ff02cd217f48840c4d54b99ed4d89b67dff771bb +size 37254 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png index 2616989dcd..39854067a2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28197831dacc61f25d6b3a0080db331ef317283e8032fa5cc4ed457709da0f89 -size 42980 +oid sha256:7b0011f773767d2806b2269281415f910b4770bc0e95dff2a2a29a2c46f9e458 +size 41488 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png index ce7df11d83..bb647daed5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:652154d82c5f43f91cface3f2e5650ee5a98cf1faed08a80589ab42225bd8120 -size 40557 +oid sha256:d50c34cdf50881d8176d90617c717f7214faeaa4fdce22ec4b93b0e5669b9869 +size 39073 From bf88fa55ddc1bd95340988bcb1bbbc96273e3a85 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 16:03:11 +0200 Subject: [PATCH 54/70] PIN : fix tests with new LockScreenConfig --- .../io/element/android/appconfig/LockScreenConfig.kt | 6 +++--- .../lockscreen/impl/setup/validation/PinValidator.kt | 7 +++++-- .../lockscreen/impl/setup/SetupPinPresenterTest.kt | 5 ++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index 5930f53428..9427a1f9c7 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -19,14 +19,14 @@ package io.element.android.appconfig object LockScreenConfig { /** - * Whether the LockScreen is mandatory or not. + * Whether the PIN is mandatory or not. */ - const val IS_MANDATORY: Boolean = false + const val IS_PIN_MANDATORY: Boolean = false /** * Some PINs are blacklisted. */ - val PIN_BLACKLIST = listOf("0000", "1234") + val PIN_BLACKLIST = setOf("0000", "1234") /** * The size of the PIN. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index 20ad023b1c..b164ee8c88 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -20,7 +20,10 @@ import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject -class PinValidator @Inject constructor() { +class PinValidator internal constructor(private val pinBlacklist: Set) { + + @Inject + constructor() : this(LockScreenConfig.PIN_BLACKLIST) sealed interface Result { data object Valid : Result @@ -29,7 +32,7 @@ class PinValidator @Inject constructor() { fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() - val isBlacklisted = LockScreenConfig.PIN_BLACKLIST.any { it == pinAsText } + val isBlacklisted = pinBlacklist.any { it == pinAsText } return if (isBlacklisted) { Result.Invalid(SetupPinFailure.PinBlacklisted) } else { diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 21005cc722..ff797b52f4 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,7 +20,6 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator @@ -32,7 +31,7 @@ import org.junit.Test class SetupPinPresenterTest { - private val blacklistedPin = LockScreenConfig.PIN_BLACKLIST + private val blacklistedPin = "1234" private val halfCompletePin = "12" private val completePin = "1235" private val mismatchedPin = "1236" @@ -101,6 +100,6 @@ class SetupPinPresenterTest { } private fun createSetupPinPresenter(): SetupPinPresenter { - return SetupPinPresenter(PinValidator(), aBuildMeta()) + return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta()) } } From a0c192266f39e1ccbf04a63c83080b3928dcd51b Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 23 Oct 2023 15:25:28 +0100 Subject: [PATCH 55/70] Update thumbs up emoji in the state provider. --- .../features/messages/impl/timeline/TimelineStateProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 1374f4aef4..2955c6783f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -139,7 +139,7 @@ fun aTimelineItemReactions( count: Int = 1, isHighlighted: Boolean = false, ): TimelineItemReactions { - val emojis = arrayOf("👍", "😀️", "😁️", "😆️", "😅️", "🤣️", "🥰️", "😇️", "😊️", "😉️", "🙃️", "🙂️", "😍️", "🤗️", "🤭️") + val emojis = arrayOf("👍️", "😀️", "😁️", "😆️", "😅️", "🤣️", "🥰️", "😇️", "😊️", "😉️", "🙃️", "🙂️", "😍️", "🤗️", "🤭️") return TimelineItemReactions( reactions = buildList { repeat(count) { index -> From f721c988eeafa20be5a04cf9f2535e218b24b16f Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 23 Oct 2023 14:39:14 +0000 Subject: [PATCH 56/70] Update screenshots --- ...ionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png index 385f874f07..ac13aa8b52 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1997fc442ccd0abe76d0fdcde639e8224e339f8b93cc3703543b39ec641edcf8 -size 37486 +oid sha256:2e7684929f93f437c50549b309e1ba75b95cddb506ad9c8ef97373d0552bddfb +size 39017 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png index 44fb2cdf99..18aaab9fd1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72976810fdc5da4e2182db9ddcd1972a3d5f58510a2083a58513441cce02579e -size 43960 +oid sha256:56f71535fd676a86e79b0e987701d34c85c96276e5338f77117b13e1b3a05144 +size 45715 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png index cbf0e90093..9098eef49b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc9f27f9d0068e1010eccbc776af4106bdb95811f6c3bdd3d13de26eea254b22 -size 37902 +oid sha256:5554661aeb29ec0d95464942da03df5e83dc78762bc08e2f57851a8f7e811a5a +size 39438 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png index 483ae0043a..0c6cb3114f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e271ef60b51973bdb283f656f6c2bbfbb8bbfc723f15128e04e0ff1123dc763 -size 38256 +oid sha256:d398070330d5c66358b7ac17d4e1b93795fb9001ffc6eabe533d3bae86de7d58 +size 39810 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png index d65f7aaa8a..74448d6967 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:207ee896047bed185f6110dd032924c9ed3ffa40a58ca6c2e829797e3074d460 -size 39600 +oid sha256:979138753000ada021a67f4bc14a89b912430c4331ac69aa702b2e50c050d7cb +size 41200 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png index fd436c9971..2e051be73c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a82276d1fa57bbed2850244886071df37b6441d7842acef4ecefd6a161404d7 -size 36106 +oid sha256:cb4be22a56036d53f7c394c3829a9f8f09820276d7529ace0d3731965a8a92f9 +size 37556 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png index 125bcd6f79..ea837fc372 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:440aa47c7c00d1957a800b245de79b9b9b9a570de7024a0da211df681effc5f3 -size 42340 +oid sha256:369e8b4e3f741476cfa5ff7e3f9d5d1e1399fcf3494fadb95540129436ef4d68 +size 43744 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png index 8aecf8543a..1c028e2b60 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c595a14f273e1ffbda9ecbe499acccb49d4ee5ff32aaa3f8d5cfee0f7cf86d2 -size 36277 +oid sha256:5cd8fe02340aeb0edb116c0f3d8f07efc1d331849688718782db25e17bb6e1ab +size 37766 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png index 23ebba41cf..cb01f9579d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:babf4bbf8494b67b000177fca06fb8817f35ba5b5851cd85e729ad1d70a9bd95 -size 36515 +oid sha256:9b2aaa54bf065e6496957c3a60d61336f65e6b76f97e5dbc773284ee49fac91d +size 37988 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png index ac459d9d58..8d45efac8c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1845e027e45d15aadc4be9c579568a791623adf25ff166ffb1595333b4535833 -size 37953 +oid sha256:752abe37a810cc8e4b36ae5f4c4aa38c9f22757b6458f272ba991050c69aee53 +size 39409 From 5c582bba1b684987c1dec14fd6f988e2171e4569 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 23 Oct 2023 18:28:00 +0100 Subject: [PATCH 57/70] Record and send voice messages (#1596) --------- Co-authored-by: ElementBot --- changelog.d/1596.feature | 1 + features/messages/impl/build.gradle.kts | 2 + .../impl/src/main/AndroidManifest.xml | 20 ++ .../messages/impl/MessagesPresenter.kt | 3 + .../features/messages/impl/MessagesState.kt | 1 + .../messages/impl/MessagesStateProvider.kt | 1 + .../features/messages/impl/MessagesView.kt | 19 + .../messagecomposer/MessageComposerView.kt | 9 +- .../VoiceMessageComposerEvents.kt | 5 + .../VoiceMessageComposerPresenter.kt | 156 ++++++++- .../VoiceMessageComposerState.kt | 1 + .../VoiceMessageComposerStateProvider.kt | 3 +- .../voicemessages/VoiceMessageException.kt | 26 ++ .../VoiceMessagePermissionRationaleDialog.kt | 37 ++ .../messages/MessagesPresenterTest.kt | 17 +- .../VoiceMessageComposerPresenterTest.kt | 327 +++++++++++++++++- gradle/libs.versions.toml | 2 + .../android/libraries/core/hash/Hash.kt | 35 ++ .../libraries/mediaupload/api/MediaSender.kt | 54 ++- .../mediaupload/api/MediaUploadInfo.kt | 1 + .../mediaupload/AndroidMediaPreProcessor.kt | 1 + .../mediaupload/test/FakeMediaPreProcessor.kt | 18 + .../api/PermissionsStateProvider.kt | 5 +- .../libraries/textcomposer/TextComposer.kt | 43 ++- ...dingProgress.kt => VoiceMessagePreview.kt} | 21 +- .../components/VoiceMessageRecording.kt | 102 ++++++ .../textcomposer/model/VoiceMessageState.kt | 6 +- libraries/voicerecorder/api/build.gradle.kts | 32 ++ .../voicerecorder/api/VoiceRecorder.kt | 55 +++ .../voicerecorder/api/VoiceRecorderState.kt | 44 +++ libraries/voicerecorder/impl/build.gradle.kts | 48 +++ .../voicerecorder/impl/VoiceRecorderImpl.kt | 132 +++++++ .../impl/audio/AndroidAudioReader.kt | 139 ++++++++ .../voicerecorder/impl/audio/Audio.kt | 28 ++ .../voicerecorder/impl/audio/AudioConfig.kt | 35 ++ .../impl/audio/AudioLevelCalculator.kt | 28 ++ .../voicerecorder/impl/audio/AudioReader.kt | 37 ++ .../impl/audio/DecibelAudioLevelCalculator.kt | 49 +++ .../impl/audio/DefaultEncoder.kt | 63 ++++ .../voicerecorder/impl/audio/Encoder.kt | 28 ++ .../voicerecorder/impl/audio/SampleRate.kt | 24 ++ .../impl/di/VoiceRecorderModule.kt | 58 ++++ .../impl/file/DefaultVoiceFileManager.kt | 49 +++ .../impl/file/VoiceFileConfig.kt | 30 ++ .../impl/file/VoiceFileManager.kt | 25 ++ .../impl/VoiceRecorderImplTest.kt | 134 +++++++ .../audio/DecibelAudioLevelCalculatorTest.kt | 46 +++ .../test/FakeAudioLevelCalculator.kt | 26 ++ .../voicerecorder/test/FakeAudioReader.kt | 49 +++ .../test/FakeAudioRecorderFactory.kt | 30 ++ .../voicerecorder/test/FakeEncoder.kt | 40 +++ .../voicerecorder/test/FakeFileSystem.kt | 43 +++ .../test/FakeVoiceFileManager.kt | 37 ++ libraries/voicerecorder/test/build.gradle.kts | 30 ++ .../voicerecorder/test/FakeVoiceRecorder.kt | 74 ++++ .../kotlin/extension/DependencyHandleScope.kt | 1 + ...ViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png | 4 +- ...ViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png | 4 +- ...gProgress-D-11_11_null,NEXUS_5,1.0,en].png | 3 - ...gProgress-N-11_12_null,NEXUS_5,1.0,en].png | 3 - ...ndButton-D-11_11_null,NEXUS_5,1.0,en].png} | 0 ...ndButton-N-11_12_null,NEXUS_5,1.0,en].png} | 0 ...rmatting-D-12_12_null,NEXUS_5,1.0,en].png} | 0 ...rmatting-N-12_13_null,NEXUS_5,1.0,en].png} | 0 ...gePreview-D-13_13_null,NEXUS_5,1.0,en].png | 3 + ...gePreview-N-13_14_null,NEXUS_5,1.0,en].png | 3 + ...Recording-D-14_14_null,NEXUS_5,1.0,en].png | 3 + ...Recording-N-14_15_null,NEXUS_5,1.0,en].png | 3 + 68 files changed, 2274 insertions(+), 82 deletions(-) create mode 100644 changelog.d/1596.feature create mode 100644 features/messages/impl/src/main/AndroidManifest.xml create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessagePermissionRationaleDialog.kt create mode 100644 libraries/core/src/main/kotlin/io/element/android/libraries/core/hash/Hash.kt rename libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/{RecordingProgress.kt => VoiceMessagePreview.kt} (75%) create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt create mode 100644 libraries/voicerecorder/api/build.gradle.kts create mode 100644 libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorder.kt create mode 100644 libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt create mode 100644 libraries/voicerecorder/impl/build.gradle.kts create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Audio.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioConfig.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioReader.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Encoder.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/SampleRate.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileConfig.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileManager.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioRecorderFactory.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeEncoder.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeFileSystem.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceFileManager.kt create mode 100644 libraries/voicerecorder/test/build.gradle.kts create mode 100644 libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-D-11_11_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-N-11_12_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_SendButton-D-11_11_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_SendButton-N-11_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_TextFormatting-D-12_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_TextFormatting-N-12_13_null,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-13_13_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-13_14_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-14_14_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-14_15_null,NEXUS_5,1.0,en].png diff --git a/changelog.d/1596.feature b/changelog.d/1596.feature new file mode 100644 index 0000000000..5108d6008b --- /dev/null +++ b/changelog.d/1596.feature @@ -0,0 +1 @@ +Record and send voice messages \ No newline at end of file diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 956b80949a..8ee3adb13c 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.voicerecorder.api) implementation(projects.features.networkmonitor.api) implementation(projects.services.analytics.api) implementation(libs.coil.compose) @@ -80,6 +81,7 @@ dependencies { testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.textcomposer.test) + testImplementation(projects.libraries.voicerecorder.test) testImplementation(libs.test.mockk) ksp(libs.showkase.processor) diff --git a/features/messages/impl/src/main/AndroidManifest.xml b/features/messages/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a00e8e1873 --- /dev/null +++ b/features/messages/impl/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 6125e920b8..cf15db5bd9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -63,6 +63,7 @@ import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -101,6 +102,7 @@ class MessagesPresenter @AssistedInject constructor( private val preferencesStore: PreferencesStore, private val featureFlagsService: FeatureFlagService, @Assisted private val navigator: MessagesNavigator, + private val buildMeta: BuildMeta, ) : Presenter { @AssistedFactory @@ -203,6 +205,7 @@ class MessagesPresenter @AssistedInject constructor( enableTextFormatting = enableTextFormatting, enableVoiceMessages = enableVoiceMessages, enableInRoomCalls = enableInRoomCalls, + appName = buildMeta.applicationName, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 3a0585f390..81feec4b63 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -50,5 +50,6 @@ data class MessagesState( val enableTextFormatting: Boolean, val enableVoiceMessages: Boolean, val enableInRoomCalls: Boolean, + val appName: String, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 249c4b487e..ae279f6d49 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -86,5 +86,6 @@ fun aMessagesState() = MessagesState( enableTextFormatting = true, enableVoiceMessages = true, enableInRoomCalls = true, + appName = "Element", eventSink = {} ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 5a7168e7ce..6dd91aa01c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -63,6 +63,8 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerEvents +import io.element.android.features.messages.impl.voicemessages.VoiceMessagePermissionRationaleDialog import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.libraries.androidutils.ui.hideKeyboard import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule @@ -83,6 +85,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.LogCompositions +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.UserId @@ -107,6 +110,10 @@ fun MessagesView( ) { LogCompositions(tag = "MessagesScreen", msg = "Root") + OnLifecycleEvent { _, event -> + state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event)) + } + AttachmentStateView( state = state.composerState.attachmentsState, onPreviewAttachments = onPreviewAttachments, @@ -306,6 +313,18 @@ private fun MessagesViewContent( enableTextFormatting = state.enableTextFormatting, ) + if (state.enableVoiceMessages && state.voiceMessageComposerState.showPermissionRationaleDialog) { + VoiceMessagePermissionRationaleDialog( + onContinue = { + state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale) + }, + onDismiss = { + state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale) + }, + appName = state.appName + ) + } + ExpandableBottomSheetScaffold( sheetDragHandle = if (state.composerState.showTextFormatting) { @Composable { BottomSheetDragHandle() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 150ae23f9b..8f3899f139 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -71,10 +71,14 @@ internal fun MessageComposerView( } } - fun onVoiceRecordButtonEvent(press: PressEvent) { + val onVoiceRecordButtonEvent = { press: PressEvent -> voiceMessageState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(press)) } + fun onSendVoiceMessage() { + voiceMessageState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + } + TextComposer( modifier = modifier, state = state.richTextEditorState, @@ -89,7 +93,8 @@ internal fun MessageComposerView( onDismissTextFormatting = ::onDismissTextFormatting, enableTextFormatting = enableTextFormatting, enableVoiceMessages = enableVoiceMessages, - onVoiceRecordButtonEvent = ::onVoiceRecordButtonEvent, + onVoiceRecordButtonEvent = onVoiceRecordButtonEvent, + onSendVoiceMessage = ::onSendVoiceMessage, onError = ::onError, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt index 7d6803fc41..42ba0d1d07 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt @@ -16,10 +16,15 @@ package io.element.android.features.messages.impl.voicemessages +import androidx.lifecycle.Lifecycle import io.element.android.libraries.textcomposer.model.PressEvent sealed interface VoiceMessageComposerEvents { data class RecordButtonEvent( val pressEvent: PressEvent ): VoiceMessageComposerEvents + data object SendVoiceMessage: VoiceMessageComposerEvents + data object AcceptPermissionRationale: VoiceMessageComposerEvents + data object DismissPermissionsRationale: VoiceMessageComposerEvents + data class LifecycleEvent(val event: Lifecycle.Event): VoiceMessageComposerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt index 106125934b..78e7d0ceb8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt @@ -16,49 +16,171 @@ package io.element.android.features.messages.impl.voicemessages +import android.Manifest import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.core.net.toUri +import androidx.lifecycle.Lifecycle import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.mediaupload.api.MediaSender +import io.element.android.libraries.permissions.api.PermissionsEvents +import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.textcomposer.model.PressEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState +import io.element.android.libraries.voicerecorder.api.VoiceRecorder +import io.element.android.libraries.voicerecorder.api.VoiceRecorderState +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File import javax.inject.Inject @SingleIn(RoomScope::class) -class VoiceMessageComposerPresenter @Inject constructor() : Presenter { +class VoiceMessageComposerPresenter @Inject constructor( + private val appCoroutineScope: CoroutineScope, + private val voiceRecorder: VoiceRecorder, + private val analyticsService: AnalyticsService, + private val mediaSender: MediaSender, + permissionsPresenterFactory: PermissionsPresenter.Factory +) : Presenter { + private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO) + @Composable override fun present(): VoiceMessageComposerState { - var voiceMessageState by remember { mutableStateOf(VoiceMessageState.Idle) } + val localCoroutineScope = rememberCoroutineScope() + val recorderState by voiceRecorder.state.collectAsState(initial = VoiceRecorderState.Idle) - fun onRecordButtonPress(event: VoiceMessageComposerEvents.RecordButtonEvent) = when(event.pressEvent) { - PressEvent.PressStart -> { - // TODO start the recording - voiceMessageState = VoiceMessageState.Recording - } - PressEvent.LongPressEnd -> { - // TODO finish the recording - voiceMessageState = VoiceMessageState.Idle - } - PressEvent.Tapped -> { - // TODO discard the recording and show the 'hold to record' tooltip - voiceMessageState = VoiceMessageState.Idle + val permissionState = permissionsPresenter.present() + var isSending by remember { mutableStateOf(false) } + + val onLifecycleEvent = { event: Lifecycle.Event -> + when (event) { + Lifecycle.Event.ON_PAUSE -> { + appCoroutineScope.finishRecording() + } + Lifecycle.Event.ON_DESTROY -> { + appCoroutineScope.cancelRecording() + } + else -> {} } } + val onRecordButtonPress = { event: VoiceMessageComposerEvents.RecordButtonEvent -> + val permissionGranted = permissionState.permissionGranted + when (event.pressEvent) { + PressEvent.PressStart -> { + Timber.v("Voice message record button pressed") + when { + permissionGranted -> { + localCoroutineScope.startRecording() + } + else -> { + Timber.i("Voice message permission needed") + permissionState.eventSink(PermissionsEvents.RequestPermissions) + } + } + } + PressEvent.LongPressEnd -> { + Timber.v("Voice message record button released") + localCoroutineScope.finishRecording() + } + PressEvent.Tapped -> { + Timber.v("Voice message record button tapped") + localCoroutineScope.cancelRecording() + } + } + } - fun handleEvents(event: VoiceMessageComposerEvents) { + val onAcceptPermissionsRationale = { + permissionState.eventSink(PermissionsEvents.OpenSystemSettingAndCloseDialog) + } + + val onDismissPermissionsRationale = { + permissionState.eventSink(PermissionsEvents.CloseDialog) + } + + val onSendButtonPress = lambda@{ + val finishedState = recorderState as? VoiceRecorderState.Finished + if (finishedState == null) { + val exception = VoiceMessageException.FileException("No file to send") + analyticsService.trackError(exception) + Timber.e(exception) + return@lambda + } + if (isSending) { + return@lambda + } + isSending = true + appCoroutineScope.sendMessage( + file = finishedState.file, + mimeType = finishedState.mimeType, + ).invokeOnCompletion { + isSending = false + } + } + + val handleEvents: (VoiceMessageComposerEvents) -> Unit = { event -> when (event) { is VoiceMessageComposerEvents.RecordButtonEvent -> onRecordButtonPress(event) + is VoiceMessageComposerEvents.SendVoiceMessage -> localCoroutineScope.launch { + onSendButtonPress() + } + VoiceMessageComposerEvents.DismissPermissionsRationale -> onDismissPermissionsRationale() + VoiceMessageComposerEvents.AcceptPermissionRationale -> onAcceptPermissionsRationale() + is VoiceMessageComposerEvents.LifecycleEvent -> onLifecycleEvent(event.event) } } return VoiceMessageComposerState( - voiceMessageState = voiceMessageState, - eventSink = { handleEvents(it) } + voiceMessageState = when (val state = recorderState) { + is VoiceRecorderState.Recording -> VoiceMessageState.Recording(level = state.level) + is VoiceRecorderState.Finished -> VoiceMessageState.Preview + else -> VoiceMessageState.Idle + }, + showPermissionRationaleDialog = permissionState.showDialog, + eventSink = handleEvents, ) } + + private fun CoroutineScope.startRecording() = launch { + try { + voiceRecorder.startRecord() + } catch (e: SecurityException) { + Timber.e(e, "Voice message error") + analyticsService.trackError(VoiceMessageException.PermissionMissing("Expected permission to record but none", e)) + } + } + + private fun CoroutineScope.finishRecording() = launch { + voiceRecorder.stopRecord() + } + + private fun CoroutineScope.cancelRecording() = launch { + voiceRecorder.stopRecord(cancelled = true) + } + + private fun CoroutineScope.sendMessage( + file: File, mimeType: String, + ) = launch { + val result = mediaSender.sendVoiceMessage( + uri = file.toUri(), + mimeType = mimeType, + waveForm = emptyList(), // TODO generate waveform + ) + + if (result.isFailure) { + Timber.e(result.exceptionOrNull(), "Voice message error") + return@launch + } + + voiceRecorder.deleteRecording() + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt index bacbe76324..8f0ab827b5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState @Stable data class VoiceMessageComposerState( val voiceMessageState: VoiceMessageState, + val showPermissionRationaleDialog: Boolean, val eventSink: (VoiceMessageComposerEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt index 63b59596c0..1a904beee3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording), + aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(level = 0.5)), ) } @@ -30,5 +30,6 @@ internal fun aVoiceMessageComposerState( voiceMessageState: VoiceMessageState = VoiceMessageState.Idle, ) = VoiceMessageComposerState( voiceMessageState = voiceMessageState, + showPermissionRationaleDialog = false, eventSink = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt new file mode 100644 index 0000000000..2020b687ae --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.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.messages.impl.voicemessages + +internal sealed class VoiceMessageException : Exception() { + data class FileException( + override val message: String?, override val cause: Throwable? = null + ) : VoiceMessageException() + data class PermissionMissing( + override val message: String?, override val cause: Throwable? + ) : VoiceMessageException() +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessagePermissionRationaleDialog.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessagePermissionRationaleDialog.kt new file mode 100644 index 0000000000..19b7f7cb46 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessagePermissionRationaleDialog.kt @@ -0,0 +1,37 @@ +/* + * 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.messages.impl.voicemessages + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun VoiceMessagePermissionRationaleDialog( + onContinue: () -> Unit, + onDismiss: () -> Unit, + appName: String, +) { + ConfirmationDialog( + content = stringResource(CommonStrings.error_missing_microphone_voice_rationale_android, appName), + onSubmitClicked = onContinue, + onDismiss = onDismiss, + submitText = stringResource(CommonStrings.action_continue), + cancelText = stringResource(CommonStrings.action_cancel), + ) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index b02afd90fb..b0958a5d2d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -66,6 +66,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.mediapickers.test.FakePickerProvider @@ -75,6 +76,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate @@ -607,20 +609,28 @@ class MessagesPresenterTest { analyticsService: FakeAnalyticsService = FakeAnalyticsService(), permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), ): MessagesPresenter { + val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) + val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) val messageComposerPresenter = MessageComposerPresenter( appCoroutineScope = this, room = matrixRoom, mediaPickerProvider = FakePickerProvider(), featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.NotificationSettings.key to true)), localMediaFactory = FakeLocalMediaFactory(mockMediaUrl), - mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom), + mediaSender = mediaSender, snackbarDispatcher = SnackbarDispatcher(), analyticsService = analyticsService, messageComposerContext = MessageComposerContextImpl(), richTextEditorStateFactory = TestRichTextEditorStateFactory(), - permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter), + permissionsPresenterFactory = permissionsPresenterFactory, + ) + val voiceMessageComposerPresenter = VoiceMessageComposerPresenter( + this, + FakeVoiceRecorder(), + analyticsService, + mediaSender, + permissionsPresenterFactory, ) - val voiceMessageComposerPresenter = VoiceMessageComposerPresenter() val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, @@ -649,6 +659,7 @@ class MessagesPresenterTest { clipboardHelper = clipboardHelper, preferencesStore = preferencesStore, featureFlagsService = FakeFeatureFlagService(), + buildMeta = aBuildMeta(), dispatchers = coroutineDispatchers, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt index 008226bf05..d1ee074e46 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt @@ -18,16 +18,31 @@ package io.element.android.features.messages.voicemessages +import android.Manifest +import androidx.lifecycle.Lifecycle import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerEvents import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerPresenter +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerState +import io.element.android.features.messages.impl.voicemessages.VoiceMessageException +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.mediaupload.api.MediaSender +import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.aPermissionsState +import io.element.android.libraries.permissions.test.FakePermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.textcomposer.model.PressEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState +import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder +import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -37,53 +52,349 @@ class VoiceMessageComposerPresenterTest { @get:Rule val warmUpRule = WarmUpRule() + private val voiceRecorder = FakeVoiceRecorder() + private val analyticsService = FakeAnalyticsService() + private val matrixRoom = FakeMatrixRoom() + private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() } + private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom) + @Test fun `present - initial state`() = runTest { - val presenter = createPresenter() + val presenter = createVoiceMessageComposerPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + + testPauseAndDestroy(initialState) } } @Test fun `present - recording state`() = runTest { - val presenter = createPresenter() + val presenter = createVoiceMessageComposerPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Recording) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(0.2)) + + testPauseAndDestroy(finalState) } } @Test fun `present - abort recording`() = runTest { - val presenter = createPresenter() + val presenter = createVoiceMessageComposerPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.Tapped)) - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + + testPauseAndDestroy(finalState) } } @Test fun `present - finish recording`() = runTest { - val presenter = createPresenter() + val presenter = createVoiceMessageComposerPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Preview) + + testPauseAndDestroy(finalState) } } - private fun createPresenter() = VoiceMessageComposerPresenter() + + @Test + fun `present - send recording`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(matrixRoom.sendMediaCount).isEqualTo(1) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - send recording before previous completed, waits`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().run { + eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + } + + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(matrixRoom.sendMediaCount).isEqualTo(1) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - send failures aren't tracked`() = runTest { + // Let sending fail due to media preprocessing error + mediaPreProcessor.givenResult(Result.failure(Exception())) + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + + val finalState = awaitItem().apply { + assertThat(voiceMessageState).isEqualTo(VoiceMessageState.Preview) + eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + } + assertThat(matrixRoom.sendMediaCount).isEqualTo(0) + assertThat(analyticsService.trackedErrors).hasSize(0) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - send failures can be retried`() = runTest { + // Let sending fail due to media preprocessing error + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + mediaPreProcessor.givenResult(Result.failure(Exception())) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + val previewState = awaitItem() + + previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + + ensureAllEventsConsumed() + assertThat(previewState.voiceMessageState).isEqualTo(VoiceMessageState.Preview) + assertThat(matrixRoom.sendMediaCount).isEqualTo(0) + + mediaPreProcessor.givenAudioResult() + previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(matrixRoom.sendMediaCount).isEqualTo(1) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - send error - missing recording is tracked`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Send the message before recording anything + initialState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + + assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(matrixRoom.sendMediaCount).isEqualTo(0) + assertThat(analyticsService.trackedErrors).hasSize(1) + + testPauseAndDestroy(initialState) + } + } + + @Test + fun `present - record error - security exceptions are tracked`() = runTest { + val exception = SecurityException("") + voiceRecorder.givenThrowsSecurityException(exception) + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + + assertThat(matrixRoom.sendMediaCount).isEqualTo(0) + assertThat(analyticsService.trackedErrors).containsExactly( + VoiceMessageException.PermissionMissing(message = "Expected permission to record but none", cause = exception) + ) + + testPauseAndDestroy(initialState) + } + } + + @Test + fun `present - permission accepted first time`() = runTest { + val permissionsPresenter = createFakePermissionsPresenter( + recordPermissionGranted = false, + ) + val presenter = createVoiceMessageComposerPresenter( + permissionsPresenter = permissionsPresenter, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + + initialState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + permissionsPresenter.setPermissionGranted() + + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(0.2)) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - permission denied previously`() = runTest { + val permissionsPresenter = createFakePermissionsPresenter( + recordPermissionGranted = false, + ) + val presenter = createVoiceMessageComposerPresenter( + permissionsPresenter = permissionsPresenter, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + + // See the dialog and accept it + awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(it.showPermissionRationaleDialog).isTrue() + it.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale) + } + + // Dialog is hidden, user accepts permissions + assertThat(awaitItem().showPermissionRationaleDialog).isFalse() + + permissionsPresenter.setPermissionGranted() + + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(0.2)) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - permission rationale dismissed`() = runTest { + val permissionsPresenter = createFakePermissionsPresenter( + recordPermissionGranted = false, + ) + val presenter = createVoiceMessageComposerPresenter( + permissionsPresenter = permissionsPresenter, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + + // See the dialog and accept it + awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(it.showPermissionRationaleDialog).isTrue() + it.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale) + } + + // Dialog is hidden, user tries to record again + awaitItem().also { + assertThat(it.showPermissionRationaleDialog).isFalse() + it.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + } + + // Dialog is shown once again + val finalState = awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(it.showPermissionRationaleDialog).isTrue() + } + + testPauseAndDestroy(finalState) + } + } + + private suspend fun TurbineTestContext.testPauseAndDestroy( + mostRecentState: VoiceMessageComposerState, + ) { + mostRecentState.eventSink( + VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE) + ) + + val onPauseState = when (mostRecentState.voiceMessageState) { + VoiceMessageState.Idle, + VoiceMessageState.Preview -> { + mostRecentState + } + is VoiceMessageState.Recording -> { + awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Preview) + } + } + } + + onPauseState.eventSink( + VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY) + ) + + when (onPauseState.voiceMessageState) { + VoiceMessageState.Idle -> + ensureAllEventsConsumed() + is VoiceMessageState.Recording, + VoiceMessageState.Preview -> + assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + } + } + + private fun TestScope.createVoiceMessageComposerPresenter( + permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(), + ): VoiceMessageComposerPresenter { + return VoiceMessageComposerPresenter( + this, + voiceRecorder, + analyticsService, + mediaSender, + FakePermissionsPresenterFactory(permissionsPresenter), + ) + } + + private fun createFakePermissionsPresenter( + recordPermissionGranted: Boolean = true, + recordPermissionShowDialog: Boolean = false, + ): FakePermissionsPresenter { + val initialPermissionState = aPermissionsState( + showDialog = recordPermissionShowDialog, + permission = Manifest.permission.RECORD_AUDIO, + permissionGranted = recordPermissionGranted, + ) + return FakePermissionsPresenter( + initialState = initialPermissionState + ) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c86738fa0..7c3afb48dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -75,6 +75,7 @@ google_firebase_bom = "com.google.firebase:firebase-bom:32.4.0" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" } +androidx_annotationjvm = { module = "androidx.annotation:annotation-jvm", version = "1.7.0" } androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx_exifinterface = "androidx.exifinterface:exifinterface:1.3.6" @@ -164,6 +165,7 @@ statemachine = "com.freeletics.flowredux:compose:1.2.0" maplibre = "org.maplibre.gl:android-sdk:10.2.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.1" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1" +opusencoder = "io.element.android:opusencoder:1.1.0" # Analytics posthog = "com.posthog.android:posthog:2.0.3" diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/hash/Hash.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/hash/Hash.kt new file mode 100644 index 0000000000..760431a7be --- /dev/null +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/hash/Hash.kt @@ -0,0 +1,35 @@ +/* + * 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.libraries.core.hash + +import java.security.MessageDigest +import java.util.Locale + +/** + * Compute a Hash of a String, using md5 algorithm. + */ +fun String.md5() = try { + val digest = MessageDigest.getInstance("md5") + val locale = Locale.ROOT + digest.update(toByteArray()) + digest.digest() + .joinToString("") { String.format(locale, "%02X", it) } + .lowercase(locale) +} catch (exc: Exception) { + // Should not happen, but just in case + hashCode().toString() +} diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 899e92efc5..dde62e7513 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -50,16 +50,43 @@ class MediaSender @Inject constructor( .flatMapCatching { info -> room.sendMedia(info, progressCallback) } - .onFailure { error -> - val job = ongoingUploadJobs.remove(Job) - if (error !is CancellationException) { - job?.cancel() - } - } - .onSuccess { - ongoingUploadJobs.remove(Job) - } + .handleSendResult() } + suspend fun sendVoiceMessage( + uri: Uri, + mimeType: String, + waveForm: List, + progressCallback: ProgressCallback? = null + ): Result { + return preProcessor + .process( + uri = uri, + mimeType = mimeType, + deleteOriginal = true, + compressIfPossible = false + ) + .flatMapCatching { info -> + val audioInfo = (info as MediaUploadInfo.Audio).audioInfo + val newInfo = MediaUploadInfo.VoiceMessage( + file = info.file, + audioInfo = audioInfo, + waveform = waveForm, + ) + room.sendMedia(newInfo, progressCallback) + } + .handleSendResult() + } + + private fun Result.handleSendResult() = this + .onFailure { error -> + val job = ongoingUploadJobs.remove(Job) + if (error !is CancellationException) { + job?.cancel() + } + } + .onSuccess { + ongoingUploadJobs.remove(Job) + } private suspend fun MatrixRoom.sendMedia( uploadInfo: MediaUploadInfo, @@ -90,7 +117,14 @@ class MediaSender @Inject constructor( progressCallback = progressCallback ) } - + is MediaUploadInfo.VoiceMessage -> { + sendVoiceMessage( + file = uploadInfo.file, + audioInfo = uploadInfo.audioInfo, + waveform = uploadInfo.waveform, + progressCallback = progressCallback + ) + } is MediaUploadInfo.AnyFile -> { sendFile( file = uploadInfo.file, diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt index 51f6372b23..e1debf6bda 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt @@ -29,5 +29,6 @@ sealed interface MediaUploadInfo { data class Image(override val file: File, val imageInfo: ImageInfo, val thumbnailFile: File) : MediaUploadInfo data class Video(override val file: File, val videoInfo: VideoInfo, val thumbnailFile: File) : MediaUploadInfo data class Audio(override val file: File, val audioInfo: AudioInfo) : MediaUploadInfo + data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List) : MediaUploadInfo data class AnyFile(override val file: File, val fileInfo: FileInfo) : MediaUploadInfo } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index cd968530f8..205be3b241 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -118,6 +118,7 @@ class AndroidMediaPreProcessor @Inject constructor( is MediaUploadInfo.Audio -> copy(file = renamedFile) is MediaUploadInfo.Image -> copy(file = renamedFile) is MediaUploadInfo.Video -> copy(file = renamedFile) + is MediaUploadInfo.VoiceMessage -> copy(file = renamedFile) } } diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt index d94414d2d7..8e7e71e8fb 100644 --- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt +++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt @@ -17,11 +17,14 @@ package io.element.android.libraries.mediaupload.test import android.net.Uri +import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.tests.testutils.simulateLongTask import java.io.File +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration class FakeMediaPreProcessor : MediaPreProcessor { @@ -53,4 +56,19 @@ class FakeMediaPreProcessor : MediaPreProcessor { fun givenResult(value: Result) { this.result = value } + + fun givenAudioResult() { + givenResult( + Result.success( + MediaUploadInfo.Audio( + file = File("audio.ogg"), + audioInfo = AudioInfo( + duration = 1000.seconds.toJavaDuration(), + size = 1000, + mimetype = "audio/ogg", + ), + ) + ) + ) + } } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt index cc59d96b44..19797b9075 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt @@ -31,10 +31,11 @@ open class PermissionsStateProvider : PreviewParameterProvider fun aPermissionsState( showDialog: Boolean, - permission: String = Manifest.permission.POST_NOTIFICATIONS + permission: String = Manifest.permission.POST_NOTIFICATIONS, + permissionGranted: Boolean = false, ) = PermissionsState( permission = permission, - permissionGranted = false, + permissionGranted = permissionGranted, shouldShowRationale = false, showDialog = showDialog, permissionAlreadyAsked = false, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 911cebb142..7924077394 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -64,7 +64,8 @@ import io.element.android.libraries.testtags.testTag import io.element.android.libraries.textcomposer.components.ComposerOptionsButton import io.element.android.libraries.textcomposer.components.DismissTextFormattingButton import io.element.android.libraries.textcomposer.components.RecordButton -import io.element.android.libraries.textcomposer.components.RecordingProgress +import io.element.android.libraries.textcomposer.components.VoiceMessagePreview +import io.element.android.libraries.textcomposer.components.VoiceMessageRecording import io.element.android.libraries.textcomposer.components.SendButton import io.element.android.libraries.textcomposer.components.TextFormatting import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape @@ -95,6 +96,7 @@ fun TextComposer( onAddAttachment: () -> Unit = {}, onDismissTextFormatting: () -> Unit = {}, onVoiceRecordButtonEvent: (PressEvent) -> Unit = {}, + onSendVoiceMessage: () -> Unit = {}, onError: (Throwable) -> Unit = {}, ) { val onSendClicked = { @@ -137,24 +139,39 @@ fun TextComposer( composerMode = composerMode, ) } - val recordButton = @Composable { + val recordVoiceButton = @Composable { RecordButton( onPressStart = { onVoiceRecordButtonEvent(PressEvent.PressStart) }, onLongPressEnd = { onVoiceRecordButtonEvent(PressEvent.LongPressEnd) }, onTap = { onVoiceRecordButtonEvent(PressEvent.Tapped) }, ) } + val sendVoiceButton = @Composable { + SendButton( + canSendMessage = voiceMessageState is VoiceMessageState.Preview, + onClick = { onSendVoiceMessage() }, + composerMode = composerMode, + ) + } val textFormattingOptions = @Composable { TextFormatting(state = state) } - val sendOrRecordButton = if (canSendMessage || !enableVoiceMessages) { - sendButton - } else { - recordButton + val sendOrRecordButton = when { + enableVoiceMessages && !canSendMessage -> + when (voiceMessageState) { + is VoiceMessageState.Preview -> sendVoiceButton + else -> recordVoiceButton + } + else -> + sendButton } - val recordingProgress = @Composable { - RecordingProgress() + val voiceRecording = @Composable { + if (voiceMessageState is VoiceMessageState.Recording) { + VoiceMessageRecording(voiceMessageState.level) + } else if (voiceMessageState is VoiceMessageState.Preview) { + VoiceMessagePreview() + } } if (showTextFormatting) { @@ -170,11 +187,12 @@ fun TextComposer( } else { StandardLayout( voiceMessageState = voiceMessageState, + enableVoiceMessages = enableVoiceMessages, modifier = layoutModifier, composerOptionsButton = composerOptionsButton, textInput = textInput, endButton = sendOrRecordButton, - recordingProgress = recordingProgress, + voiceRecording = voiceRecording, ) } @@ -190,9 +208,10 @@ fun TextComposer( @Composable private fun StandardLayout( voiceMessageState: VoiceMessageState, + enableVoiceMessages: Boolean, textInput: @Composable () -> Unit, composerOptionsButton: @Composable () -> Unit, - recordingProgress: @Composable () -> Unit, + voiceRecording: @Composable () -> Unit, endButton: @Composable () -> Unit, modifier: Modifier = Modifier, ) { @@ -200,13 +219,13 @@ private fun StandardLayout( modifier = modifier, verticalAlignment = Alignment.Bottom, ) { - if (voiceMessageState is VoiceMessageState.Recording) { + if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) { Box( modifier = Modifier .padding(start = 16.dp, bottom = 8.dp, top = 8.dp) .weight(1f) ) { - recordingProgress() + voiceRecording() } } else { Box( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt similarity index 75% rename from libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index 2fc0420e05..351293a329 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -17,14 +17,10 @@ package io.element.android.libraries.textcomposer.components import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -36,7 +32,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme @Composable -internal fun RecordingProgress( +internal fun VoiceMessagePreview( modifier: Modifier = Modifier, ) { Row( @@ -50,16 +46,9 @@ internal fun RecordingProgress( .heightIn(26.dp), verticalAlignment = Alignment.CenterVertically, ) { - Box( - modifier = Modifier - .size(8.dp) - .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) - ) - Spacer(Modifier.size(8.dp)) - - // TODO Replace with timer UI + // TODO Replace with recording preview UI Text( - text = "Recording...", // Not localized because it is a placeholder + text = "Finished recording", // Not localized because it is a placeholder color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodySmMedium ) @@ -68,6 +57,6 @@ internal fun RecordingProgress( @PreviewsDayNight @Composable -internal fun RecordingProgressPreview() = ElementPreview { - RecordingProgress() +internal fun VoiceMessagePreviewPreview() = ElementPreview { + VoiceMessagePreview() } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt new file mode 100644 index 0000000000..24703a579c --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -0,0 +1,102 @@ +/* + * 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.libraries.textcomposer.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +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 + +@Composable +internal fun VoiceMessageRecording( + level: Double, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = ElementTheme.colors.bgSubtleSecondary, + shape = MaterialTheme.shapes.medium, + ) + .padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp) + .heightIn(26.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(8.dp) + .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) + ) + Spacer(Modifier.size(8.dp)) + + // TODO Replace with timer UI + Text( + text = "Recording...", // Not localized because it is a placeholder + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodySmMedium + ) + + Spacer(Modifier.size(20.dp)) + + // TODO Replace with waveform UI + DebugAudioLevel( + modifier = Modifier.weight(1f), level = level + ) + } +} + +@Composable +private fun DebugAudioLevel( + level: Double, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .height(26.dp) + ) { + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .fillMaxWidth(level.toFloat()) + .background(ElementTheme.colors.iconQuaternary, shape = MaterialTheme.shapes.small) + .fillMaxHeight() + ) + } +} + +@PreviewsDayNight +@Composable +internal fun VoiceMessageRecordingPreview() = ElementPreview { + VoiceMessageRecording(0.5) +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index d376c4ee70..835000478a 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -18,5 +18,9 @@ package io.element.android.libraries.textcomposer.model sealed class VoiceMessageState { data object Idle: VoiceMessageState() - data object Recording: VoiceMessageState() + + data object Preview: VoiceMessageState() + data class Recording( + val level: Double, + ): VoiceMessageState() } diff --git a/libraries/voicerecorder/api/build.gradle.kts b/libraries/voicerecorder/api/build.gradle.kts new file mode 100644 index 0000000000..bed69b7d28 --- /dev/null +++ b/libraries/voicerecorder/api/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * 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") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.voicerecorder.api" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.androidx.annotationjvm) + implementation(libs.coroutines.core) +} diff --git a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorder.kt b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorder.kt new file mode 100644 index 0000000000..77465ddeea --- /dev/null +++ b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorder.kt @@ -0,0 +1,55 @@ +/* + * 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.libraries.voicerecorder.api + +import android.Manifest +import androidx.annotation.RequiresPermission +import kotlinx.coroutines.flow.StateFlow + +/** + * Audio recorder which records audio to opus/ogg files. + */ +interface VoiceRecorder { + /** + * Start a recording. + * + * Call [stopRecord] to stop the recording and release resources. + */ + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + suspend fun startRecord() + + /** + * Stop the current recording. + * + * Call [deleteRecording] to delete any recorded audio. + * + * @param cancelled If true, the recording is deleted. + */ + suspend fun stopRecord( + cancelled: Boolean = false + ) + + /** + * Stop the current recording and delete the output file. + */ + suspend fun deleteRecording() + + /** + * The current state of the recorder. + */ + val state: StateFlow +} diff --git a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt new file mode 100644 index 0000000000..8d531c3565 --- /dev/null +++ b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.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.libraries.voicerecorder.api + +import java.io.File + +sealed class VoiceRecorderState { + /** + * The recorder is idle and not recording. + */ + data object Idle : VoiceRecorderState() + + /** + * The recorder is currently recording. + * + * @property level The current audio level of the recording as a fraction of 1. + */ + data class Recording(val level: Double) : VoiceRecorderState() + + /** + * The recorder has finished recording. + * + * @property file The recorded file. + * @property mimeType The mime type of the file. + */ + data class Finished( + val file: File, + val mimeType: String, + ) : VoiceRecorderState() +} diff --git a/libraries/voicerecorder/impl/build.gradle.kts b/libraries/voicerecorder/impl/build.gradle.kts new file mode 100644 index 0000000000..6ebfb28997 --- /dev/null +++ b/libraries/voicerecorder/impl/build.gradle.kts @@ -0,0 +1,48 @@ +/* + * 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") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.voicerecorder.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + api(projects.libraries.voicerecorder.api) + api(libs.opusencoder) + + implementation(libs.dagger) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + + implementation(libs.androidx.annotationjvm) + implementation(libs.coroutines.core) + + testImplementation(projects.tests.testutils) + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.mockk) + testImplementation(libs.test.turbine) + testImplementation(libs.coroutines.core) + testImplementation(libs.coroutines.test) +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt new file mode 100644 index 0000000000..ef91118371 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt @@ -0,0 +1,132 @@ +/* + * 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.libraries.voicerecorder.impl + +import android.Manifest +import androidx.annotation.RequiresPermission +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.coroutine.childScope +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.voicerecorder.api.VoiceRecorder +import io.element.android.libraries.voicerecorder.api.VoiceRecorderState +import io.element.android.libraries.voicerecorder.impl.audio.Audio +import io.element.android.libraries.voicerecorder.impl.audio.AudioConfig +import io.element.android.libraries.voicerecorder.impl.audio.AudioLevelCalculator +import io.element.android.libraries.voicerecorder.impl.audio.AudioReader +import io.element.android.libraries.voicerecorder.impl.audio.Encoder +import io.element.android.libraries.voicerecorder.impl.file.VoiceFileConfig +import io.element.android.libraries.voicerecorder.impl.file.VoiceFileManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File +import java.util.UUID +import javax.inject.Inject + +@SingleIn(RoomScope::class) +@ContributesBinding(RoomScope::class) +class VoiceRecorderImpl @Inject constructor( + private val dispatchers: CoroutineDispatchers, + private val audioReaderFactory: AudioReader.Factory, + private val encoder: Encoder, + private val fileManager: VoiceFileManager, + private val config: AudioConfig, + private val fileConfig: VoiceFileConfig, + private val audioLevelCalculator: AudioLevelCalculator, + appCoroutineScope: CoroutineScope, +) : VoiceRecorder { + private val voiceCoroutineScope by lazy { + appCoroutineScope.childScope(dispatchers.io, "VoiceRecorder-${UUID.randomUUID()}") + } + + private var outputFile: File? = null + private var audioReader: AudioReader? = null + private var recordingJob: Job? = null + + private val _state = MutableStateFlow(VoiceRecorderState.Idle) + override val state: StateFlow = _state + + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + override suspend fun startRecord() { + Timber.i("Voice recorder started recording") + outputFile = fileManager.createFile() + .also(encoder::init) + + val audioRecorder = audioReaderFactory.create(config, dispatchers).also { audioReader = it } + + recordingJob = voiceCoroutineScope.launch { + audioRecorder.record { audio -> + when (audio) { + is Audio.Data -> { + val audioLevel = audioLevelCalculator.calculateAudioLevel(audio.buffer) + _state.emit(VoiceRecorderState.Recording(audioLevel)) + encoder.encode(audio.buffer, audio.readSize) + } + is Audio.Error -> { + Timber.e("Voice message error: code=${audio.audioRecordErrorCode}") + _state.emit(VoiceRecorderState.Recording(0.0)) + } + } + } + } + } + + /** + * Stop the current recording. + * + * Call [deleteRecording] to delete any recorded audio. + */ + override suspend fun stopRecord( + cancelled: Boolean + ) { + recordingJob?.cancel()?.also { + Timber.i("Voice recorder stopped recording") + } + recordingJob = null + + audioReader?.stop() + audioReader = null + encoder.release() + + if (cancelled) { + deleteRecording() + } + + _state.emit( + when (val file = outputFile) { + null -> VoiceRecorderState.Idle + else -> VoiceRecorderState.Finished(file, fileConfig.mimeType) + } + ) + } + + /** + * Stop the current recording and delete the output file. + */ + override suspend fun deleteRecording() { + outputFile?.let(fileManager::deleteFile)?.also { + Timber.i("Voice recorder deleted recording") + } + outputFile = null + _state.emit(VoiceRecorderState.Idle) + } +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt new file mode 100644 index 0000000000..a2342f3c2f --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt @@ -0,0 +1,139 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +import android.Manifest +import android.media.AudioRecord +import android.media.audiofx.AutomaticGainControl +import android.media.audiofx.NoiseSuppressor +import androidx.annotation.RequiresPermission +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.di.RoomScope +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext + +class AndroidAudioReader +@RequiresPermission(Manifest.permission.RECORD_AUDIO) private constructor( + private val config: AudioConfig, + private val dispatchers: CoroutineDispatchers, +) : AudioReader { + private val audioRecord: AudioRecord + private var noiseSuppressor: NoiseSuppressor? = null + private var automaticGainControl: AutomaticGainControl? = null + private val outputBuffer: ShortArray + + init { + outputBuffer = createOutputBuffer(config.sampleRate) + audioRecord = AudioRecord.Builder().setAudioSource(config.source).setAudioFormat(config.format).setBufferSizeInBytes(outputBuffer.sizeInBytes()).build() + noiseSuppressor = requestNoiseSuppressor(audioRecord) + automaticGainControl = requestAutomaticGainControl(audioRecord) + } + + /** + * Record audio data continuously. + * + * @param onAudio callback when audio is read. + */ + override suspend fun record( + onAudio: suspend (Audio) -> Unit, + ) { + audioRecord.startRecording() + withContext(dispatchers.io) { + while (isActive) { + if (audioRecord.recordingState != AudioRecord.RECORDSTATE_RECORDING) { + break + } + onAudio(read()) + } + } + } + + private fun read(): Audio { + val result = audioRecord.read(outputBuffer, 0, outputBuffer.size) + + if (isAudioRecordErrorResult(result)) { + return Audio.Error(result) + } + + return Audio.Data( + result, + outputBuffer, + ) + } + + override fun stop() { + if (audioRecord.state == AudioRecord.STATE_INITIALIZED) { + audioRecord.stop() + } + audioRecord.release() + + noiseSuppressor?.release() + noiseSuppressor = null + + automaticGainControl?.release() + automaticGainControl = null + } + + private fun createOutputBuffer(sampleRate: SampleRate): ShortArray { + val bufferSizeInShorts = AudioRecord.getMinBufferSize( + sampleRate.hz, + config.format.channelMask, + config.format.encoding + ) + return ShortArray(bufferSizeInShorts) + } + + private fun requestNoiseSuppressor(audioRecord: AudioRecord): NoiseSuppressor? { + if (!NoiseSuppressor.isAvailable()) { + return null + } + + return tryOrNull { + NoiseSuppressor.create(audioRecord.audioSessionId).apply { + enabled = true + } + } + } + + private fun requestAutomaticGainControl(audioRecord: AudioRecord): AutomaticGainControl? { + if (!AutomaticGainControl.isAvailable()) { + return null + } + + return tryOrNull { + AutomaticGainControl.create(audioRecord.audioSessionId).apply { + enabled = true + } + } + } + + @ContributesBinding(RoomScope::class) + companion object Factory : AudioReader.Factory { + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + override fun create(config: AudioConfig, dispatchers: CoroutineDispatchers): AndroidAudioReader { + return AndroidAudioReader(config, dispatchers) + } + } +} + +private fun isAudioRecordErrorResult(result: Int): Boolean { + return result < 0 +} + +private fun ShortArray.sizeInBytes(): Int = size * Short.SIZE_BYTES diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Audio.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Audio.kt new file mode 100644 index 0000000000..3e51d615f4 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Audio.kt @@ -0,0 +1,28 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +sealed class Audio { + class Data( + val readSize: Int, + val buffer: ShortArray, + ) : Audio() + + data class Error( + val audioRecordErrorCode: Int + ) : Audio() +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioConfig.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioConfig.kt new file mode 100644 index 0000000000..6ff912c2ae --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioConfig.kt @@ -0,0 +1,35 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +import android.media.AudioFormat +import android.media.MediaRecorder.AudioSource + +/** + * Audio configuration for voice recording. + * + * @property source the audio source to use, see constants in [AudioSource] + * @property format the audio format to use, see [AudioFormat] + * @property sampleRate the sample rate to use. Ensure this matches the value set in [format]. + * @property bitRate the bitrate in bps + */ +data class AudioConfig( + val source: Int, + val format: AudioFormat, + val sampleRate: SampleRate, + val bitRate: Int, +) diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt new file mode 100644 index 0000000000..554b6ba4b1 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt @@ -0,0 +1,28 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +interface AudioLevelCalculator { + /** + * Calculate the audio level of the audio buffer. + * + * @param buffer The audio buffer containing raw audio data. + * + * @return A value between 0 and 1. + */ + fun calculateAudioLevel(buffer: ShortArray): Double +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioReader.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioReader.kt new file mode 100644 index 0000000000..230c9533fd --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioReader.kt @@ -0,0 +1,37 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers + +interface AudioReader { + /** + * Record audio data continuously. + * + * @param onAudio callback when audio is read. + */ + suspend fun record( + onAudio: suspend (Audio) -> Unit, + ) + + fun stop() + + interface Factory { + fun create(config: AudioConfig, dispatchers: CoroutineDispatchers): AudioReader + } + +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt new file mode 100644 index 0000000000..8a16acf83b --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt @@ -0,0 +1,49 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.RoomScope +import javax.inject.Inject +import kotlin.math.log10 +import kotlin.math.min +import kotlin.math.sqrt + +@ContributesBinding(RoomScope::class) +class DecibelAudioLevelCalculator @Inject constructor() : AudioLevelCalculator { + companion object { + private const val REFERENCE_DB = 50.0 // Reference dB for normal conversation + } + + override fun calculateAudioLevel(buffer: ShortArray): Double { + val rms = buffer.rootMeanSquare() + + // Convert to decibels and clip + val db = 20 * log10(rms / REFERENCE_DB) + val clipped = min(db, REFERENCE_DB) + + // Scale to the range [0.0, 1.0] + return clipped / REFERENCE_DB + } + + private fun ShortArray.rootMeanSquare(): Double { + // Use Double to avoid overflow + val sumOfSquares: Double = sumOf { it.toDouble() * it.toDouble() } + val avgSquare = sumOfSquares / size.toDouble() + return sqrt(avgSquare) + } +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt new file mode 100644 index 0000000000..a888824fe5 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt @@ -0,0 +1,63 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.RoomScope +import io.element.android.opusencoder.OggOpusEncoder +import timber.log.Timber +import java.io.File +import javax.inject.Inject +import javax.inject.Provider + +/** + * Safe wrapper for OggOpusEncoder. + */ +@ContributesBinding(RoomScope::class) +class DefaultEncoder @Inject constructor( + private val encoderProvider: Provider, + config: AudioConfig, +) : Encoder { + private val bitRate = config.bitRate +private val sampleRate = config.sampleRate.asEncoderModel() + + private var encoder: OggOpusEncoder? = null + override fun init( + file: File, + ) { + encoder?.release() + encoder = encoderProvider.get().apply { + init(file.absolutePath, sampleRate) + setBitrate(bitRate) + // TODO check encoder application: 2048 (voice, default is typically 2049 as audio) + } + } + + override fun encode( + buffer: ShortArray, + readSize: Int, + ) { + encoder?.encode(buffer, readSize) + ?: Timber.w("Can't encode when encoder not initialized") + } + + override fun release() { + encoder?.release() + ?: Timber.w("Can't release encoder that is not initialized") + encoder = null + } +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Encoder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Encoder.kt new file mode 100644 index 0000000000..67685635aa --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Encoder.kt @@ -0,0 +1,28 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +import java.io.File + +interface Encoder { + + fun init(file: File) + + fun encode(buffer: ShortArray, readSize: Int) + + fun release() +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/SampleRate.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/SampleRate.kt new file mode 100644 index 0000000000..b392b6e19f --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/SampleRate.kt @@ -0,0 +1,24 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +import io.element.android.opusencoder.configuration.SampleRate as LibOpusOggSampleRate + +data object SampleRate { + const val hz = 48_000 + fun asEncoderModel() = LibOpusOggSampleRate.Rate48kHz +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt new file mode 100644 index 0000000000..b21ab48ac3 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt @@ -0,0 +1,58 @@ +/* + * 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.libraries.voicerecorder.impl.di + +import android.media.AudioFormat +import android.media.MediaRecorder +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.voicerecorder.impl.audio.AudioConfig +import io.element.android.libraries.voicerecorder.impl.audio.SampleRate +import io.element.android.libraries.voicerecorder.impl.file.VoiceFileConfig +import io.element.android.opusencoder.OggOpusEncoder + +@Module +@ContributesTo(RoomScope::class) +object VoiceRecorderModule { + @Provides + fun provideAudioConfig(): AudioConfig { + val sampleRate = SampleRate + return AudioConfig( + format = AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(sampleRate.hz) + .setChannelMask(AudioFormat.CHANNEL_IN_MONO) + .build(), + bitRate = 24_000, // 24 kbps + sampleRate = sampleRate, + source = MediaRecorder.AudioSource.MIC, + ) + } + + @Provides + fun provideVoiceFileConfig(): VoiceFileConfig = + VoiceFileConfig( + cacheSubdir = "voice_recordings", + fileExt = "ogg", + mimeType = "audio/ogg", + ) + + @Provides + fun provideOggOpusEncoder(): OggOpusEncoder = OggOpusEncoder.create() +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt new file mode 100644 index 0000000000..07ef54991f --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt @@ -0,0 +1,49 @@ +/* + * 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.libraries.voicerecorder.impl.file + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.hash.md5 +import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import java.io.File +import java.util.UUID +import javax.inject.Inject + +@ContributesBinding(RoomScope::class) +class DefaultVoiceFileManager @Inject constructor( + @CacheDirectory private val cacheDir: File, + private val config: VoiceFileConfig, + room: MatrixRoom, +) : VoiceFileManager { + + private val roomId: RoomId = room.roomId + + override fun createFile(): File { + val fileName = "${UUID.randomUUID()}.${config.fileExt}" + val outputDirectory = File(cacheDir, config.cacheSubdir) + val roomDir = File(outputDirectory, roomId.value.md5()) + .apply(File::mkdirs) + return File(roomDir, fileName) + } + + override fun deleteFile(file: File) { + file.delete() + } +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileConfig.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileConfig.kt new file mode 100644 index 0000000000..a7b1f4607d --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileConfig.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.libraries.voicerecorder.impl.file + +/** + * File configuration for voice recording. + * + * @property cacheSubdir the subdirectory in the cache dir to use. + * @property fileExt the file extension for audio files. + * @property mimeType the mime type of audio files. + */ +data class VoiceFileConfig( + val cacheSubdir: String, + val fileExt: String, + val mimeType: String, +) diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileManager.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileManager.kt new file mode 100644 index 0000000000..77e85b910e --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileManager.kt @@ -0,0 +1,25 @@ +/* + * 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.libraries.voicerecorder.impl.file + +import java.io.File + +interface VoiceFileManager { + fun createFile(): File + + fun deleteFile(file: File) +} diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt new file mode 100644 index 0000000000..847e1c514f --- /dev/null +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt @@ -0,0 +1,134 @@ +/* + * 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.libraries.voicerecorder.impl + +import android.media.AudioFormat +import android.media.MediaRecorder +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.voicerecorder.api.VoiceRecorderState +import io.element.android.libraries.voicerecorder.impl.audio.Audio +import io.element.android.libraries.voicerecorder.impl.audio.AudioConfig +import io.element.android.libraries.voicerecorder.impl.audio.SampleRate +import io.element.android.libraries.voicerecorder.impl.di.VoiceRecorderModule +import io.element.android.libraries.voicerecorder.test.FakeAudioLevelCalculator +import io.element.android.libraries.voicerecorder.test.FakeAudioRecorderFactory +import io.element.android.libraries.voicerecorder.test.FakeEncoder +import io.element.android.libraries.voicerecorder.test.FakeFileSystem +import io.element.android.libraries.voicerecorder.test.FakeVoiceFileManager +import io.element.android.tests.testutils.testCoroutineDispatchers +import io.mockk.mockk +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.BeforeClass +import org.junit.Test +import java.io.File + +class VoiceRecorderImplTest { + private val fakeFileSystem = FakeFileSystem() + + @Test + fun `it emits the initial state`() = runTest { + val voiceRecorder = createVoiceRecorder() + voiceRecorder.state.test { + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + } + } + + @Test + fun `when recording, it emits the recording state`() = runTest { + val voiceRecorder = createVoiceRecorder() + voiceRecorder.state.test { + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + + voiceRecorder.startRecord() + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.0)) + } + } + + @Test + fun `when stopped, it provides a file`() = runTest { + val voiceRecorder = createVoiceRecorder() + voiceRecorder.state.test { + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + + voiceRecorder.startRecord() + skipItems(3) + voiceRecorder.stopRecord() + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Finished(File(FILE_PATH), "audio/ogg")) + assertThat(fakeFileSystem.files[File(FILE_PATH)]).isEqualTo(ENCODED_DATA) + } + } + + @Test + fun `when cancelled, it deletes the file`() = runTest { + val voiceRecorder = createVoiceRecorder() + voiceRecorder.state.test { + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + + voiceRecorder.startRecord() + skipItems(3) + voiceRecorder.stopRecord(cancelled = true) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + assertThat(fakeFileSystem.files[File(FILE_PATH)]).isNull() + } + } + + private fun TestScope.createVoiceRecorder(): VoiceRecorderImpl { + val fileConfig = VoiceRecorderModule.provideVoiceFileConfig() + return VoiceRecorderImpl( + dispatchers = testCoroutineDispatchers(), + audioReaderFactory = FakeAudioRecorderFactory( + audio = AUDIO, + ), + encoder = FakeEncoder(fakeFileSystem), + config = AudioConfig( + format = AUDIO_FORMAT, + bitRate = 24_000, // 24 kbps + sampleRate = SampleRate, + source = MediaRecorder.AudioSource.MIC, + ), + fileConfig = fileConfig, + fileManager = FakeVoiceFileManager(fakeFileSystem, fileConfig, FILE_ID), + audioLevelCalculator = FakeAudioLevelCalculator(), + appCoroutineScope = backgroundScope, + ) + } + + companion object { + const val FILE_ID: String = "recording" + const val FILE_PATH = "voice_recordings/${FILE_ID}.ogg" + private lateinit var AUDIO_FORMAT: AudioFormat + + // FakeEncoder doesn't actually encode, it just writes the data to the file + private const val ENCODED_DATA = "[32767, 32767, 32767][32767, 32767, 32767]" + private const val MAX_AMP = Short.MAX_VALUE + private val AUDIO = listOf( + Audio.Data(3, shortArrayOf(MAX_AMP, MAX_AMP, MAX_AMP)), + Audio.Error(-1), + Audio.Data(3, shortArrayOf(MAX_AMP, MAX_AMP, MAX_AMP)), + ) + + @BeforeClass + @JvmStatic + fun initAudioFormat() { + AUDIO_FORMAT = mockk() + } + } +} diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt new file mode 100644 index 0000000000..8ffbf1ef8e --- /dev/null +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt @@ -0,0 +1,46 @@ +/* + * 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.libraries.voicerecorder.impl.audio + +import org.junit.Test + +class DecibelAudioLevelCalculatorTest { + + @Test + fun `given max values, it returns values within range`() { + val calculator = DecibelAudioLevelCalculator() + val buffer = ShortArray(100) { Short.MAX_VALUE } + val level = calculator.calculateAudioLevel(buffer) + assert(level in 0.0..1.0) + } + + @Test + fun `given mixed values, it returns values within range`() { + val calculator = DecibelAudioLevelCalculator() + val buffer = shortArrayOf(Short.MAX_VALUE, Short.MIN_VALUE, -1, 1) + val level = calculator.calculateAudioLevel(buffer) + assert(level in 0.0..1.0) + } + + @Test + fun `given min values, it returns values within range`() { + val calculator = DecibelAudioLevelCalculator() + val buffer = ShortArray(100) { Short.MIN_VALUE } + val level = calculator.calculateAudioLevel(buffer) + assert(level in 0.0..1.0) + } +} diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt new file mode 100644 index 0000000000..1615067f6c --- /dev/null +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.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.libraries.voicerecorder.test + +import io.element.android.libraries.voicerecorder.impl.audio.AudioLevelCalculator +import kotlin.math.abs + +class FakeAudioLevelCalculator: AudioLevelCalculator { + override fun calculateAudioLevel(buffer: ShortArray): Double { + return buffer.map { abs(it.toDouble()) }.average() / Short.MAX_VALUE + } +} diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt new file mode 100644 index 0000000000..71fd2df041 --- /dev/null +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt @@ -0,0 +1,49 @@ +/* + * 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.libraries.voicerecorder.test + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.voicerecorder.impl.audio.Audio +import io.element.android.libraries.voicerecorder.impl.audio.AudioReader +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield + +class FakeAudioReader( + private val dispatchers: CoroutineDispatchers, + private val audio: List