From 3a15b92eb6aa8dc67ee7014bfecc24b892443232 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Oct 2023 18:02:34 +0200 Subject: [PATCH 1/4] SecureBackup: update matrix sdk module. --- .../impl/unlock/PinUnlockPresenter.kt | 2 +- .../impl/DefaultLogoutPreferencePresenter.kt | 2 +- .../libraries/matrix/api/MatrixClient.kt | 5 +- .../api/encryption/BackupUploadState.kt | 35 +++++ .../api/encryption/EnableRecoveryProgress.kt | 25 ++++ .../api/encryption/EncryptionService.kt | 52 +++++++ .../matrix/api/encryption/RecoveryState.kt | 24 ++++ .../libraries/matrix/impl/RustMatrixClient.kt | 27 +++- .../matrix/impl/di/SessionMatrixModule.kt | 6 + .../encryption/BackupUploadStateMapper.kt | 41 ++++++ .../EnableRecoveryProgressMapper.kt | 36 +++++ .../impl/encryption/RecoveryStateMapper.kt | 31 ++++ .../impl/encryption/RustEncryptionService.kt | 133 ++++++++++++++++++ .../RustSessionVerificationService.kt | 3 +- .../libraries/matrix/test/FakeMatrixClient.kt | 10 +- .../android/libraries/matrix/test/TestData.kt | 2 + .../test/encryption/FakeEncryptionService.kt | 95 +++++++++++++ 17 files changed, 518 insertions(+), 11 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt 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 b26f967b4c..ed8ee1933d 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 @@ -142,7 +142,7 @@ class PinUnlockPresenter @Inject constructor( private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { - matrixClient.logout() + matrixClient.logout(ignoreSdkError = true) }.runCatchingUpdatingState(signOutAction) } } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt index 2fece4449b..49d0606633 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt @@ -58,7 +58,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli private fun CoroutineScope.logout(logoutAction: MutableState>) = launch { suspend { - matrixClient.logout() + matrixClient.logout(false /* TODO */) }.runCatchingUpdatingState(logoutAction) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index fc215e4780..f9ac0d84a5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -21,6 +21,7 @@ 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.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -55,6 +56,7 @@ interface MatrixClient : Closeable { fun pushersService(): PushersService fun notificationService(): NotificationService fun notificationSettingsService(): NotificationSettingsService + fun encryptionService(): EncryptionService suspend fun getCacheSize(): Long /** @@ -66,8 +68,9 @@ interface MatrixClient : Closeable { * Logout the user. * Returns an optional URL. When the URL is there, it should be presented to the user after logout for * Relying Party (RP) initiated logout on their account page. + * @param ignoreSdkError if true, the SDK will ignore any error and delete the session data anyway. */ - suspend fun logout(): String? + suspend fun logout(ignoreSdkError: Boolean): String? suspend fun loadUserDisplayName(): Result suspend fun loadUserAvatarURLString(): Result suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt new file mode 100644 index 0000000000..4424db4684 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.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.matrix.api.encryption + +sealed interface BackupUploadState { + data object Unknown : BackupUploadState + + data class CheckingIfUploadNeeded( + val backedUpCount: Int, + val totalCount: Int, + ) : BackupUploadState + + data object Waiting : BackupUploadState + + data class Uploading( + val backedUpCount: Int, + val totalCount: Int, + ) : BackupUploadState + + data object Done : BackupUploadState +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt new file mode 100644 index 0000000000..1ee85a1cc1 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.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.matrix.api.encryption + +sealed interface EnableRecoveryProgress { + data object Unknown : EnableRecoveryProgress + data object CreatingRecoveryKey : EnableRecoveryProgress + data object CreatingBackup : EnableRecoveryProgress + data class BackingUp(val backedUpCount: Int, val totalCount: Int) : EnableRecoveryProgress + data class Done(val recoveryKey: String) : EnableRecoveryProgress +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt new file mode 100644 index 0000000000..b11cc3f9ab --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.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.api.encryption + +import kotlinx.coroutines.flow.StateFlow + +interface EncryptionService { + val backupStateStateFlow: StateFlow + val recoveryStateStateFlow: StateFlow + val backupUploadStateStateFlow: StateFlow + val enableRecoveryProgressStateFlow: StateFlow + + suspend fun enableBackups(): Result + + suspend fun isLastDevice(): Result + + /** + * Enable recovery. Observe enableProgressStateFlow to get progress and recovery key. + */ + suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result + + /** + * Change the recovery and return the new recovery key. + */ + suspend fun resetRecoveryKey(): Result + + suspend fun disableRecovery(): Result + + /** + * Note: accept bot recoveryKey and passphrase. + */ + suspend fun fixRecoveryIssues(recoveryKey: String): Result + + /** + * Observe [backupUploadStateStateFlow] to get progress. + */ + suspend fun waitForBackupUploadSteadyState(): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt new file mode 100644 index 0000000000..c033332d8f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.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.matrix.api.encryption + +enum class RecoveryState { + UNKNOWN, + ENABLED, + DISABLED, + INCOMPLETE, +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 67bbcb835c..c339bca26c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -41,6 +42,7 @@ import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.impl.core.toProgressWatcher +import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.notification.RustNotificationService @@ -117,6 +119,7 @@ class RustMatrixClient constructor( private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock) private val notificationSettingsService = RustNotificationSettingsService(notificationSettings, dispatchers) private val roomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers) + private val encryptionService = RustEncryptionService(client, dispatchers).apply { start() } private val isLoggingOut = AtomicBoolean(false) @@ -136,7 +139,7 @@ class RustMatrixClient constructor( ) sessionStore.updateData(newData) } - doLogout(doRequest = false, removeSession = false) + doLogout(doRequest = false, removeSession = false, ignoreSdkError = false) } } else { Timber.v("didReceiveAuthError -> already cleaning up") @@ -319,6 +322,8 @@ class RustMatrixClient constructor( override fun notificationService(): NotificationService = notificationService + override fun encryptionService(): EncryptionService = encryptionService + override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService override fun close() { @@ -331,6 +336,7 @@ class RustMatrixClient constructor( innerRoomListService.destroy() notificationClient.destroy() notificationProcessSetup.destroy() + encryptionService.destroy() client.destroy() } @@ -344,16 +350,29 @@ class RustMatrixClient constructor( baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false) } - override suspend fun logout(): String? = doLogout(doRequest = true, removeSession = true) + override suspend fun logout(ignoreSdkError: Boolean): String? = doLogout( + doRequest = true, + removeSession = true, + ignoreSdkError = ignoreSdkError, + ) - private suspend fun doLogout(doRequest: Boolean, removeSession: Boolean): String? { + private suspend fun doLogout( + doRequest: Boolean, + removeSession: Boolean, + ignoreSdkError: Boolean, + ): String? { var result: String? = null withContext(sessionDispatcher) { if (doRequest) { try { result = client.logout() } catch (failure: Throwable) { - Timber.e(failure, "Fail to call logout on HS. Still delete local files.") + if (ignoreSdkError) { + Timber.e(failure, "Fail to call logout on HS. Still delete local files.") + } else { + Timber.e(failure, "Fail to call logout on HS.") + throw failure + } } } close() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 1159f668e6..71f01f0aac 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -21,6 +21,7 @@ import dagger.Module import dagger.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -50,6 +51,11 @@ object SessionMatrixModule { return matrixClient.roomListService } + @Provides + fun providesEncryptionService(matrixClient: MatrixClient): EncryptionService { + return matrixClient.encryptionService() + } + @Provides fun provideMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader { return matrixClient.mediaLoader diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt new file mode 100644 index 0000000000..71bc8a081a --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt @@ -0,0 +1,41 @@ +/* + * 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.encryption + +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState + +class BackupUploadStateMapper { + fun map(rustEnableProgress: RustBackupUploadState): BackupUploadState { + return when (rustEnableProgress) { + is RustBackupUploadState.CheckingIfUploadNeeded -> + BackupUploadState.CheckingIfUploadNeeded( + backedUpCount = rustEnableProgress.backedUpCount.toInt(), + totalCount = rustEnableProgress.totalCount.toInt(), + ) + RustBackupUploadState.Done -> + BackupUploadState.Done + is RustBackupUploadState.Uploading -> + BackupUploadState.Uploading( + backedUpCount = rustEnableProgress.backedUpCount.toInt(), + totalCount = rustEnableProgress.totalCount.toInt(), + ) + RustBackupUploadState.Waiting -> + BackupUploadState.Waiting + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt new file mode 100644 index 0000000000..a50a68267d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt @@ -0,0 +1,36 @@ +/* + * 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.encryption + +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress + +class EnableRecoveryProgressMapper { + fun map(rustEnableProgress: RustEnableRecoveryProgress): EnableRecoveryProgress { + return when (rustEnableProgress) { + is RustEnableRecoveryProgress.CreatingRecoveryKey -> EnableRecoveryProgress.CreatingRecoveryKey + is RustEnableRecoveryProgress.CreatingBackup -> EnableRecoveryProgress.CreatingBackup + is RustEnableRecoveryProgress.BackingUp -> EnableRecoveryProgress.BackingUp( + backedUpCount = rustEnableProgress.backedUpCount.toInt(), + totalCount = rustEnableProgress.totalCount.toInt(), + ) + is RustEnableRecoveryProgress.Done -> EnableRecoveryProgress.Done( + recoveryKey = rustEnableProgress.recoveryKey + ) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt new file mode 100644 index 0000000000..8d9050f22d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.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.libraries.matrix.impl.encryption + +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState + +class RecoveryStateMapper { + fun map(state: RustRecoveryState): RecoveryState { + return when (state) { + RustRecoveryState.UNKNOWN -> RecoveryState.UNKNOWN + RustRecoveryState.ENABLED -> RecoveryState.ENABLED + RustRecoveryState.DISABLED -> RecoveryState.DISABLED + RustRecoveryState.INCOMPLETE -> RecoveryState.INCOMPLETE + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt new file mode 100644 index 0000000000..9b1e3909d7 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -0,0 +1,133 @@ +/* + * 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.encryption + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.BackupStateListener +import org.matrix.rustcomponents.sdk.BackupSteadyStateListener +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener +import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.RecoveryStateListener +import org.matrix.rustcomponents.sdk.BackupState as RustBackupState +import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState +import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress +import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState + +internal class RustEncryptionService( + client: Client, + private val dispatchers: CoroutineDispatchers, +) : EncryptionService { + + private val service: Encryption = client.encryption() + + private val backupStateMapper = BackupStateMapper() + private val recoveryStateMapper = RecoveryStateMapper() + private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper() + private val backupUploadStateMapper = BackupUploadStateMapper() + + override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(service.backupState().let(backupStateMapper::map)) + override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map)) + override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Unknown) + override val backupUploadStateStateFlow: MutableStateFlow = MutableStateFlow(BackupUploadState.Unknown) + + fun start() { + service.backupStateListener(object : BackupStateListener { + override fun onUpdate(status: RustBackupState) { + backupStateStateFlow.value = backupStateMapper.map(status) + } + }) + + service.recoveryStateListener(object : RecoveryStateListener { + override fun onUpdate(status: RustRecoveryState) { + recoveryStateStateFlow.value = recoveryStateMapper.map(status) + } + }) + } + + fun destroy() { + // No way to remove the listeners... + service.destroy() + } + + override suspend fun enableBackups(): Result = withContext(dispatchers.io) { + runCatching { + service.enableBackups() + } + } + + override suspend fun enableRecovery( + waitForBackupsToUpload: Boolean, + ): Result = withContext(dispatchers.io) { + runCatching { + service.enableRecovery( + waitForBackupsToUpload = waitForBackupsToUpload, + progressListener = object : EnableRecoveryProgressListener { + override fun onUpdate(status: RustEnableRecoveryProgress) { + enableRecoveryProgressStateFlow.value = enableRecoveryProgressMapper.map(status) + } + } + ) + // enableRecovery returns the encryption key, but we read it from the state flow + .let { } + } + } + + override suspend fun waitForBackupUploadSteadyState( + ): Result = withContext(dispatchers.io) { + runCatching { + service.waitForBackupUploadSteadyState( + progressListener = object : BackupSteadyStateListener { + override fun onUpdate(status: RustBackupUploadState) { + backupUploadStateStateFlow.value = backupUploadStateMapper.map(status) + } + } + ) + } + } + + override suspend fun disableRecovery(): Result = withContext(dispatchers.io) { + runCatching { + service.disableRecovery() + } + } + + override suspend fun isLastDevice(): Result = withContext(dispatchers.io) { + runCatching { + service.isLastDevice() + } + } + + override suspend fun resetRecoveryKey(): Result = withContext(dispatchers.io) { + runCatching { + service.resetRecoveryKey() + } + } + + override suspend fun fixRecoveryIssues(recoveryKey: String): Result = withContext(dispatchers.io) { + runCatching { + service.fixRecoveryIssues(recoveryKey) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index 6616539e95..a4dfadfa5b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -33,9 +33,8 @@ import org.matrix.rustcomponents.sdk.SessionVerificationController import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface import org.matrix.rustcomponents.sdk.SessionVerificationEmoji -import javax.inject.Inject -class RustSessionVerificationService @Inject constructor( +class RustSessionVerificationService( private val syncService: RustSyncService, private val sessionCoroutineScope: CoroutineScope, ) : SessionVerificationService, SessionVerificationControllerDelegate { 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 2332a37fdc..e8228e806e 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 @@ -22,6 +22,7 @@ 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.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService @@ -55,6 +57,7 @@ class FakeMatrixClient( private val notificationService: FakeNotificationService = FakeNotificationService(), private val notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private val syncService: FakeSyncService = FakeSyncService(), + private val encryptionService: FakeEncryptionService = FakeEncryptionService(), private val accountManagementUrlString: Result = Result.success(null), ) : MatrixClient { @@ -124,9 +127,11 @@ class FakeMatrixClient( override suspend fun clearCache() { } - override suspend fun logout(): String? { + override suspend fun logout(ignoreSdkError: Boolean): String? { delay(100) - logoutFailure?.let { throw it } + if (ignoreSdkError.not()) { + logoutFailure?.let { throw it } + } return null } @@ -173,6 +178,7 @@ class FakeMatrixClient( override fun notificationService(): NotificationService = notificationService override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService + override fun encryptionService(): EncryptionService = encryptionService override fun roomMembershipObserver(): RoomMembershipObserver { return RoomMembershipObserver() diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 8e71447716..ca55e6d514 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -72,3 +72,5 @@ const val A_FAILURE_REASON = "There has been a failure" val A_THROWABLE = Throwable(A_FAILURE_REASON) val AN_EXCEPTION = Exception(A_FAILURE_REASON) + +const val A_RECOVERY_KEY = "1234 5678" diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt new file mode 100644 index 0000000000..548b2f9165 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -0,0 +1,95 @@ +/* + * 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.encryption + +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeEncryptionService : EncryptionService { + private var disableRecoveryFailure: Exception? = null + override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) + override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(RecoveryState.UNKNOWN) + override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Unknown) + override val backupUploadStateStateFlow: MutableStateFlow = MutableStateFlow(BackupUploadState.Unknown) + + private var fixRecoveryIssuesFailure: Exception? = null + + override suspend fun enableBackups(): Result = simulateLongTask { + return Result.success(Unit) + } + + fun givenDisableRecoveryFailure(exception: Exception) { + disableRecoveryFailure = exception + } + + fun givenFixRecoveryIssuesFailure(exception: Exception?) { + fixRecoveryIssuesFailure = exception + } + + override suspend fun disableRecovery(): Result = simulateLongTask { + disableRecoveryFailure?.let { return Result.failure(it) } + return Result.success(Unit) + } + + override suspend fun fixRecoveryIssues(recoveryKey: String): Result = simulateLongTask { + fixRecoveryIssuesFailure?.let { return Result.failure(it) } + return Result.success(Unit) + } + + private var isLastDevice = false + + fun givenIsLastDevice(isLastDevice: Boolean) { + this.isLastDevice = isLastDevice + } + + override suspend fun isLastDevice(): Result { + return Result.success(isLastDevice) + } + + override suspend fun resetRecoveryKey(): Result = simulateLongTask { + return Result.success(fakeRecoveryKey) + } + + override suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result = simulateLongTask { + return Result.success(Unit) + } + + override suspend fun waitForBackupUploadSteadyState(): Result { + return Result.success(Unit) + } + + suspend fun emitBackupUploadState(state: BackupUploadState) { + backupUploadStateStateFlow.emit(state) + } + + suspend fun emitBackupState(state: BackupState) { + backupStateStateFlow.emit(state) + } + + suspend fun emitEnableRecoveryProgress(state: EnableRecoveryProgress) { + enableRecoveryProgressStateFlow.emit(state) + } + + companion object { + const val fakeRecoveryKey = "fake" + } +} From 0457e5915c8029816fad92b01480299de7be2433 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 26 Oct 2023 18:58:00 +0200 Subject: [PATCH 2/4] Convert mx waveform to floats as early as possible in the chain (#1652) This way we're sure that internally we always deal with [0;1] float samples. the [0;1024] int range is used only at the rust sdk boundary. --- .../components/event/TimelineItemVoiceView.kt | 3 +- .../TimelineItemContentMessageFactory.kt | 3 +- .../model/event/TimelineItemVoiceContent.kt | 2 +- .../event/TimelineItemVoiceContentProvider.kt | 6 +-- .../impl/voicemessages/WaveformUtils.kt | 23 +++++++++ .../designsystem/components/media/Waveform.kt | 50 ------------------- .../components/media/WaveformPlaybackView.kt | 38 +++++++++++--- ...iceView-D-40_40_null_5,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-40_40_null_6,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-40_40_null_7,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-40_40_null_8,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-40_40_null_9,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_5,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_6,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_7,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_8,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_9,NEXUS_5,1.0,en].png | 4 +- ...ewUnified-D-41_41_null,NEXUS_5,1.0,en].png | 4 +- ...ewUnified-N-41_42_null,NEXUS_5,1.0,en].png | 4 +- ...mPlaybackView-D_0_null,NEXUS_5,1.0,en].png | 4 +- ...mPlaybackView-N_1_null,NEXUS_5,1.0,en].png | 4 +- 21 files changed, 88 insertions(+), 93 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index 34bdc75ca2..e02773131d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -44,7 +44,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider -import io.element.android.libraries.designsystem.components.media.Waveform import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -88,7 +87,7 @@ fun TimelineItemVoiceView( WaveformPlaybackView( showCursor = state.button == VoiceMessageState.Button.Pause, playbackProgress = state.progress, - waveform = Waveform(data = content.waveform), + waveform = content.waveform, modifier = Modifier .height(34.dp) .weight(1f), 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 4f78c38393..3262653134 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 @@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor import io.element.android.features.messages.impl.timeline.util.toHtmlDocument +import io.element.android.features.messages.impl.voicemessages.fromMSC3246range import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -117,7 +118,7 @@ class TimelineItemContentMessageFactory @Inject constructor( mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, - waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(), + waveform = messageType.details?.waveform?.fromMSC3246range()?.toImmutableList() ?: persistentListOf(), ) else -> TimelineItemAudioContent( body = messageType.body, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt index ec4647b7bd..0d3bb903de 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt @@ -27,7 +27,7 @@ data class TimelineItemVoiceContent( val duration: Duration, val mediaSource: MediaSource, val mimeType: String, - val waveform: ImmutableList, + val waveform: ImmutableList, ) : TimelineItemEventContent { override val type: String = "TimelineItemAudioContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt index 8c9ac1f21c..830255f06e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt @@ -32,11 +32,11 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0), + waveform: List = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), ) = TimelineItemVoiceContent( eventId = eventId?.let { EventId(it) }, body = body, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt new file mode 100644 index 0000000000..354e2eba7d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt @@ -0,0 +1,23 @@ +/* + * 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 + +/** + * Resizes the given [0;1024] int list as per unstable MSC3246 spec + * to a [0;1] range float list to be used for waveform rendering. + */ +fun List.fromMSC3246range(): List = map { it / 1024f } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt deleted file mode 100644 index 4e86c87d18..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt +++ /dev/null @@ -1,50 +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.libraries.designsystem.components.media - -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlin.math.roundToInt - -data class Waveform ( - val data: ImmutableList -) { - companion object { - private val dataRange = 0..1024 - } - - fun normalisedData(maxSamplesCount: Int): ImmutableList { - if(maxSamplesCount <= 0) { - return persistentListOf() - } - - // Filter the data to keep only the expected number of samples - val result = if (data.size > maxSamplesCount) { - (0.. - val targetIndex = (index.toDouble() * (data.count().toDouble() / maxSamplesCount.toDouble())).roundToInt() - data[targetIndex] - } - } else { - data - } - - // Normalize the sample in the allowed range - return result.map { it.toFloat() / dataRange.last.toFloat() }.toPersistentList() - } -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt index d9d15c1c3b..b5d472ef69 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt @@ -46,22 +46,25 @@ 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.theme.ElementTheme +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlin.math.max +import kotlin.math.roundToInt private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F + @OptIn(ExperimentalComposeUiApi::class) @Composable fun WaveformPlaybackView( playbackProgress: Float, showCursor: Boolean, - waveform: Waveform, + waveform: ImmutableList, modifier: Modifier = Modifier, onSeek: (progress: Float) -> Unit = {}, brush: Brush = SolidColor(ElementTheme.colors.iconQuaternary), - progressBrush: Brush = SolidColor(ElementTheme.colors.iconSecondary), - cursorBrush: Brush = SolidColor(ElementTheme.colors.iconAccentTertiary), + progressBrush: Brush = SolidColor(ElementTheme.colors.iconSecondary), + cursorBrush: Brush = SolidColor(ElementTheme.colors.iconAccentTertiary), lineWidth: Dp = 2.dp, linePadding: Dp = 2.dp, minimumGraphAmplitude: Float = 2F, @@ -145,7 +148,7 @@ fun WaveformPlaybackView( ), blendMode = BlendMode.SrcAtop ) - if(showCursor || seekProgress.value != null) { + if (showCursor || seekProgress.value != null) { drawRoundRect( brush = cursorBrush, topLeft = Offset( @@ -166,24 +169,43 @@ fun WaveformPlaybackView( @PreviewsDayNight @Composable internal fun WaveformPlaybackViewPreview() = ElementPreview { - Column{ + Column { WaveformPlaybackView( modifier = Modifier.height(34.dp), showCursor = false, playbackProgress = 0.5f, - waveform = Waveform(persistentListOf()), + waveform = persistentListOf(), ) WaveformPlaybackView( modifier = Modifier.height(34.dp), showCursor = false, playbackProgress = 0.5f, - waveform = Waveform(persistentListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)), + waveform = persistentListOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), ) WaveformPlaybackView( modifier = Modifier.height(34.dp), showCursor = true, playbackProgress = 0.5f, - waveform = Waveform(List(1024) { it }.toPersistentList()), + waveform = List(1024) { it / 1024f }.toPersistentList(), ) } } + +private fun ImmutableList.normalisedData(maxSamplesCount: Int): ImmutableList { + if (maxSamplesCount <= 0) { + return persistentListOf() + } + + // Filter the data to keep only the expected number of samples + val result = if (this.size > maxSamplesCount) { + (0.. + val targetIndex = (index.toDouble() * (this.count().toDouble() / maxSamplesCount.toDouble())).roundToInt() + this[targetIndex] + } + } else { + this + } + + return result.toPersistentList() +} diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png index e2de1a2da9..2ec7b91240 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18378d89af53ac54931d3feb3129ff90e0e45c707144322de3d8740fd3ce3645 -size 6436 +oid sha256:e9ea30725d03749028f907e341ee09bb98a67cc6bda76757f7b3d266fed3722f +size 7177 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png index 663dc09151..f189640668 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d95ae8cc103ad50397c804ba2fe16c3b4fb380456f2b3be6f99ec62877b36758 -size 6429 +oid sha256:52ae85fdd9eeb0e3d81336c38147d8251079e2ae61364ca7f7cd573676dc2c57 +size 7085 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png index e6c7409c2e..7f1bd43732 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10058823be0d5f0a23e1712d4523188729dd7579005a48a63e8e553baea105c0 -size 5936 +oid sha256:da00e2729bcd8ac55de08a2ff2e101e6746a64a5fdc07c8ddbb8297543dc493f +size 6663 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png index ad49bcfa8d..c18ddb3953 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12eacc1babdc55ec2faed630889157ea0c9e6dca8845ecc93bc72bd645f23a9e -size 6481 +oid sha256:d44ed3c1ff4607171e21c580f337ca0a5664992fb90d2072b511af8e72ac8353 +size 7070 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png index 80ae6763ba..1a1c09685c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e4ee91fc8599e4c3e1d093587a9688c36df971a94bb832e6662c86572fb08c7 -size 6436 +oid sha256:7eb5e7f99047da383a6bc5a45eb76bd902c21fcede196626fc4f6f5507097d51 +size 7333 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png index 1078abcc5a..aa9de6ea89 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2649d3974b096f17a6e80d728b08409c0f1b28ed649f33c7798f6223f55ca29 -size 6343 +oid sha256:d4a2824fdc49adb0e3fc155e7da319414544a87dc083bdece78e85ba72669d78 +size 7155 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png index b2a3aef0c5..4e5b8e146d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7841691e0bbcfa0655304ac6a7c0ea24353aae9bfc264dc7978c2a4526781ee3 -size 6357 +oid sha256:3817e348ba2775528e3805ad6f5eed044f82b241f73692180c0638c95fb79fa2 +size 7076 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png index a0e382a78c..32b8d33a13 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1a1ad6a3c02f24a8b811197f6999d93db57d3c1e9b867638c88f4759e9f1b87 -size 5926 +oid sha256:fd21f1c5ad2616b698a386d78d5efe0ab40aa1959382ed2a568c10441d093a8f +size 6692 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png index 5ec0a0c07f..dbee328561 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0adbfdd9131913c11d7e4777b4bdfbda84b3a9c6f1784781698d9208023e9c39 -size 6400 +oid sha256:07f52161378dfab92ffb76b860680390ccb5dfcc05347c5fd99504c806dd3210 +size 6930 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png index 510775c570..d997afeeb6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf18143a576ef0a136ca6e07c51521f5d41b6b5f381d168ff311cdf099752550 -size 6349 +oid sha256:4adce5249b20bdc9ed6f4d5f066140e8c9b0f62d04ebf953704a106a6ca2ff53 +size 7178 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png index fad12e2edc..6b0bdb01df 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:216e36042ad1ef11331d8610ab32383112bde38e314ae0d8c42d23931c7874a2 -size 43748 +oid sha256:5c52825ef36b00ee13496c23db981c226a3f71c0d1755508835b4ab3a140cff6 +size 46402 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png index 8c7ece47b1..d76cf9b40d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2542c95c067d4d65e76ea606801dc3dbfc196d8128a8547ed3e1bd93b84d4ab9 -size 42513 +oid sha256:ceb0cfa1b7ac95de658e339d29b4e18d8e4673d1a269db3d46bc6fd97381e9c2 +size 45460 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png index fb4d22eff4..e762d06dd7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efc60fcfb39718bfce62f16e82a0c85249b71bf850b2d996bb1234a882261bc9 -size 10021 +oid sha256:dbe3214d0b8c497f563957c9df870aa7b6cbf981e174a7f9481cc1555af50533 +size 10247 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png index dfc884aa45..b4f6fc2a60 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6f5b1b3ce84e262eec2f5fea04e4d8d15d0ec4dabd589b73b8b0c0b41afb514 -size 9754 +oid sha256:876ac7c1ac9f3a188bd4ebcc4faef6fd363e166b1ec3d5a844872f7a0cf676aa +size 9969 From a7cfb610b19e3b9d9875169338475aef232e55af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 07:37:33 +0200 Subject: [PATCH 3/4] Update dependency com.google.firebase:firebase-bom to v32.4.1 (#1657) 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 d17bcf36bc..41eba3a713 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.4.0" +google_firebase_bom = "com.google.firebase:firebase-bom:32.4.1" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } From 515dca86b67df7520d851c206c4660af0aa7264c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 08:12:44 +0200 Subject: [PATCH 4/4] Update plugin com.google.firebase.appdistribution to v4.0.1 (#1656) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 324da8447d..c8e11927d1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,7 +27,7 @@ plugins { alias(libs.plugins.anvil) alias(libs.plugins.ksp) alias(libs.plugins.kapt) - id("com.google.firebase.appdistribution") version "4.0.0" + id("com.google.firebase.appdistribution") version "4.0.1" id("org.jetbrains.kotlinx.knit") version "0.4.0" id("kotlin-parcelize") // To be able to update the firebase.xml files, uncomment and build the project