Merge branch 'develop' of https://github.com/vector-im/element-x-android into dla/feature/custom_room_notification_settings_list

This commit is contained in:
David Langley 2023-10-18 22:07:14 +01:00
commit 87b8bfe99d
1981 changed files with 15504 additions and 5063 deletions

View file

@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.impl.notificationsettings.RustNotific
import io.element.android.libraries.matrix.impl.oidc.toRustAction
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
@ -114,6 +115,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 isLoggingOut = AtomicBoolean(false)
@ -124,7 +126,16 @@ class RustMatrixClient constructor(
Timber.v("didReceiveAuthError -> do the cleanup")
//TODO handle isSoftLogout parameter.
appCoroutineScope.launch {
doLogout(doRequest = false)
val existingData = sessionStore.getSession(client.userId())
if (existingData != null) {
// Set isTokenValid to false
val newData = client.session().toSessionData(
isTokenValid = false,
loginType = existingData.loginType,
)
sessionStore.updateData(newData)
}
doLogout(doRequest = false, removeSession = false)
}
} else {
Timber.v("didReceiveAuthError -> already cleaning up")
@ -134,7 +145,12 @@ class RustMatrixClient constructor(
override fun didRefreshTokens() {
Timber.w("didRefreshTokens()")
appCoroutineScope.launch {
sessionStore.updateData(client.session().toSessionData())
val existingData = sessionStore.getSession(client.userId()) ?: return@launch
val newData = client.session().toSessionData(
isTokenValid = existingData.isTokenValid,
loginType = existingData.loginType,
)
sessionStore.updateData(newData)
}
}
}
@ -185,13 +201,15 @@ class RustMatrixClient constructor(
systemClock = clock,
roomContentForwarder = roomContentForwarder,
sessionData = sessionStore.getSession(sessionId.value)!!,
roomSyncSubscriber = roomSyncSubscriber
)
}
}
private fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.fullRoom()
// Keep using fullRoomBlocking for now as it's faster.
val fullRoom = cachedRoomListItem?.fullRoomBlocking()
return if (cachedRoomListItem == null || fullRoom == null) {
Timber.d("No room cached for $roomId")
null
@ -281,10 +299,9 @@ class RustMatrixClient constructor(
runCatching { client.setDisplayName(displayName) }
}
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit> =
withContext(sessionDispatcher) {
runCatching { client.uploadAvatar(mimeType, data.toUByteArray().toList()) }
runCatching { client.uploadAvatar(mimeType, data) }
}
override suspend fun removeAvatar(): Result<Unit> =
@ -292,7 +309,6 @@ class RustMatrixClient constructor(
runCatching { client.removeAvatar() }
}
override fun syncService(): SyncService = rustSyncService
override fun sessionVerificationService(): SessionVerificationService = verificationService
@ -326,9 +342,9 @@ class RustMatrixClient constructor(
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
}
override suspend fun logout(): String? = doLogout(doRequest = true)
override suspend fun logout(): String? = doLogout(doRequest = true, removeSession = true)
private suspend fun doLogout(doRequest: Boolean): String? {
private suspend fun doLogout(doRequest: Boolean, removeSession: Boolean): String? {
var result: String? = null
withContext(sessionDispatcher) {
if (doRequest) {
@ -340,7 +356,9 @@ class RustMatrixClient constructor(
}
close()
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
sessionStore.removeSession(sessionId.value)
if (removeSession) {
sessionStore.removeSession(sessionId.value)
}
}
return result
}
@ -364,10 +382,9 @@ class RustMatrixClient constructor(
}
}
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> = withContext(sessionDispatcher) {
runCatching {
client.uploadMedia(mimeType, data.toUByteArray().toList(), progressCallback?.toProgressWatcher())
client.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher())
}
}

View file

@ -20,18 +20,19 @@ import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException
fun Throwable.mapAuthenticationException(): AuthenticationException {
val message = this.message ?: "Unknown error"
return when (this) {
is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(this.message!!)
is RustAuthenticationException.Generic -> AuthenticationException.Generic(this.message!!)
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message!!)
is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message!!)
else -> AuthenticationException.Generic(this.message ?: "Unknown error")
is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(message)
is RustAuthenticationException.Generic -> AuthenticationException.Generic(message)
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(message)
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(message)
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message)
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message)
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message)
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message)
is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message)
is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message)
else -> AuthenticationException.Generic(message)
}
}

