From c377656354de3326d424a481b80fe358c2f3559f Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Tue, 31 Oct 2023 15:27:17 +0100 Subject: [PATCH 01/23] Show "Voice message" in voice message push notifications (#1705) Don't show the event body anymore as it's not relevant for voice messages. --- .../push/impl/notifications/NotifiableEventResolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c8d496a64e..82b076aa73 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 @@ -211,7 +211,7 @@ class NotifiableEventResolver @Inject constructor( ): String { return when (val messageType = content.messageType) { is AudioMessageType -> messageType.body - is VoiceMessageType -> messageType.body + is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message) is EmoteMessageType -> "* $senderDisplayName ${messageType.body}" is FileMessageType -> messageType.body is ImageMessageType -> messageType.body From 3bbaf8e5e7812d404fce5f1a6bd15f3b3f4956f2 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 31 Oct 2023 16:43:18 +0100 Subject: [PATCH 02/23] Improve the logs for `TimelineException.CannotPaginate` (#1708) --- .../libraries/matrix/impl/timeline/RustMatrixTimeline.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 1f0d13c9bf..74ad10ad26 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -194,8 +194,12 @@ class RustMatrixTimeline( waitForToken = true, ) innerRoom.paginateBackwards(paginationOptions) - }.onFailure { - Timber.d(it, "Fail to paginate for room ${matrixRoom.roomId}") + }.onFailure { error -> + if (error is TimelineException.CannotPaginate) { + Timber.d("Can't paginate backwards on room ${matrixRoom.roomId}, we're already at the start") + } else { + Timber.e(error, "Error paginating backwards on room ${matrixRoom.roomId}") + } }.onSuccess { Timber.v("Success back paginating for room ${matrixRoom.roomId}") } From f554fb8dcc42482707db3a4af3eea7f2732f4875 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 31 Oct 2023 16:48:24 +0100 Subject: [PATCH 03/23] Change FeatureFlagService.isFeatureEnabled return value from Boolean to Flow --- .../featureflag/api/FeatureFlagService.kt | 12 +++++++++++- libraries/featureflag/impl/build.gradle.kts | 1 + .../impl/DefaultFeatureFlagService.kt | 8 +++++--- .../featureflag/impl/FeatureFlagProvider.kt | 3 ++- .../impl/PreferencesFeatureFlagProvider.kt | 7 ++++--- .../impl/StaticFeatureFlagProvider.kt | 9 ++++++--- .../impl/DefaultFeatureFlagServiceTest.kt | 19 +++++++++++++------ .../impl/FakeMutableFeatureFlagProvider.kt | 11 +++++++---- libraries/featureflag/test/build.gradle.kts | 1 + .../test/FakeFeatureFlagService.kt | 16 ++++++++++++---- 10 files changed, 62 insertions(+), 25 deletions(-) diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt index 8089c837ff..cd0c8937d7 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt @@ -16,13 +16,23 @@ package io.element.android.libraries.featureflag.api +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + interface FeatureFlagService { /** * @param feature the feature to check for * * @return true if the feature is enabled */ - suspend fun isFeatureEnabled(feature: Feature): Boolean + suspend fun isFeatureEnabled(feature: Feature): Boolean = isFeatureEnabledFlow(feature).first() + + /** + * @param feature the feature to check for + * + * @return a flow of booleans, true if the feature is enabled, false if it is disabled. + */ + fun isFeatureEnabledFlow(feature: Feature): Flow /** * @param feature the feature to enable or disable diff --git a/libraries/featureflag/impl/build.gradle.kts b/libraries/featureflag/impl/build.gradle.kts index ba0747b28e..64fcd1eece 100644 --- a/libraries/featureflag/impl/build.gradle.kts +++ b/libraries/featureflag/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(libs.androidx.datastore.preferences) implementation(projects.libraries.di) implementation(projects.libraries.core) + implementation(libs.coroutines.core) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.test.truth) diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt index b445599693..fd6f1b1f47 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt @@ -21,6 +21,8 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlagService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -29,12 +31,12 @@ class DefaultFeatureFlagService @Inject constructor( private val providers: Set<@JvmSuppressWildcards FeatureFlagProvider> ) : FeatureFlagService { - override suspend fun isFeatureEnabled(feature: Feature): Boolean { + override fun isFeatureEnabledFlow(feature: Feature): Flow { return providers.filter { it.hasFeature(feature) } .sortedByDescending(FeatureFlagProvider::priority) .firstOrNull() - ?.isFeatureEnabled(feature) - ?: feature.defaultValue + ?.isFeatureEnabledFlow(feature) + ?: flowOf(feature.defaultValue) } override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean { diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt index 833d9f9003..e40d59ec5b 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt @@ -17,10 +17,11 @@ package io.element.android.libraries.featureflag.impl import io.element.android.libraries.featureflag.api.Feature +import kotlinx.coroutines.flow.Flow interface FeatureFlagProvider { val priority: Int - suspend fun isFeatureEnabled(feature: Feature): Boolean + fun isFeatureEnabledFlow(feature: Feature): Flow fun hasFeature(feature: Feature): Boolean } 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..76344e078c 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,8 @@ 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.first +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged 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 isFeatureEnabledFlow(feature: Feature): Flow { return store.data.map { prefs -> prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue - }.first() + }.distinctUntilChanged() } override fun hasFeature(feature: Feature): Boolean { 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 82471c5983..e9dcc88a21 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 @@ -18,6 +18,8 @@ package io.element.android.libraries.featureflag.impl import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlags +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import javax.inject.Inject /** @@ -29,9 +31,9 @@ class StaticFeatureFlagProvider @Inject constructor() : override val priority = LOW_PRIORITY - override suspend fun isFeatureEnabled(feature: Feature): Boolean { - return if (feature is FeatureFlags) { - when(feature) { + override fun isFeatureEnabledFlow(feature: Feature): Flow { + val isFeatureEnabled = if (feature is FeatureFlags) { + when (feature) { FeatureFlags.LocationSharing -> true FeatureFlags.Polls -> true FeatureFlags.NotificationSettings -> true @@ -43,6 +45,7 @@ class StaticFeatureFlagProvider @Inject constructor() : } else { false } + return flowOf(isFeatureEnabled) } override fun hasFeature(feature: Feature) = true diff --git a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt index 890959639f..117005fd6f 100644 --- a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt +++ b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.featureflag.impl +import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.api.FeatureFlags import kotlinx.coroutines.test.runTest @@ -26,8 +27,10 @@ class DefaultFeatureFlagServiceTest { @Test fun `given service without provider when feature is checked then it returns the default value`() = runTest { val featureFlagService = DefaultFeatureFlagService(emptySet()) - val isFeatureEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing) - assertThat(isFeatureEnabled).isEqualTo(FeatureFlags.LocationSharing.defaultValue) + featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test { + assertThat(awaitItem()).isEqualTo(FeatureFlags.LocationSharing.defaultValue) + cancelAndIgnoreRemainingEvents() + } } @Test @@ -50,9 +53,11 @@ class DefaultFeatureFlagServiceTest { val featureFlagProvider = FakeMutableFeatureFlagProvider(0) val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider)) featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true) - assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true) - featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, false) - assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(false) + featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test { + assertThat(awaitItem()).isEqualTo(true) + featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, false) + assertThat(awaitItem()).isEqualTo(false) + } } @Test @@ -62,6 +67,8 @@ class DefaultFeatureFlagServiceTest { val featureFlagService = DefaultFeatureFlagService(setOf(lowPriorityFeatureFlagProvider, highPriorityFeatureFlagProvider)) lowPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, false) highPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, true) - assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true) + featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test { + assertThat(awaitItem()).isEqualTo(true) + } } } diff --git a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt index f1d075ace2..20242cca69 100644 --- a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt @@ -17,17 +17,20 @@ package io.element.android.libraries.featureflag.impl import io.element.android.libraries.featureflag.api.Feature +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow class FakeMutableFeatureFlagProvider(override val priority: Int) : MutableFeatureFlagProvider { - private val enabledFeatures = HashMap() + private val enabledFeatures = mutableMapOf>() override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean) { - enabledFeatures[feature.key] = enabled + val flow = enabledFeatures.getOrPut(feature.key) { MutableStateFlow(enabled) } + flow.emit(enabled) } - override suspend fun isFeatureEnabled(feature: Feature): Boolean { - return enabledFeatures[feature.key] ?: feature.defaultValue + override fun isFeatureEnabledFlow(feature: Feature): Flow { + return enabledFeatures.getOrPut(feature.key) { MutableStateFlow(feature.defaultValue) } } override fun hasFeature(feature: Feature): Boolean = true diff --git a/libraries/featureflag/test/build.gradle.kts b/libraries/featureflag/test/build.gradle.kts index 952b9323f6..3c3b199ce6 100644 --- a/libraries/featureflag/test/build.gradle.kts +++ b/libraries/featureflag/test/build.gradle.kts @@ -23,5 +23,6 @@ android { dependencies { api(projects.libraries.featureflag.api) + implementation(libs.coroutines.core) } } diff --git a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt index 9c86bad752..18c9920d74 100644 --- a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt +++ b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt @@ -18,19 +18,27 @@ package io.element.android.libraries.featureflag.test import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlagService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow class FakeFeatureFlagService( initialState: Map = emptyMap() ) : FeatureFlagService { - private val enabledFeatures = HashMap(initialState) + private val enabledFeatures = initialState + .map { + it.key to MutableStateFlow(it.value) + } + .toMap() + .toMutableMap() override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean { - enabledFeatures[feature.key] = enabled + val flow = enabledFeatures.getOrPut(feature.key) { MutableStateFlow(enabled) } + flow.emit(enabled) return true } - override suspend fun isFeatureEnabled(feature: Feature): Boolean { - return enabledFeatures[feature.key] ?: false + override fun isFeatureEnabledFlow(feature: Feature): Flow { + return enabledFeatures.getOrPut(feature.key) { MutableStateFlow(feature.defaultValue) } } } From b5c68f1b95b9574d13c522cf822aa18af0925a04 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 31 Oct 2023 13:14:04 +0100 Subject: [PATCH 04/23] Fix tests --- .../advanced/AdvancedSettingsPresenterTest.kt | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) 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 ec49d57fb9..c2a7fb3e20 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 @@ -24,6 +24,7 @@ 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 io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -41,10 +42,10 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitLastSequentialItem() assertThat(initialState.isDeveloperModeEnabled).isFalse() assertThat(initialState.isRichTextEditorEnabled).isFalse() - assertThat(initialState.customElementCallBaseUrlState).isNull() + assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull() } } @@ -56,7 +57,7 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitLastSequentialItem() assertThat(initialState.isDeveloperModeEnabled).isFalse() initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) assertThat(awaitItem().isDeveloperModeEnabled).isTrue() @@ -73,7 +74,7 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitLastSequentialItem() assertThat(initialState.isRichTextEditorEnabled).isFalse() initialState.eventSink.invoke(AdvancedSettingsEvents.SetRichTextEditorEnabled(true)) assertThat(awaitItem().isRichTextEditorEnabled).isTrue() @@ -92,7 +93,7 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitLastSequentialItem() assertThat(initialState.customElementCallBaseUrlState).isNull() } } @@ -105,10 +106,7 @@ class AdvancedSettingsPresenterTest { 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() + val initialState = awaitLastSequentialItem() assertThat(initialState.customElementCallBaseUrlState).isNotNull() assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull() @@ -128,10 +126,7 @@ class AdvancedSettingsPresenterTest { 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 + val urlValidator = awaitLastSequentialItem().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() From 355ee9596492233d018d27bebfae62082b67a571 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 31 Oct 2023 16:58:33 +0100 Subject: [PATCH 05/23] [Element Call] Keep MatrixClient alive while the call is working (#1695) * Element Call: keep MatrixClient alive to get event updates --- .../android/appnav/di/MatrixClientsHolder.kt | 2 +- ...CallScreeEvents.kt => CallScreenEvents.kt} | 6 +- .../features/call/ui/CallScreenPresenter.kt | 47 +++++++++++-- .../features/call/ui/CallScreenState.kt | 2 +- .../features/call/ui/CallScreenView.kt | 6 +- .../call/ui/CallScreenPresenterTest.kt | 69 +++++++++++++++++-- .../matrix/api/MatrixClientProvider.kt | 7 ++ .../matrix/test/FakeMatrixClientProvider.kt | 2 + 8 files changed, 125 insertions(+), 16 deletions(-) rename features/call/src/main/kotlin/io/element/android/features/call/ui/{CallScreeEvents.kt => CallScreenEvents.kt} (86%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt index 372fead0ee..f75725b1bb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt @@ -49,7 +49,7 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService: sessionIdsToMatrixClient.remove(sessionId) } - fun getOrNull(sessionId: SessionId): MatrixClient? { + override fun getOrNull(sessionId: SessionId): MatrixClient? { return sessionIdsToMatrixClient[sessionId] } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreeEvents.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt similarity index 86% rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreeEvents.kt rename to features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt index 8ed4454fea..d16baacf3e 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreeEvents.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt @@ -18,7 +18,7 @@ package io.element.android.features.call.ui import io.element.android.features.call.utils.WidgetMessageInterceptor -sealed interface CallScreeEvents { - data object Hangup : CallScreeEvents - data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreeEvents +sealed interface CallScreenEvents { + data object Hangup : CallScreenEvents + data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt index f9bb8bde2f..6883ebeb61 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt @@ -17,6 +17,7 @@ package io.element.android.features.call.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -37,10 +38,13 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -53,9 +57,11 @@ class CallScreenPresenter @AssistedInject constructor( @Assisted private val callType: CallType, @Assisted private val navigator: CallScreenNavigator, private val callWidgetProvider: CallWidgetProvider, - private val userAgentProvider: UserAgentProvider, + userAgentProvider: UserAgentProvider, private val clock: SystemClock, private val dispatchers: CoroutineDispatchers, + private val matrixClientsProvider: MatrixClientProvider, + private val appCoroutineScope: CoroutineScope, ) : Presenter { @AssistedFactory @@ -78,6 +84,8 @@ class CallScreenPresenter @AssistedInject constructor( loadUrl(callType, urlState, callWidgetDriver) } + HandleMatrixClientSyncState() + callWidgetDriver.value?.let { driver -> LaunchedEffect(Unit) { driver.incomingMessages @@ -115,21 +123,22 @@ class CallScreenPresenter @AssistedInject constructor( } } - fun handleEvents(event: CallScreeEvents) { + fun handleEvents(event: CallScreenEvents) { when (event) { - is CallScreeEvents.Hangup -> { + is CallScreenEvents.Hangup -> { val widgetId = callWidgetDriver.value?.id val interceptor = messageInterceptor.value if (widgetId != null && interceptor != null && isJoinedCall) { // If the call was joined, we need to hang up first. Then the UI will be dismissed automatically. sendHangupMessage(widgetId, interceptor) + isJoinedCall = false } else { coroutineScope.launch { close(callWidgetDriver.value, navigator) } } } - is CallScreeEvents.SetupMessageChannels -> { + is CallScreenEvents.SetupMessageChannels -> { messageInterceptor.value = event.widgetMessageInterceptor } } @@ -166,6 +175,36 @@ class CallScreenPresenter @AssistedInject constructor( } } + @Composable + private fun HandleMatrixClientSyncState() { + val coroutineScope = rememberCoroutineScope() + DisposableEffect(Unit) { + val client = (callType as? CallType.RoomCall)?.sessionId?.let { + matrixClientsProvider.getOrNull(it) + } ?: return@DisposableEffect onDispose { } + + coroutineScope.launch { + client.syncService().syncState + .onEach { state -> + if (state != SyncState.Running) { + client.syncService().startSync() + } + } + .collect() + } + onDispose { + // We can't use the local coroutine scope here because it will be disposed before this effect + appCoroutineScope.launch { + client.syncService().run { + if (syncState.value == SyncState.Running) { + stopSync() + } + } + } + } + } + } + private fun parseMessage(message: String): WidgetMessage? { return WidgetMessageSerializer.deserialize(message).getOrNull() } 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 index d9716251fc..12cd7612ae 100644 --- 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 @@ -22,5 +22,5 @@ data class CallScreenState( val urlState: Async, val userAgent: String, val isInWidgetMode: Boolean, - val eventSink: (CallScreeEvents) -> Unit, + val eventSink: (CallScreenEvents) -> Unit, ) diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt index 33611515a7..acc01e6149 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt @@ -65,14 +65,14 @@ internal fun CallScreenView( navigationIcon = { BackButton( resourceId = CommonDrawables.ic_compound_close, - onClick = { state.eventSink(CallScreeEvents.Hangup) } + onClick = { state.eventSink(CallScreenEvents.Hangup) } ) } ) } ) { padding -> BackHandler { - state.eventSink(CallScreeEvents.Hangup) + state.eventSink(CallScreenEvents.Hangup) } CallWebView( modifier = Modifier @@ -88,7 +88,7 @@ internal fun CallScreenView( }, onWebViewCreated = { webView -> val interceptor = WebViewWidgetMessageInterceptor(webView) - state.eventSink(CallScreeEvents.SetupMessageChannels(interceptor)) + state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) } ) } 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 index c318b1dfaa..77f83de209 100644 --- 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 @@ -25,14 +25,21 @@ 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.api.sync.SyncState 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.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.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent @@ -95,7 +102,7 @@ class CallScreenPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) // And incoming message from the Widget Driver is passed to the WebView widgetDriver.givenIncomingMessage("A message") @@ -125,9 +132,9 @@ class CallScreenPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) - initialState.eventSink(CallScreeEvents.Hangup) + initialState.eventSink(CallScreenEvents.Hangup) // Let background coroutines run runCurrent() @@ -155,7 +162,7 @@ class CallScreenPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) messageInterceptor.givenInterceptedMessage("""{"action":"im.vector.hangup","api":"fromWidget","widgetId":"1","requestId":"1"}""") @@ -169,12 +176,64 @@ class CallScreenPresenterTest { } } + @Test + fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeWidgetDriver() + val matrixClient = FakeMatrixClient() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + consumeItemsUntilTimeout() + + assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Running) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - automatically stops the Matrix client sync on dispose`() = runTest { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeWidgetDriver() + val matrixClient = FakeMatrixClient() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) + ) + val hasRun = Mutex(true) + val job = launch { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.collect { + hasRun.unlock() + } + } + + hasRun.lock() + + job.cancelAndJoin() + + assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Terminated) + } + private fun TestScope.createCallScreenPresenter( callType: CallType, navigator: CallScreenNavigator = FakeCallScreenNavigator(), widgetDriver: FakeWidgetDriver = FakeWidgetDriver(), widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), ): CallScreenPresenter { val userAgentProvider = object : UserAgentProvider { override fun provide(): String { @@ -189,6 +248,8 @@ class CallScreenPresenterTest { userAgentProvider, clock, dispatchers, + matrixClientsProvider, + this, ) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt index 44d1a1d1a6..eaa0356209 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt @@ -25,4 +25,11 @@ interface MatrixClientProvider { * Most of the time you want to use injected constructor instead of retrieving a MatrixClient with this provider. */ suspend fun getOrRestore(sessionId: SessionId): Result + + /** + * Can be used to retrieve an existing [MatrixClient] with the given [SessionId]. + * @param sessionId the [SessionId] of the [MatrixClient] to retrieve. + * @return the [MatrixClient] if it exists. + */ + fun getOrNull(sessionId: SessionId): MatrixClient? } 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 index 80cdcff7ec..53ebf00f41 100644 --- 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 @@ -24,4 +24,6 @@ class FakeMatrixClientProvider( private val getClient: (SessionId) -> Result = { Result.success(FakeMatrixClient()) } ) : MatrixClientProvider { override suspend fun getOrRestore(sessionId: SessionId): Result = getClient(sessionId) + + override fun getOrNull(sessionId: SessionId): MatrixClient? = getClient(sessionId).getOrNull() } From ed26c7462740cb4d110fb56059b3be8cba3645c9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 31 Oct 2023 18:23:48 +0100 Subject: [PATCH 06/23] LockScreen : enable the feature --- .../element/android/libraries/featureflag/api/FeatureFlags.kt | 2 +- .../libraries/featureflag/impl/StaticFeatureFlagProvider.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 f7dad97490..e04c1e6cd2 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 @@ -53,7 +53,7 @@ enum class FeatureFlags( key = "feature.pinunlock", title = "Pin unlock", description = "Allow user to lock/unlock the app with a pin code or biometrics", - defaultValue = false, + defaultValue = true, ), InRoomCalls( key = "feature.elementcall", 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 e9dcc88a21..3e67fef201 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 @@ -38,7 +38,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.Polls -> true FeatureFlags.NotificationSettings -> true FeatureFlags.VoiceMessages -> true - FeatureFlags.PinUnlock -> false + FeatureFlags.PinUnlock -> true FeatureFlags.InRoomCalls -> true FeatureFlags.Mentions -> false } From 6832b1f2db972d1638a1cf55954ca2a1fd2346cc Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 31 Oct 2023 19:22:43 +0100 Subject: [PATCH 07/23] Feature/fga/biometric unlock (#1702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Biometric unlock : refactor a bit existing classes * Biometric unlock : first implementation * Biometric: add ui for biometric setup * Biometric unlock : use localazy strings * Biometric unlock setup : branch skip/allow events * Biometric : fix tests * Biometrics: add small test * Biometric : clean up * Update screenshots * Biometric unlock : address some PR review * Biometric : improve a bit edge cases * Fix lint issues --------- Co-authored-by: ganfra Co-authored-by: ElementBot Co-authored-by: Jorge Martín --- .../io/element/android/x/MainActivity.kt | 4 +- app/src/main/res/values-night/themes.xml | 2 +- app/src/main/res/values/themes.xml | 2 +- .../android/appconfig/LockScreenConfig.kt | 17 +- features/lockscreen/impl/build.gradle.kts | 1 + .../impl/DefaultLockScreenService.kt | 20 ++- .../lockscreen/impl/LockScreenFlowNode.kt | 4 +- .../impl/biometric/BiometricUnlock.kt | 134 ++++++++++++++++ .../impl/biometric/BiometricUnlockError.kt | 38 +++++ .../impl/biometric/BiometricUnlockManager.kt | 38 +++++ .../DefaultBiometricUnlockCallback.kt | 14 +- .../DefaultBiometricUnlockManager.kt | 148 ++++++++++++++++++ .../impl/pin/DefaultPinCodeManager.kt | 32 ++-- .../impl/settings/LockScreenSettingsEvents.kt | 2 +- .../settings/LockScreenSettingsFlowNode.kt | 24 ++- .../settings/LockScreenSettingsPresenter.kt | 16 +- .../impl/settings/LockScreenSettingsState.kt | 1 + .../LockScreenSettingsStateProvider.kt | 2 + .../impl/settings/LockScreenSettingsView.kt | 12 +- .../impl/setup/LockScreenSetupFlowNode.kt | 115 ++++++++++++++ .../setup/biometric/SetupBiometricEvents.kt | 22 +++ .../setup/biometric/SetupBiometricNode.kt | 59 +++++++ .../biometric/SetupBiometricPresenter.kt | 61 ++++++++ .../setup/biometric/SetupBiometricState.kt | 22 +++ .../biometric/SetupBiometricStateProvider.kt | 33 ++++ .../setup/biometric/SetupBiometricView.kt | 105 +++++++++++++ .../impl/setup/{ => pin}/SetupPinEvents.kt | 2 +- .../impl/setup/{ => pin}/SetupPinNode.kt | 2 +- .../impl/setup/{ => pin}/SetupPinPresenter.kt | 6 +- .../impl/setup/{ => pin}/SetupPinState.kt | 4 +- .../setup/{ => pin}/SetupPinStateProvider.kt | 4 +- .../impl/setup/{ => pin}/SetupPinView.kt | 4 +- .../{ => pin}/validation/PinValidator.kt | 2 +- .../{ => pin}/validation/SetupPinFailure.kt | 2 +- .../storage/EncryptedPinCodeStorage.kt | 2 +- .../LockScreenStore.kt} | 16 +- .../PreferencesLockScreenStore.kt} | 21 ++- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 1 + .../impl/unlock/PinUnlockPresenter.kt | 21 ++- .../lockscreen/impl/unlock/PinUnlockState.kt | 14 ++ .../impl/unlock/PinUnlockStateProvider.kt | 6 + .../lockscreen/impl/unlock/PinUnlockView.kt | 20 ++- .../impl/src/main/res/values/localazy.xml | 3 + .../biometric/FakeBiometricUnlockManager.kt | 41 +++++ .../impl/fixtures/LockScreenConfig.kt | 10 +- .../impl/fixtures/PinCodeManager.kt | 12 +- .../impl/pin/DefaultPinCodeManagerTest.kt | 10 +- ...odeStore.kt => InMemoryLockScreenStore.kt} | 15 +- .../LockScreenSettingsPresenterTest.kt | 7 +- .../biometric/SetupBiometricPresenterTest.kt | 74 +++++++++ .../setup/{ => pin}/SetupPinPresenterTest.kt | 6 +- .../impl/unlock/PinUnlockPresenterTest.kt | 14 +- gradle/libs.versions.toml | 3 +- .../cryptography/api/SecretKeyRepository.kt | 38 +++++ .../cryptography/impl/CryptographyModule.kt | 37 +++++ ...ider.kt => KeyStoreSecretKeyRepository.kt} | 26 +-- ...ovider.kt => SimpleSecretKeyRepository.kt} | 10 +- .../src/main/res/values/localazy.xml | 2 + ...etricView-D-2_2_null_0,NEXUS_5,1.0,en].png | 3 + ...etricView-N-2_3_null_0,NEXUS_5,1.0,en].png | 3 + ...pPinView-D-3_3_null_0,NEXUS_5,1.0,en].png} | 0 ...pPinView-D-3_3_null_1,NEXUS_5,1.0,en].png} | 0 ...pPinView-D-3_3_null_2,NEXUS_5,1.0,en].png} | 0 ...pPinView-D-3_3_null_3,NEXUS_5,1.0,en].png} | 0 ...pPinView-D-3_3_null_4,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_0,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_1,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_2,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_3,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_4,NEXUS_5,1.0,en].png} | 0 ..._PinKeypad-D-5_5_null,NEXUS_5,1.0,en].png} | 0 ..._PinKeypad-N-5_6_null,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_0,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_1,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_2,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_3,NEXUS_5,1.0,en].png} | 0 ...nlockView-D-4_4_null_4,NEXUS_5,1.0,en].png | 3 + ...lockView-D-4_4_null_5,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_6,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_0,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_1,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_2,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_3,NEXUS_5,1.0,en].png} | 0 ...nlockView-N-4_5_null_4,NEXUS_5,1.0,en].png | 3 + ...lockView-N-4_5_null_5,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_6,NEXUS_5,1.0,en].png} | 0 86 files changed, 1269 insertions(+), 106 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlock.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockError.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockManager.kt rename libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt => features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockCallback.kt (66%) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockManager.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinEvents.kt (92%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinNode.kt (96%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinPresenter.kt (94%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinState.kt (87%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinStateProvider.kt (93%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinView.kt (97%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/validation/PinValidator.kt (94%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/validation/SetupPinFailure.kt (90%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{pin => }/storage/EncryptedPinCodeStorage.kt (95%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{pin/storage/PinCodeStore.kt => storage/LockScreenStore.kt} (71%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{pin/storage/PreferencesPinCodeStore.kt => storage/PreferencesLockScreenStore.kt} (82%) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricUnlockManager.kt rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/{InMemoryPinCodeStore.kt => InMemoryLockScreenStore.kt} (73%) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinPresenterTest.kt (96%) create mode 100644 libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt create mode 100644 libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt rename libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/{KeyStoreSecretKeyProvider.kt => KeyStoreSecretKeyRepository.kt} (75%) rename libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/{SimpleSecretKeyProvider.kt => SimpleSecretKeyRepository.kt} (78%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.biometric_null_SetupBiometricView-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.setup.biometric_null_SetupBiometricView-N-2_3_null_0,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-5_5_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-5_6_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_4,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_4,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_6,NEXUS_5,1.0,en].png} (100%) diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index 3b35a0ebf4..7109743052 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import com.bumble.appyx.core.integration.NodeHost -import com.bumble.appyx.core.integrationpoint.NodeComponentActivity +import com.bumble.appyx.core.integrationpoint.NodeActivity import com.bumble.appyx.core.plugin.NodeReadyObserver import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag @@ -42,7 +42,7 @@ import timber.log.Timber private val loggerTag = LoggerTag("MainActivity") -class MainActivity : NodeComponentActivity() { +class MainActivity : NodeActivity() { private lateinit var mainNode: MainNode diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index a6572451d7..fad45b6def 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -22,5 +22,5 @@ @style/Theme.ElementX - -