mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Merge
This commit is contained in:
+12
-2
@@ -8,9 +8,19 @@
|
||||
package io.element.android.libraries.matrix.api
|
||||
|
||||
/**
|
||||
* An interface to fetch content from a URL using a simple GET request.
|
||||
* An interface to fetch content from a URL using a simple GET request launched from a Matrix client.
|
||||
*/
|
||||
fun interface UrlContentFetcher {
|
||||
interface ClientUrlContentFetcher {
|
||||
/**
|
||||
* The domain of the public server for this homeserver.
|
||||
*/
|
||||
val server: String?
|
||||
|
||||
/**
|
||||
* The URL of the actual Matrix homeserver.
|
||||
*/
|
||||
val homeserverUrl: String
|
||||
|
||||
/**
|
||||
* Execute generic GET requests through the SDKs internal HTTP client.
|
||||
*/
|
||||
+7
-4
@@ -60,16 +60,13 @@ import java.util.Optional
|
||||
* One instance exists per session, created on login or session restore and torn down by [logout], [clearCache] or account deactivation.
|
||||
* Suspending members here report failures through a [Result] instead of throwing, and run their work off the main thread.
|
||||
*/
|
||||
interface MatrixClient : UrlContentFetcher {
|
||||
interface MatrixClient : ClientUrlContentFetcher {
|
||||
/** Identifier of the logged in user; a [SessionId] and a [UserId] are the same value. */
|
||||
val sessionId: SessionId
|
||||
|
||||
/** Identifier of this device, as assigned by the homeserver when the session was created. */
|
||||
val deviceId: DeviceId
|
||||
|
||||
/** Base URL of the homeserver this client is connected to. */
|
||||
val homeserverUrl: String
|
||||
|
||||
/** Directories holding this session's databases, caches and downloaded media. */
|
||||
val sessionPaths: SessionPaths
|
||||
|
||||
@@ -136,6 +133,9 @@ interface MatrixClient : UrlContentFetcher {
|
||||
/** Scans media content before it is displayed, or `null` when no content scanner is configured for this session. */
|
||||
val contentScanner: ContentScanner?
|
||||
|
||||
/** Whether the session is in the process of shutting down, either because it is being logged out or its cache is being cleared. */
|
||||
val isShuttingDown: Boolean
|
||||
|
||||
/**
|
||||
* Returns the room with the given id if the user has joined it, along with its live [Timeline].
|
||||
* A new instance is created on each call and the caller owns it, so it must be closed once no longer needed.
|
||||
@@ -245,6 +245,9 @@ interface MatrixClient : UrlContentFetcher {
|
||||
/** Whether the homeserver advertises support for user status (MSC4426). */
|
||||
suspend fun isUserStatusSupported(): Result<Boolean>
|
||||
|
||||
/** Whether the homeserver advertises support for the Profiles sliding sync extension (MSC4262). */
|
||||
suspend fun isProfilesSlidingSyncExtensionSupported(): Result<Boolean>
|
||||
|
||||
/**
|
||||
* Enable or disable automatically setting the user's status to "in a call" (m.call) while in a call.
|
||||
*
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api
|
||||
|
||||
/**
|
||||
* An abstraction over the Matrix SDK's `ClientBuilder` to allow sharing it with other modules without exposing the SDK directly.
|
||||
*/
|
||||
interface MatrixClientBuilder
|
||||
+1
-1
@@ -15,4 +15,4 @@ import java.lang.AutoCloseable
|
||||
* Note: this client implements an [AutoCloseable] interface, so using [AutoCloseable.close] or [AutoCloseable.use] will clean up any resources associated
|
||||
* with it, such as temporary files.
|
||||
*/
|
||||
interface TemporaryMatrixClient : UrlContentFetcher, AutoCloseable
|
||||
interface TemporaryMatrixClient : ClientUrlContentFetcher, AutoCloseable
|
||||
|
||||
+3
-3
@@ -12,10 +12,10 @@ package io.element.android.libraries.matrix.api
|
||||
*/
|
||||
interface TemporaryMatrixClientFactory {
|
||||
/**
|
||||
* Builds an unauthenticated client pointing at [homeServerUrl], backed by a fresh set of temporary session directories.
|
||||
* Builds an unauthenticated client pointing at [serverName], backed by a fresh set of temporary session directories.
|
||||
* The caller owns the returned client and must close it to delete those directories.
|
||||
*
|
||||
* @param homeServerUrl the URL of the homeserver the client will talk to.
|
||||
* @param serverName the domain of the public-facing server for the actual homeserver
|
||||
*/
|
||||
suspend fun create(homeServerUrl: String): Result<TemporaryMatrixClient>
|
||||
suspend fun create(serverName: String): Result<TemporaryMatrixClient>
|
||||
}
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.scanner
|
||||
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
/**
|
||||
* Provides the URL of the content scanner service for a given homeserver, if any is set up.
|
||||
*/
|
||||
fun interface ContentScannerUrlProvider {
|
||||
/**
|
||||
* Returns the URL of the content scanner service for the given [sessionId], or `null` if no content scanner is set up.
|
||||
*/
|
||||
suspend fun getContentScannerUrl(sessionId: SessionId): Result<String?>
|
||||
|
||||
/**
|
||||
* Creates a provider, which needs a fetcher because the scanner URL is itself discovered over HTTP.
|
||||
*/
|
||||
fun interface Factory {
|
||||
/**
|
||||
* @param urlContentFetcher used to read the configuration advertised by the homeserver; a client that is not authenticated yet also works.
|
||||
*/
|
||||
fun create(urlContentFetcher: UrlContentFetcher): ContentScannerUrlProvider
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -128,4 +128,11 @@ sealed interface GalleryItemType {
|
||||
data class OtherMessageType(
|
||||
val msgType: String,
|
||||
val body: String,
|
||||
) : MessageType
|
||||
) : MessageType {
|
||||
/**
|
||||
* A `m.key.verification.request` carries a body meant for clients that cannot handle in-chat
|
||||
* verification, so it must not be rendered as a message.
|
||||
*/
|
||||
val isKeyVerificationRequest: Boolean
|
||||
get() = msgType == "m.key.verification.request"
|
||||
}
|
||||
|
||||
+20
-2
@@ -135,6 +135,7 @@ import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -145,7 +146,7 @@ import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
|
||||
@Suppress("LargeClass")
|
||||
class RustMatrixClient(
|
||||
override val sessionPaths: SessionPaths,
|
||||
private val innerClient: Client,
|
||||
val innerClient: Client,
|
||||
private val sessionStore: SessionStore,
|
||||
private val sessionDelegate: RustClientSessionDelegate,
|
||||
private val innerSyncService: ClientSyncService,
|
||||
@@ -162,6 +163,7 @@ class RustMatrixClient(
|
||||
) : MatrixClient {
|
||||
override val sessionId: UserId = UserId(innerClient.userId())
|
||||
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
|
||||
override val server: String? = innerClient.server()
|
||||
override val homeserverUrl: String = innerClient.homeserver()
|
||||
override val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId")
|
||||
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
||||
@@ -304,6 +306,10 @@ class RustMatrixClient(
|
||||
.buffer(Channel.UNLIMITED)
|
||||
.stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf())
|
||||
|
||||
private val _isShuttingDown = AtomicBoolean(false)
|
||||
override val isShuttingDown: Boolean
|
||||
get() = _isShuttingDown.get()
|
||||
|
||||
init {
|
||||
// Make sure the session delegate has a reference to the client to be able to logout on auth error
|
||||
sessionDelegate.bindClient(this)
|
||||
@@ -320,7 +326,9 @@ class RustMatrixClient(
|
||||
}
|
||||
|
||||
private suspend fun setupUserProfile() {
|
||||
val supported = isUserStatusSupported().getOrDefault(false)
|
||||
// Subscribing to own profile updates only requires the Profiles sliding sync extension,
|
||||
// not the full user status capability (which also needs the status profile field to be settable).
|
||||
val supported = isProfilesSlidingSyncExtensionSupported().getOrDefault(false)
|
||||
if (supported) {
|
||||
// No need to seed the data here, it's already stored by the sdk.
|
||||
ownProfileTaskHandle = innerClient.subscribeToOwnProfile(ownProfileListener)
|
||||
@@ -530,6 +538,12 @@ class RustMatrixClient(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isProfilesSlidingSyncExtensionSupported(): Result<Boolean> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerClient.isProfilesSlidingSyncExtensionSupported()
|
||||
}
|
||||
}
|
||||
|
||||
override fun enableAutomaticCallStatus(enabled: Boolean) {
|
||||
innerClient.enableAutomaticCallStatus(enabled)
|
||||
}
|
||||
@@ -666,11 +680,13 @@ class RustMatrixClient(
|
||||
}
|
||||
|
||||
override suspend fun clearCache() {
|
||||
_isShuttingDown.set(true)
|
||||
innerClient.clearCaches(innerSyncService)
|
||||
destroy()
|
||||
}
|
||||
|
||||
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) {
|
||||
_isShuttingDown.set(true)
|
||||
sessionCoroutineScope.cancel()
|
||||
// Remove current delegate so we don't receive an auth error
|
||||
clientDelegateTaskHandle?.cancelAndDestroy()
|
||||
@@ -708,6 +724,8 @@ class RustMatrixClient(
|
||||
}
|
||||
|
||||
override suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit> = withContext(sessionDispatcher) {
|
||||
_isShuttingDown.set(true)
|
||||
|
||||
Timber.w("Deactivating account")
|
||||
// Remove current delegate so we don't receive an auth error
|
||||
clientDelegateTaskHandle?.cancelAndDestroy()
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClientBuilder
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
|
||||
/**
|
||||
* A wrapper around the Matrix SDK's [ClientBuilder] to allow sharing it with other modules without exposing the SDK directly.
|
||||
*/
|
||||
class RustMatrixClientBuilder(
|
||||
val inner: ClientBuilder,
|
||||
) : MatrixClientBuilder
|
||||
+4
-19
@@ -9,6 +9,7 @@
|
||||
package io.element.android.libraries.matrix.impl
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.enterprise.api.ClientBuilderEnterpriseHook
|
||||
import io.element.android.libraries.androidutils.crypto.ClientSecret
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.data.ByteUnit
|
||||
@@ -19,7 +20,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
|
||||
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
|
||||
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
|
||||
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
|
||||
@@ -37,7 +37,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
import org.matrix.rustcomponents.sdk.ContentScanner
|
||||
import org.matrix.rustcomponents.sdk.CrossProcessLockConfig
|
||||
import org.matrix.rustcomponents.sdk.RequestConfig
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
@@ -70,7 +69,7 @@ class RustMatrixClientFactory(
|
||||
private val clientBuilderProvider: ClientBuilderProvider,
|
||||
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
|
||||
private val workManagerScheduler: WorkManagerScheduler,
|
||||
private val contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory,
|
||||
private val clientBuilderEnterpriseHook: ClientBuilderEnterpriseHook,
|
||||
) {
|
||||
private val sessionDelegate = RustClientSessionDelegate(
|
||||
sessionStore = sessionStore,
|
||||
@@ -101,6 +100,7 @@ class RustMatrixClientFactory(
|
||||
)
|
||||
.homeserverUrl(sessionData.homeserverUrl)
|
||||
.username(sessionData.userId)
|
||||
.let { (clientBuilderEnterpriseHook(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner }
|
||||
.use { it.build() }
|
||||
|
||||
client.setMediaRetentionPolicy(
|
||||
@@ -135,21 +135,6 @@ class RustMatrixClientFactory(
|
||||
|
||||
client.setUtdDelegate(UtdTracker(analyticsService))
|
||||
|
||||
// If a content scanner URL is available for the homeserver, create a RustContentScanner and set it on the client.
|
||||
// This allows the SDK to use the content scanner for automatic media scanning.
|
||||
// If no content scanner URL is available, the contentScanner will be null.
|
||||
val contentScannerUrlProvider = contentScannerUrlProviderFactory.create(RustTemporaryMatrixClient(client, null))
|
||||
val contentScanner = contentScannerUrlProvider.getContentScannerUrl(SessionId(client.userId()))
|
||||
.getOrNull()
|
||||
?.let { contentScannerUrl ->
|
||||
val contentScanner = ContentScanner(contentScannerUrl)
|
||||
client.setContentScanner(contentScanner)
|
||||
RustContentScanner(
|
||||
client = client,
|
||||
rustScanner = contentScanner,
|
||||
)
|
||||
}
|
||||
|
||||
val syncService = client.syncService()
|
||||
.withSharePos(true)
|
||||
.withOfflineMode()
|
||||
@@ -170,7 +155,7 @@ class RustMatrixClientFactory(
|
||||
featureFlagService = featureFlagService,
|
||||
analyticsService = analyticsService,
|
||||
workManagerScheduler = workManagerScheduler,
|
||||
contentScanner = contentScanner,
|
||||
contentScanner = client.contentScanner()?.let { RustContentScanner(client, it) },
|
||||
isMessageSearchAvailable = isMessageSearchAvailable,
|
||||
).also {
|
||||
Timber.tag("RustMatrixClient").i("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
||||
|
||||
+3
@@ -22,6 +22,9 @@ class RustTemporaryMatrixClient(
|
||||
client.getUrl(url)
|
||||
}.mapFailure { it.mapClientException() }
|
||||
|
||||
override val server: String? = client.server()
|
||||
override val homeserverUrl: String = client.homeserver()
|
||||
|
||||
override fun close() {
|
||||
client.close()
|
||||
paths?.let {
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ class RustTemporaryMatrixClientFactory(
|
||||
private val sessionPathsFactory: SessionPathsFactory,
|
||||
private val rustMatrixClientFactory: RustMatrixClientFactory,
|
||||
) : TemporaryMatrixClientFactory {
|
||||
override suspend fun create(homeServerUrl: String): Result<TemporaryMatrixClient> {
|
||||
override suspend fun create(serverName: String): Result<TemporaryMatrixClient> {
|
||||
return runCatchingExceptions {
|
||||
val sessionPaths = sessionPathsFactory.create()
|
||||
val client = rustMatrixClientFactory.getBaseClientBuilder(
|
||||
@@ -28,7 +28,7 @@ class RustTemporaryMatrixClientFactory(
|
||||
slidingSyncType = ClientBuilderSlidingSync.Native,
|
||||
isMessageSearchAvailable = false,
|
||||
)
|
||||
.homeserverUrl(homeServerUrl)
|
||||
.serverName(serverName)
|
||||
.build()
|
||||
RustTemporaryMatrixClient(client, sessionPaths)
|
||||
}
|
||||
|
||||
+17
-1
@@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.auth
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.features.enterprise.api.ClientEnterpriseHook
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.androidutils.crypto.ClientSecret
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
@@ -73,6 +74,7 @@ class RustMatrixAuthenticationService(
|
||||
private val oAuthConfigurationProvider: OAuthConfigurationProvider,
|
||||
private val enterpriseService: EnterpriseService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val clientEnterpriseHook: ClientEnterpriseHook,
|
||||
) : MatrixAuthenticationService {
|
||||
// Any existing Element Classic session that we want to try to import secrets from during login.
|
||||
private var elementClassicSession: ElementClassicSession? = null
|
||||
@@ -166,6 +168,10 @@ class RustMatrixAuthenticationService(
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
||||
|
||||
// Apply enterprise hooks to the newly created client as soon as possible
|
||||
clientEnterpriseHook(matrixClient)
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
@@ -242,6 +248,9 @@ class RustMatrixAuthenticationService(
|
||||
client.restoreSession(sessionData.toSession())
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
||||
|
||||
// Apply enterprise hooks to the newly created client as soon as possible
|
||||
clientEnterpriseHook(matrixClient)
|
||||
|
||||
// We wait for the verification state to be known
|
||||
matrixClient.waitForKnownVerificationState()
|
||||
|
||||
@@ -278,7 +287,6 @@ class RustMatrixAuthenticationService(
|
||||
.let {
|
||||
enterpriseService.tweakMasUrl(
|
||||
url = it,
|
||||
homeserver = client.server() ?: client.homeserver(),
|
||||
urlContentFetcher = getUrlResolver,
|
||||
)
|
||||
}
|
||||
@@ -333,6 +341,10 @@ class RustMatrixAuthenticationService(
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
||||
|
||||
// Apply enterprise hooks to the newly created client as soon as possible
|
||||
clientEnterpriseHook(matrixClient)
|
||||
|
||||
matrixClient.waitForKnownVerificationState()
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
@@ -398,6 +410,10 @@ class RustMatrixAuthenticationService(
|
||||
sessionPaths = emptySessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
||||
|
||||
// Apply enterprise hooks to the newly created client as soon as possible
|
||||
clientEnterpriseHook(matrixClient)
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
|
||||
+8
-2
@@ -13,9 +13,9 @@ import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Provides
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
|
||||
import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
@@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchService
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
@@ -111,7 +112,12 @@ object SessionMatrixModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesGetUrlResolverClient(matrixClient: MatrixClient): UrlContentFetcher {
|
||||
fun providesGetUrlResolverClient(matrixClient: MatrixClient): ClientUrlContentFetcher {
|
||||
return matrixClient
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesMessageSearchService(matrixClient: MatrixClient): MessageSearchService {
|
||||
return matrixClient.messageSearchService
|
||||
}
|
||||
}
|
||||
|
||||
+3
-10
@@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.impl.media
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.ensureDefaultSubtype
|
||||
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
|
||||
@@ -71,15 +71,8 @@ class RustMediaLoader(
|
||||
val mediaFile = innerClient.getMediaFile(
|
||||
mediaSource = mediaSource,
|
||||
filename = filename,
|
||||
mimeType = when {
|
||||
mimeType == null -> MimeTypes.OctetStream
|
||||
MimeTypes.hasSubtype(mimeType) -> mimeType
|
||||
// Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension.
|
||||
mimeType == MimeTypes.Images -> MimeTypes.Jpeg
|
||||
mimeType == MimeTypes.Videos -> MimeTypes.Mp4
|
||||
mimeType == MimeTypes.Audio -> MimeTypes.Mp3
|
||||
else -> MimeTypes.OctetStream
|
||||
},
|
||||
// Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension.
|
||||
mimeType = mimeType.ensureDefaultSubtype(),
|
||||
useCache = useCache,
|
||||
tempDir = cacheDirectory.path,
|
||||
)
|
||||
|
||||
+1
-3
@@ -11,7 +11,6 @@ package io.element.android.libraries.matrix.impl
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
|
||||
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
||||
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
|
||||
@@ -52,7 +51,6 @@ fun TestScope.createRustMatrixClientFactory(
|
||||
),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||
contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory = { ContentScannerUrlProvider { Result.success(null) } },
|
||||
) = RustMatrixClientFactory(
|
||||
cacheDirectory = cacheDirectory,
|
||||
appCoroutineScope = backgroundScope,
|
||||
@@ -67,5 +65,5 @@ fun TestScope.createRustMatrixClientFactory(
|
||||
clientBuilderProvider = clientBuilderProvider,
|
||||
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||
workManagerScheduler = workManagerScheduler,
|
||||
contentScannerUrlProviderFactory = contentScannerUrlProviderFactory,
|
||||
clientBuilderEnterpriseHook = { builder, _ -> builder },
|
||||
)
|
||||
|
||||
+1
-3
@@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
|
||||
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
||||
@@ -64,7 +63,6 @@ class RustTemporaryMatrixClientFactoryTest {
|
||||
),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||
contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory = { ContentScannerUrlProvider { Result.success(null) } },
|
||||
) = RustMatrixClientFactory(
|
||||
cacheDirectory = cacheDirectory,
|
||||
appCoroutineScope = backgroundScope,
|
||||
@@ -79,6 +77,6 @@ class RustTemporaryMatrixClientFactoryTest {
|
||||
clientBuilderProvider = clientBuilderProvider,
|
||||
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||
workManagerScheduler = workManagerScheduler,
|
||||
contentScannerUrlProviderFactory = contentScannerUrlProviderFactory,
|
||||
clientBuilderEnterpriseHook = { builder, _ -> builder },
|
||||
)
|
||||
}
|
||||
|
||||
+1
-2
@@ -16,7 +16,6 @@ import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import java.io.File
|
||||
import kotlin.io.path.createTempDirectory
|
||||
import org.matrix.rustcomponents.sdk.ClientException as RustClientException
|
||||
@@ -84,7 +83,7 @@ class RustTemporaryMatrixClientTest {
|
||||
}
|
||||
|
||||
private fun createRustTemporaryMatrixClient(
|
||||
client: Client = Client(NoHandle),
|
||||
client: Client = FakeFfiClient(),
|
||||
paths: SessionPaths,
|
||||
): RustTemporaryMatrixClient {
|
||||
return RustTemporaryMatrixClient(client, paths)
|
||||
|
||||
+1
@@ -98,6 +98,7 @@ class RustMatrixAuthenticationServiceTest {
|
||||
),
|
||||
enterpriseService = enterpriseService,
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
clientEnterpriseHook = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -10,13 +10,16 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile
|
||||
import io.element.android.libraries.matrix.impl.scanner.FakeFfiContentScanner
|
||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.A_SERVER_NAME
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||
import org.matrix.rustcomponents.sdk.ContentScanner
|
||||
import org.matrix.rustcomponents.sdk.CreateRoomParameters
|
||||
import org.matrix.rustcomponents.sdk.Encryption
|
||||
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
|
||||
@@ -45,6 +48,7 @@ class FakeFfiClient(
|
||||
private val userId: String = A_USER_ID.value,
|
||||
private val deviceId: String = A_DEVICE_ID.value,
|
||||
private val homeserver: String = A_HOMESERVER_URL,
|
||||
private val server: String? = A_SERVER_NAME,
|
||||
private val notificationClient: NotificationClient = FakeFfiNotificationClient(),
|
||||
private val notificationSettings: NotificationSettings = FakeFfiNotificationSettings(),
|
||||
private val encryption: Encryption = FakeFfiEncryption(),
|
||||
@@ -59,13 +63,16 @@ class FakeFfiClient(
|
||||
private val accountDataResult: (String) -> String? = { lambdaError() },
|
||||
private val setAccountDataResult: (String, String) -> Unit = { _, _ -> lambdaError() },
|
||||
private val isUserStatusSupportedResult: () -> Boolean = { false },
|
||||
private val isProfilesSlidingSyncExtensionSupportedResult: () -> Boolean = { false },
|
||||
private val subscribeToOwnProfileResult: (ProfileListener) -> Unit = {},
|
||||
private val getUrlResult: (String) -> ByteArray = { lambdaError() },
|
||||
private val contentScannerResult: () -> ContentScanner = { FakeFfiContentScanner() },
|
||||
private val closeResult: () -> Unit = {},
|
||||
) : Client(NoHandle) {
|
||||
override fun userId(): String = userId
|
||||
override fun deviceId(): String = deviceId
|
||||
override fun homeserver(): String = homeserver
|
||||
override fun server(): String? = server
|
||||
override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient
|
||||
override suspend fun getNotificationSettings(): NotificationSettings = notificationSettings
|
||||
override fun encryption(): Encryption = encryption
|
||||
@@ -100,6 +107,8 @@ class FakeFfiClient(
|
||||
|
||||
override suspend fun isUserStatusSupported(): Boolean = isUserStatusSupportedResult()
|
||||
|
||||
override suspend fun isProfilesSlidingSyncExtensionSupported(): Boolean = isProfilesSlidingSyncExtensionSupportedResult()
|
||||
|
||||
override fun subscribeToOwnProfile(listener: ProfileListener): TaskHandle {
|
||||
subscribeToOwnProfileResult(listener)
|
||||
return FakeFfiTaskHandle()
|
||||
@@ -139,5 +148,9 @@ class FakeFfiClient(
|
||||
getUrlResult(url)
|
||||
}
|
||||
|
||||
override suspend fun contentScanner(): ContentScanner = simulateLongTask {
|
||||
contentScannerResult()
|
||||
}
|
||||
|
||||
override fun close() = closeResult()
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.test
|
||||
|
||||
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeClientUrlContentFetcher(
|
||||
override val server: String = A_SERVER_NAME,
|
||||
override val homeserverUrl: String = A_HOMESERVER_URL,
|
||||
private val getUrlResult: (String) -> Result<ByteArray> = { lambdaError() },
|
||||
) : ClientUrlContentFetcher {
|
||||
override suspend fun getUrl(url: String): Result<ByteArray> {
|
||||
return getUrlResult(url)
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -79,6 +79,7 @@ class FakeMatrixClient(
|
||||
override val sessionId: SessionId = A_SESSION_ID,
|
||||
override val sessionPaths: SessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")),
|
||||
override val deviceId: DeviceId = A_DEVICE_ID,
|
||||
override val server: String = A_SERVER_NAME,
|
||||
override val homeserverUrl: String = A_HOMESERVER_URL,
|
||||
override val sessionCoroutineScope: CoroutineScope = TestScope(),
|
||||
private val userDisplayName: String? = A_USER_NAME,
|
||||
@@ -99,7 +100,7 @@ class FakeMatrixClient(
|
||||
override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||
private val homeserverCapabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(),
|
||||
private val accountManagementUrlResult: (AccountManagementAction?) -> Result<String?> = { lambdaError() },
|
||||
private val resolveRoomAliasResult: (RoomAlias) -> Result<Optional<ResolvedRoomAlias>> = {
|
||||
private val resolveRoomAliasResult: suspend (RoomAlias) -> Result<Optional<ResolvedRoomAlias>> = {
|
||||
Result.success(
|
||||
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
|
||||
)
|
||||
@@ -133,6 +134,7 @@ class FakeMatrixClient(
|
||||
private val resetWellKnownConfigLambda: () -> Result<Unit> = { lambdaError() },
|
||||
private val enableAutomaticCallStatusLambda: (Boolean) -> Unit = { },
|
||||
override val contentScanner: ContentScanner? = null,
|
||||
private val isShuttingDownResult: () -> Boolean = { false },
|
||||
) : MatrixClient {
|
||||
var setDisplayNameCalled: Boolean = false
|
||||
private set
|
||||
@@ -147,6 +149,7 @@ class FakeMatrixClient(
|
||||
var setUserStatusResult: Result<Unit> = Result.success(Unit)
|
||||
var clearUserStatusResult: Result<Unit> = Result.success(Unit)
|
||||
var isUserStatusSupportedResult: Result<Boolean> = Result.success(true)
|
||||
var isProfilesSlidingSyncExtensionSupportedResult: Result<Boolean> = Result.success(true)
|
||||
|
||||
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl))
|
||||
override val userProfile: StateFlow<MatrixUser> = _userProfile
|
||||
@@ -293,6 +296,8 @@ class FakeMatrixClient(
|
||||
|
||||
override suspend fun isUserStatusSupported(): Result<Boolean> = isUserStatusSupportedResult
|
||||
|
||||
override suspend fun isProfilesSlidingSyncExtensionSupported(): Result<Boolean> = isProfilesSlidingSyncExtensionSupportedResult
|
||||
|
||||
override fun enableAutomaticCallStatus(enabled: Boolean) = enableAutomaticCallStatusLambda(enabled)
|
||||
|
||||
override suspend fun joinRoom(roomId: RoomId): Result<RoomInfo?> = joinRoomLambda(roomId)
|
||||
@@ -305,6 +310,9 @@ class FakeMatrixClient(
|
||||
return knockRoomLambda(roomIdOrAlias, message, serverNames)
|
||||
}
|
||||
|
||||
override val isShuttingDown: Boolean
|
||||
get() = isShuttingDownResult()
|
||||
|
||||
// Mocks
|
||||
|
||||
fun givenCreateRoomResult(result: Result<RoomId>) {
|
||||
|
||||
+2
-1
@@ -11,10 +11,11 @@ import io.element.android.libraries.matrix.api.TemporaryMatrixClient
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeTemporaryMatrixClient(
|
||||
override val server: String = A_SERVER_NAME,
|
||||
override val homeserverUrl: String = A_HOMESERVER_URL,
|
||||
private val getUrlResult: (String) -> Result<ByteArray> = { lambdaError() },
|
||||
private val closeLambda: () -> Unit = {},
|
||||
) : TemporaryMatrixClient {
|
||||
override suspend fun getUrl(url: String): Result<ByteArray> = getUrlResult(url)
|
||||
|
||||
override fun close() = closeLambda()
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,5 +13,5 @@ import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
|
||||
class FakeTemporaryMatrixClientFactory(
|
||||
private val createResult: (String) -> Result<TemporaryMatrixClient> = { Result.success(FakeTemporaryMatrixClient()) },
|
||||
) : TemporaryMatrixClientFactory {
|
||||
override suspend fun create(homeServerUrl: String): Result<TemporaryMatrixClient> = createResult(homeServerUrl)
|
||||
override suspend fun create(serverName: String): Result<TemporaryMatrixClient> = createResult(serverName)
|
||||
}
|
||||
|
||||
+1
@@ -72,6 +72,7 @@ const val A_SPACE_NAME = "A space name"
|
||||
|
||||
const val A_REDACTION_REASON = "A redaction reason"
|
||||
|
||||
const val A_SERVER_NAME = "matrix.org"
|
||||
const val A_HOMESERVER_URL = "matrix.org"
|
||||
const val A_HOMESERVER_URL_2 = "matrix-client.org"
|
||||
|
||||
|
||||
+5
@@ -70,10 +70,15 @@ class FakeMatrixAuthenticationService(
|
||||
return importCreatedSessionLambda(externalSession)
|
||||
}
|
||||
|
||||
/** The login hint passed to the most recent [getOAuthUrl] call. */
|
||||
var getOAuthUrlLoginHint: String? = null
|
||||
private set
|
||||
|
||||
override suspend fun getOAuthUrl(
|
||||
prompt: OAuthPrompt,
|
||||
loginHint: String?,
|
||||
): Result<OAuthDetails> = simulateLongTask {
|
||||
getOAuthUrlLoginHint = loginHint
|
||||
oAuthError?.let { Result.failure(it) } ?: Result.success(AN_OAUTH_DATA)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user