View file

@ -30,6 +30,8 @@ import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -62,7 +64,7 @@ class RustMatrixAuthenticationService @Inject constructor(
)
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
override fun isLoggedIn(): Flow<Boolean> {
override fun loggedInStateFlow(): Flow<LoggedInState> {
return sessionStore.isLoggedIn()
}
@ -74,7 +76,11 @@ class RustMatrixAuthenticationService @Inject constructor(
runCatching {
val sessionData = sessionStore.getSession(sessionId.value)
if (sessionData != null) {
rustMatrixClientFactory.create(sessionData)
if (sessionData.isTokenValid) {
rustMatrixClientFactory.create(sessionData)
} else {
error("Token is not valid")
}
} else {
error("No session to restore with id $sessionId")
}
@ -102,7 +108,12 @@ class RustMatrixAuthenticationService @Inject constructor(
withContext(coroutineDispatchers.io) {
runCatching {
val client = authService.login(username, password, "Element X Android", null)
val sessionData = client.use { it.session().toSessionData() }
val sessionData = client.use {
it.session().toSessionData(
isTokenValid = true,
loginType = LoginType.PASSWORD,
)
}
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
}.mapFailure { failure ->
@ -144,7 +155,12 @@ class RustMatrixAuthenticationService @Inject constructor(
runCatching {
val urlForOidcLogin = pendingOidcAuthenticationData ?: error("You need to call `getOidcUrl()` first")
val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
val sessionData = client.use { it.session().toSessionData() }
val sessionData = client.use {
it.session().toSessionData(
isTokenValid = true,
loginType = LoginType.OIDC,
)
}
pendingOidcAuthenticationData?.close()
pendingOidcAuthenticationData = null
sessionStore.storeData(sessionData)

View file

@ -16,11 +16,15 @@
package io.element.android.libraries.matrix.impl.mapper
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
import org.matrix.rustcomponents.sdk.Session
import java.util.Date
internal fun Session.toSessionData() = SessionData(
internal fun Session.toSessionData(
isTokenValid: Boolean,
loginType: LoginType,
) = SessionData(
userId = userId,
deviceId = deviceId,
accessToken = accessToken,
@ -29,4 +33,6 @@ internal fun Session.toSessionData() = SessionData(
oidcData = oidcData,
slidingSyncProxy = slidingSyncProxy,
loginTimestamp = Date(),
isTokenValid = isTokenValid,
loginType = loginType,
)

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.media
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource
@ -77,7 +78,7 @@ class RustMediaLoader(
val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource,
body = body,
mimeType = mimeType ?: "application/octet-stream",
mimeType = mimeType ?: MimeTypes.OctetStream,
tempDir = cacheDirectory.path,
)
RustMediaFile(mediaFile)

View file

@ -94,6 +94,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
}
MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction
MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker
is MessageLikeEventContent.Poll -> NotificationContent.MessageLike.Poll(senderId, question)
}
}
}

View file

@ -27,7 +27,6 @@ import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener
import org.matrix.rustcomponents.sdk.genTransactionId
import kotlin.time.Duration.Companion.milliseconds
/**
@ -61,7 +60,7 @@ class RoomContentForwarder(
// Sending a message requires a registered timeline listener
targetRoom.addTimelineListener(NoOpTimelineListener)
withTimeout(timeoutMs.milliseconds) {
targetRoom.send(content, genTransactionId())
targetRoom.send(content)
}
}
// After sending, we remove the timeline

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.RoomSubscription
import timber.log.Timber
class RoomSyncSubscriber(
private val roomListService: RoomListService,
private val dispatchers: CoroutineDispatchers,
) {
private val subscriptionCounts = HashMap<RoomId, Int>()
private val mutex = Mutex()
private val settings = RoomSubscription(
requiredState = listOf(
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""),
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
),
timelineLimit = null
)
suspend fun subscribe(roomId: RoomId) = mutex.withLock {
withContext(dispatchers.io) {
try {
val currentSubscription = subscriptionCounts.getOrElse(roomId) { 0 }
if (currentSubscription == 0) {
Timber.d("Subscribing to room $roomId}")
roomListService.room(roomId.value).use { roomListItem ->
roomListItem.subscribe(settings)
}
}
subscriptionCounts[roomId] = currentSubscription + 1
} catch (exception: Exception) {
Timber.e("Failed to subscribe to room $roomId")
}
}
}
suspend fun unsubscribe(roomId: RoomId) = mutex.withLock {
withContext(dispatchers.io) {
try {
val currentSubscription = subscriptionCounts.getOrElse(roomId) { 0 }
when (currentSubscription) {
0 -> return@withContext
1 -> {
Timber.d("Unsubscribe from room $roomId")
roomListService.room(roomId.value).use { roomListItem ->
roomListItem.unsubscribe()
}
}
}
subscriptionCounts[roomId] = currentSubscription - 1
} catch (exception: Exception) {
Timber.e("Failed to unsubscribe from room $roomId")
}
}
}
}

View file

@ -40,7 +40,6 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
import io.element.android.libraries.matrix.impl.media.map
@ -55,19 +54,17 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
import org.matrix.rustcomponents.sdk.RoomSubscription
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.genTransactionId
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import timber.log.Timber
@ -84,6 +81,7 @@ class RustMatrixRoom(
private val systemClock: SystemClock,
private val roomContentForwarder: RoomContentForwarder,
private val sessionData: SessionData,
private val roomSyncSubscriber: RoomSyncSubscriber,
) : MatrixRoom {
override val roomId = RoomId(innerRoom.id())
@ -118,27 +116,15 @@ class RustMatrixRoom(
override val timeline: MatrixTimeline = _timeline
override fun subscribeToSync() {
val settings = RoomSubscription(
requiredState = listOf(
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""),
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
),
timelineLimit = null
)
roomListItem.subscribe(settings)
}
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
override fun unsubscribeFromSync() {
roomListItem.unsubscribe()
}
override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId)
override fun destroy() {
roomCoroutineScope.cancel()
innerRoom.destroy()
roomListItem.destroy()
specialModeEventTimelineItem?.destroy()
}
override val name: String?
@ -193,7 +179,7 @@ class RustMatrixRoom(
while (true) {
// Loading the whole membersIterator as a stop-gap measure.
// We should probably implement some sort of paging in the future.
yield()
ensureActive()
addAll(membersIterator.nextChunk(1000u) ?: break)
}
}
@ -241,10 +227,9 @@ class RustMatrixRoom(
}
override suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
val transactionId = genTransactionId()
messageEventContentFromParts(body, htmlBody).use { content ->
runCatching {
innerRoom.send(content, transactionId)
innerRoom.send(content)
}
}
}
@ -253,26 +238,46 @@ class RustMatrixRoom(
withContext(roomDispatcher) {
if (originalEventId != null) {
runCatching {
innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value, transactionId?.value)
val editedEvent = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(originalEventId.value)
editedEvent.use {
innerRoom.edit(
newContent = messageEventContentFromParts(body, htmlBody),
editItem = it,
)
}
specialModeEventTimelineItem = null
}
} else {
runCatching {
transactionId?.let { cancelSend(it) }
innerRoom.send(messageEventContentFromParts(body, htmlBody), genTransactionId())
innerRoom.send(messageEventContentFromParts(body, htmlBody))
}
}
}
private var specialModeEventTimelineItem: EventTimelineItem? = null
override suspend fun enterSpecialMode(eventId: EventId?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
specialModeEventTimelineItem?.destroy()
specialModeEventTimelineItem = null
specialModeEventTimelineItem = eventId?.let { innerRoom.getEventTimelineItemByEventId(it.value) }
}
}
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventId.value, genTransactionId())
val inReplyTo = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(eventId.value)
inReplyTo.use { eventTimelineItem ->
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventTimelineItem)
}
specialModeEventTimelineItem = null
}
}
override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) {
val transactionId = genTransactionId()
runCatching {
innerRoom.redact(eventId.value, reason, transactionId)
innerRoom.redact(eventId.value, reason)
}
}
@ -368,10 +373,9 @@ class RustMatrixRoom(
}
}
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.uploadAvatar(mimeType, data.toUByteArray().toList())
innerRoom.uploadAvatar(mimeType, data, null)
}
}
@ -416,7 +420,6 @@ class RustMatrixRoom(
description = description,
zoomLevel = zoomLevel?.toUByte(),
assetType = assetType?.toInner(),
txnId = genTransactionId(),
)
}
}
@ -433,7 +436,6 @@ class RustMatrixRoom(
answers = answers,
maxSelections = maxSelections.toUByte(),
pollKind = pollKind.toInner(),
txnId = genTransactionId(),
)
}
}
@ -446,7 +448,6 @@ class RustMatrixRoom(
innerRoom.sendPollResponse(
pollStartId = pollStartId.value,
answers = answers,
txnId = genTransactionId(),
)
}
}
@ -459,11 +460,24 @@ class RustMatrixRoom(
innerRoom.endPoll(
pollStartId = pollStartId.value,
text = text,
txnId = genTransactionId(),
)
}
}
override suspend fun sendVoiceMessage(
file: File,
audioInfo: AudioInfo,
waveform: List<Int>,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler> = sendAttachment(listOf(file)) {
innerRoom.sendVoiceMessage(
url = file.path,
audioInfo = audioInfo.map(),
waveform = waveform.map { it.toUShort() },
progressWatcher = progressCallback?.toProgressWatcher(),
)
}
private suspend fun sendAttachment(files: List<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> {
return runCatching {
MediaUploadHandlerImpl(files, handle())

View file

@ -23,14 +23,14 @@ import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.catch
import org.matrix.rustcomponents.sdk.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListInterface
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
@ -40,7 +40,7 @@ import timber.log.Timber
private const val SYNC_INDICATOR_DELAY_BEFORE_SHOWING = 1000u
private const val SYNC_INDICATOR_DELAY_BEFORE_HIDING = 0u
fun RoomList.loadingStateFlow(): Flow<RoomListLoadingState> =
fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
mxCallbackFlow {
val listener = object : RoomListLoadingStateListener {
override fun onUpdate(state: RoomListLoadingState) {
@ -58,7 +58,7 @@ fun RoomList.loadingStateFlow(): Flow<RoomListLoadingState> =
Timber.d(it, "loadingStateFlow() failed")
}.buffer(Channel.UNLIMITED)
fun RoomList.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<List<RoomListEntriesUpdate>> =
fun RoomListInterface.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<List<RoomListEntriesUpdate>> =
mxCallbackFlow {
val listener = object : RoomListEntriesListener {
override fun onUpdate(roomEntriesUpdate: List<RoomListEntriesUpdate>) {
@ -76,7 +76,7 @@ fun RoomList.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit):
Timber.d(it, "entriesFlow() failed")
}.buffer(Channel.UNLIMITED)
fun RoomListService.stateFlow(): Flow<RoomListServiceState> =
fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
mxCallbackFlow {
val listener = object : RoomListServiceStateListener {
override fun onUpdate(state: RoomListServiceState) {
@ -88,7 +88,7 @@ fun RoomListService.stateFlow(): Flow<RoomListServiceState> =
}
}.buffer(Channel.UNLIMITED)
fun RoomListService.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
mxCallbackFlow {
val listener = object : RoomListServiceSyncIndicatorListener {
override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) {
@ -104,7 +104,7 @@ fun RoomListService.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
}
}.buffer(Channel.UNLIMITED)
fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
return try {
room(roomId)
} catch (exception: Exception) {

View file

@ -26,14 +26,14 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.util.UUID
class RoomSummaryListProcessor(
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
private val roomListService: RoomListService,
private val roomListService: RoomListServiceInterface,
private val dispatcher: CoroutineDispatcher,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) {
@ -73,7 +73,7 @@ class RoomSummaryListProcessor(
}
}
private fun MutableList<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) {
private suspend fun MutableList<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) {
when (update) {
is RoomListEntriesUpdate.Append -> {
val roomSummaries = update.values.map {
@ -119,7 +119,7 @@ class RoomSummaryListProcessor(
}
}
private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
private suspend fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
return when (entry) {
RoomListEntry.Empty -> buildEmptyRoomSummary()
is RoomListEntry.Filled -> buildAndCacheRoomSummaryForIdentifier(entry.roomId)
@ -133,9 +133,9 @@ class RoomSummaryListProcessor(
return RoomSummary.Empty(UUID.randomUUID().toString())
}
private fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem ->
roomListItem.roomInfoBlocking().use { roomInfo ->
roomListItem.roomInfo().use { roomInfo ->
RoomSummary.Filled(
details = roomSummaryDetailsFactory.create(roomInfo)
)

View file

@ -0,0 +1,103 @@
/*
* 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.auth
import com.google.common.truth.ThrowableSubject
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.junit.Test
import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException
class AuthenticationExceptionMappingTest {
@Test
fun `mapping an exception with no message returns 'Unknown error' message`() {
val exception = Exception()
val mappedException = exception.mapAuthenticationException()
assertThat(mappedException.message).isEqualTo("Unknown error")
}
@Test
fun `mapping a generic exception returns a Generic AuthenticationException`() {
val exception = Exception("Generic exception")
val mappedException = exception.mapAuthenticationException()
assertThat(mappedException).isException<AuthenticationException.Generic>("Generic exception")
}
@Test
fun `mapping specific exceptions map to their kotlin counterparts`() {
assertThat(RustAuthenticationException.ClientMissing("Client missing").mapAuthenticationException())
.isException<AuthenticationException.ClientMissing>("Client missing")
assertThat(RustAuthenticationException.Generic("Generic").mapAuthenticationException()).isException<AuthenticationException.Generic>("Generic")
assertThat(RustAuthenticationException.InvalidServerName("Invalid server name").mapAuthenticationException())
.isException<AuthenticationException.InvalidServerName>("Invalid server name")
assertThat(RustAuthenticationException.SessionMissing("Session missing").mapAuthenticationException())
.isException<AuthenticationException.SessionMissing>("Session missing")
assertThat(RustAuthenticationException.SlidingSyncNotAvailable("Sliding sync not available").mapAuthenticationException())
.isException<AuthenticationException.SlidingSyncNotAvailable>("Sliding sync not available")
}
@Test
fun `mapping Oidc related exceptions creates an 'OidcError' with different types`() {
assertIsOidcError(
throwable = RustAuthenticationException.OidcException("Oidc exception"),
type = "OidcException",
message = "Oidc exception"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcMetadataInvalid("Oidc metadata invalid"),
type = "OidcMetadataInvalid",
message = "Oidc metadata invalid"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcMetadataMissing("Oidc metadata missing"),
type = "OidcMetadataMissing",
message = "Oidc metadata missing"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcNotSupported("Oidc not supported"),
type = "OidcNotSupported",
message = "Oidc not supported"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcCancelled("Oidc cancelled"),
type = "OidcCancelled",
message = "Oidc cancelled"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcCallbackUrlInvalid("Oidc callback url invalid"),
type = "OidcCallbackUrlInvalid",
message = "Oidc callback url invalid"
)
}
private inline fun <reified T> ThrowableSubject.isException(message: String) {
isInstanceOf(T::class.java)
hasMessageThat().isEqualTo(message)
}
private fun assertIsOidcError(throwable: Throwable, type: String, message: String) {
val authenticationException = throwable.mapAuthenticationException()
assertThat(authenticationException).isInstanceOf(AuthenticationException.OidcError::class.java)
assertThat((authenticationException as? AuthenticationException.OidcError)?.type).isEqualTo(type)
assertThat(authenticationException.message).isEqualTo(message)
}
}

View file

@ -0,0 +1,249 @@
/*
* 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.roomlist
import com.google.common.truth.Truth.assertThat
import com.sun.jna.Pointer
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import org.junit.Test
import org.matrix.rustcomponents.sdk.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListInput
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener
import org.matrix.rustcomponents.sdk.TaskHandle
import kotlin.time.Duration.Companion.milliseconds
// NOTE: this class is using a fake implementation of a Rust SDK interface which returns actual Rust objects with pointers.
// Since we don't access the data in those objects, this is fine for our tests, but that's as far as we can test this class.
class RoomSummaryListProcessorTests {
private val summaries = MutableStateFlow<List<RoomSummary>>(emptyList())
@Test
fun `postUpdates can't start until postEntries is done`() = runTest {
val processor = createProcessor()
val update = listOf(RoomListEntriesUpdate.Reset(emptyList()))
val timeoutError = runCatching {
withTimeout(10.milliseconds) { processor.postUpdate(update) }
}.exceptionOrNull()
assertThat(timeoutError).isInstanceOf(CancellationException::class.java)
processor.postEntries(listOf(RoomListEntry.Empty))
processor.postUpdate(update)
}
@Test
fun `postEntries adds all new entries with no diffing`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
processor.postEntries(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty))
assertThat(summaries.value.count()).isEqualTo(4)
}
@Test
fun `Append adds new entries at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty))))
assertThat(summaries.value.count()).isEqualTo(4)
assertThat(summaries.value.subList(1, 4).all { it is RoomSummary.Empty }).isTrue()
}
@Test
fun `PushBack adds a new entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(2)
assertThat(summaries.value.last()).isInstanceOf(RoomSummary.Empty::class.java)
}
@Test
fun `PushFront inserts a new entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(2)
assertThat(summaries.value.first()).isInstanceOf(RoomSummary.Empty::class.java)
}
@Test
fun `Set replaces an entry at some index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(1)
assertThat(summaries.value[index]).isInstanceOf(RoomSummary.Empty::class.java)
}
@Test
fun `Insert inserts a new entry at the provided index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), RoomListEntry.Empty)))
assertThat(summaries.value.count()).isEqualTo(2)
assertThat(summaries.value[index]).isInstanceOf(RoomSummary.Empty::class.java)
}
@Test
fun `Remove removes an entry at some index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Remove(index.toUInt())))
assertThat(summaries.value.count()).isEqualTo(1)
assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID_2.value)
}
@Test
fun `PopBack removes an entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PopBack))
assertThat(summaries.value.count()).isEqualTo(1)
assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID.value)
}
@Test
fun `PopFront removes an entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.PopFront))
assertThat(summaries.value.count()).isEqualTo(1)
assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID_2.value)
}
@Test
fun `Clear removes all the entries`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
val processor = createProcessor()
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Clear))
assertThat(summaries.value).isEmpty()
}
@Test
fun `Truncate removes all entries after the provided length`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
val processor = createProcessor()
val index = 0
// Start processing updates
processor.postEntries(listOf())
// Process actual update
processor.postUpdate(listOf(RoomListEntriesUpdate.Truncate(1u)))
assertThat(summaries.value.count()).isEqualTo(1)
assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID.value)
}
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
summaries,
fakeRoomListService,
dispatcher = StandardTestDispatcher(testScheduler),
roomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
)
// Fake room list service that returns Rust objects with null pointers. Luckily for us, they don't crash for our test cases
private val fakeRoomListService = object : RoomListServiceInterface {
override suspend fun allRooms(): RoomList {
return RoomList(Pointer.NULL)
}
override suspend fun applyInput(input: RoomListInput) = Unit
override suspend fun invites(): RoomList {
return RoomList(Pointer.NULL)
}
override fun room(roomId: String): RoomListItem {
return RoomListItem(Pointer.NULL)
}
override fun state(listener: RoomListServiceStateListener): TaskHandle {
return TaskHandle(Pointer.NULL)
}
override fun syncIndicator(delayBeforeShowingInMs: UInt, delayBeforeHidingInMs: UInt, listener: RoomListServiceSyncIndicatorListener): TaskHandle {
return TaskHandle(Pointer.NULL)
}
}
}