Merge branch 'develop' of https://github.com/vector-im/element-x-android into langleyd/live_waveform

This commit is contained in:
David Langley
2023-10-27 08:44:25 +01:00
40 changed files with 608 additions and 106 deletions

View File

@@ -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

View File

@@ -142,7 +142,7 @@ class PinUnlockPresenter @Inject constructor(
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
suspend {
matrixClient.logout()
matrixClient.logout(ignoreSdkError = true)
}.runCatchingUpdatingState(signOutAction)
}
}

View File

@@ -58,7 +58,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
private fun CoroutineScope.logout(logoutAction: MutableState<Async<String?>>) = launch {
suspend {
matrixClient.logout()
matrixClient.logout(false /* TODO */)
}.runCatchingUpdatingState(logoutAction)
}
}

View File

@@ -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),

View File

@@ -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,

View File

@@ -27,7 +27,7 @@ data class TimelineItemVoiceContent(
val duration: Duration,
val mediaSource: MediaSource,
val mimeType: String,
val waveform: ImmutableList<Int>,
val waveform: ImmutableList<Float>,
) : TimelineItemEventContent {
override val type: String = "TimelineItemAudioContent"
}

View File

@@ -32,11 +32,11 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider<TimelineI
),
aTimelineItemVoiceContent(
durationMs = 10_000,
waveform = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0),
waveform = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f),
),
aTimelineItemVoiceContent(
durationMs = 1_800_000, // 30 minutes
waveform = List(1024) { it },
waveform = List(1024) { it / 1024f },
),
)
}
@@ -47,7 +47,7 @@ fun aTimelineItemVoiceContent(
durationMs: Long = 61_000,
contentUri: String = "mxc://matrix.org/1234567890abcdefg",
mimeType: String = MimeTypes.Ogg,
waveform: List<Int> = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0),
waveform: List<Float> = 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,

View File

@@ -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<Int>.fromMSC3246range(): List<Float> = map { it / 1024f }

View File

@@ -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" }

View File

@@ -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<Int>
) {
companion object {
private val dataRange = 0..1024
}
fun normalisedData(maxSamplesCount: Int): ImmutableList<Float> {
if(maxSamplesCount <= 0) {
return persistentListOf()
}
// Filter the data to keep only the expected number of samples
val result = if (data.size > maxSamplesCount) {
(0..<maxSamplesCount)
.map { index ->
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()
}
}

View File

@@ -46,21 +46,24 @@ 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.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<Float>,
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,
) {
@@ -134,7 +137,7 @@ fun WaveformPlaybackView(
),
blendMode = BlendMode.SrcAtop
)
if(showCursor || seekProgress.value != null) {
if (showCursor || seekProgress.value != null) {
drawRoundRect(
brush = cursorBrush,
topLeft = Offset(
@@ -155,24 +158,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<Float>.normalisedData(maxSamplesCount: Int): ImmutableList<Float> {
if (maxSamplesCount <= 0) {
return persistentListOf()
}
// Filter the data to keep only the expected number of samples
val result = if (this.size > maxSamplesCount) {
(0..<maxSamplesCount)
.map { index ->
val targetIndex = (index.toDouble() * (this.count().toDouble() / maxSamplesCount.toDouble())).roundToInt()
this[targetIndex]
}
} else {
this
}
return result.toPersistentList()
}

View File

@@ -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<String>
suspend fun loadUserAvatarURLString(): Result<String?>
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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<BackupState>
val recoveryStateStateFlow: StateFlow<RecoveryState>
val backupUploadStateStateFlow: StateFlow<BackupUploadState>
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
suspend fun enableBackups(): Result<Unit>
suspend fun isLastDevice(): Result<Boolean>
/**
* Enable recovery. Observe enableProgressStateFlow to get progress and recovery key.
*/
suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result<Unit>
/**
* Change the recovery and return the new recovery key.
*/
suspend fun resetRecoveryKey(): Result<String>
suspend fun disableRecovery(): Result<Unit>
/**
* Note: accept bot recoveryKey and passphrase.
*/
suspend fun fixRecoveryIssues(recoveryKey: String): Result<Unit>
/**
* Observe [backupUploadStateStateFlow] to get progress.
*/
suspend fun waitForBackupUploadSteadyState(): Result<Unit>
}

View File

@@ -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,
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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<BackupState> = MutableStateFlow(service.backupState().let(backupStateMapper::map))
override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map))
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Unknown)
override val backupUploadStateStateFlow: MutableStateFlow<BackupUploadState> = 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<Unit> = withContext(dispatchers.io) {
runCatching {
service.enableBackups()
}
}
override suspend fun enableRecovery(
waitForBackupsToUpload: Boolean,
): Result<Unit> = 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<Unit> = withContext(dispatchers.io) {
runCatching {
service.waitForBackupUploadSteadyState(
progressListener = object : BackupSteadyStateListener {
override fun onUpdate(status: RustBackupUploadState) {
backupUploadStateStateFlow.value = backupUploadStateMapper.map(status)
}
}
)
}
}
override suspend fun disableRecovery(): Result<Unit> = withContext(dispatchers.io) {
runCatching {
service.disableRecovery()
}
}
override suspend fun isLastDevice(): Result<Boolean> = withContext(dispatchers.io) {
runCatching {
service.isLastDevice()
}
}
override suspend fun resetRecoveryKey(): Result<String> = withContext(dispatchers.io) {
runCatching {
service.resetRecoveryKey()
}
}
override suspend fun fixRecoveryIssues(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
runCatching {
service.fixRecoveryIssues(recoveryKey)
}
}
}

View File

@@ -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 {

View File

@@ -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<String?> = 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()

View File

@@ -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"

View File

@@ -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<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(RecoveryState.UNKNOWN)
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Unknown)
override val backupUploadStateStateFlow: MutableStateFlow<BackupUploadState> = MutableStateFlow(BackupUploadState.Unknown)
private var fixRecoveryIssuesFailure: Exception? = null
override suspend fun enableBackups(): Result<Unit> = simulateLongTask {
return Result.success(Unit)
}
fun givenDisableRecoveryFailure(exception: Exception) {
disableRecoveryFailure = exception
}
fun givenFixRecoveryIssuesFailure(exception: Exception?) {
fixRecoveryIssuesFailure = exception
}
override suspend fun disableRecovery(): Result<Unit> = simulateLongTask {
disableRecoveryFailure?.let { return Result.failure(it) }
return Result.success(Unit)
}
override suspend fun fixRecoveryIssues(recoveryKey: String): Result<Unit> = 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<Boolean> {
return Result.success(isLastDevice)
}
override suspend fun resetRecoveryKey(): Result<String> = simulateLongTask {
return Result.success(fakeRecoveryKey)
}
override suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result<Unit> = simulateLongTask {
return Result.success(Unit)
}
override suspend fun waitForBackupUploadSteadyState(): Result<Unit> {
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"
}
}