diff --git a/dependencies.gradle b/dependencies.gradle index 14077987b22..0f18443f224 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -19,7 +19,7 @@ def markwon = "4.6.2" def moshi = "1.15.1" def lifecycle = "2.8.3" def flowBinding = "1.2.0" -def flipper = "0.190.0" +def flipper = "0.259.0" def epoxy = "5.0.0" def mavericks = "3.0.9" def glide = "4.16.0" diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5d1a9f14971..78f4a6d1799 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -17,7 +17,7 @@ buildscript { } } dependencies { - classpath "io.realm:realm-gradle-plugin:10.16.0" + classpath "io.realm:realm-gradle-plugin:10.18.0" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index cf0f4bdce0b..9c94847b22a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -296,6 +296,11 @@ interface Session { */ fun getOkHttpClient(): OkHttpClient + /** + * Same as [getOkHttpClient] but will add the access token to the request. + */ + fun getAuthenticatedOkHttpClient(): OkHttpClient + /** * A global session listener to get notified for some events. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt index 20f977e86e9..3dd3712489e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt @@ -61,6 +61,8 @@ interface ContentUrlResolver { */ fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String? + fun requiresAuthentication(resolvedUrl: String): Boolean + sealed class ResolvedMethod { data class GET(val url: String) : ResolvedMethod() data class POST(val url: String, val jsonBody: String) : ResolvedMethod() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index ecd03288fc4..101947f9d9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -95,6 +95,10 @@ data class HomeServerCapabilities( * If set to true, the SDK will not use the network constraint when configuring Worker for the WorkManager, provided in Wellknown. */ val disableNetworkConstraint: Boolean? = null, + /** + * True if the home server supports authenticated media. + */ + val canUseAuthenticatedMedia: Boolean = false, ) { enum class RoomCapabilitySupport { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 86341729ca3..45e0fd072c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver import org.matrix.android.sdk.internal.session.contentscanner.DisabledContentScannerService +import org.matrix.android.sdk.internal.session.media.IsAuthenticatedMediaSupported internal class DefaultLoginWizard( private val authAPI: AuthAPI, @@ -45,8 +46,14 @@ internal class DefaultLoginWizard( private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") private val getProfileTask: GetProfileTask = DefaultGetProfileTask( - authAPI, - DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig, DisabledContentScannerService()) + authAPI = authAPI, + contentUrlResolver = DefaultContentUrlResolver( + homeServerConnectionConfig = pendingSessionData.homeServerConnectionConfig, + scannerService = DisabledContentScannerService(), + isAuthenticatedMediaSupported = object : IsAuthenticatedMediaSupported { + override fun invoke() = false + } + ) ) override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt index d443d6e3c8c..f683ae773b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt @@ -61,5 +61,6 @@ internal data class HomeServerVersion( val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1) val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0) val v1_4_0 = HomeServerVersion(major = 1, minor = 4, patch = 0) + val v1_11_0 = HomeServerVersion(major = 1, minor = 11, patch = 0) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 83186344bba..d819bef584e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -54,6 +54,7 @@ private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token" private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind" private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440" private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" + @Deprecated("The availability of stable get_login_token is now exposed as a capability and part of login flow") private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" @@ -142,6 +143,15 @@ internal fun Versions.doesServerSupportLogoutDevices(): Boolean { return getMaxVersion() >= HomeServerVersion.r0_6_1 } +/** + * Indicate if the server supports MSC3916 : https://github.com/matrix-org/matrix-spec-proposals/pull/3916 + * + * @return true if authenticated media is supported + */ +internal fun Versions.doesServerSupportAuthenticatedMedia(): Boolean { + return getMaxVersion() >= HomeServerVersion.v1_11_0 +} + private fun Versions.getMaxVersion(): HomeServerVersion { return supportedVersions ?.mapNotNull { HomeServerVersion.parse(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index e02ffc36c53..cee831f37ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -72,6 +72,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo052 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo053 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo054 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo055 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo056 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -80,7 +81,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 55L, + schemaVersion = 56L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -145,5 +146,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 53) MigrateSessionTo053(realm).perform() if (oldVersion < 54) MigrateSessionTo054(realm).perform() if (oldVersion < 55) MigrateSessionTo055(realm).perform() + if (oldVersion < 56) MigrateSessionTo056(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 25af5be66df..8cdabaf1504 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -51,6 +51,7 @@ internal object HomeServerCapabilitiesMapper { externalAccountManagementUrl = entity.externalAccountManagementUrl, authenticationIssuer = entity.authenticationIssuer, disableNetworkConstraint = entity.disableNetworkConstraint, + canUseAuthenticatedMedia = entity.canUseAuthenticatedMedia, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo056.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo056.kt new file mode 100644 index 00000000000..dd345b71836 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo056.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo056(realm: DynamicRealm) : RealmMigrator(realm, 56) { + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_AUTHENTICATED_MEDIA, Boolean::class.java) + ?.forceRefreshOfHomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 3891948418b..0096071d547 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -38,6 +38,7 @@ internal open class HomeServerCapabilitiesEntity( var externalAccountManagementUrl: String? = null, var authenticationIssuer: String? = null, var disableNetworkConstraint: Boolean? = null, + var canUseAuthenticatedMedia: Boolean = false, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt index a34606a6bbd..fe27cea5ca0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/AccessTokenInterceptor.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.network import okhttp3.Interceptor import okhttp3.Response +import org.matrix.android.sdk.internal.network.httpclient.addAuthenticationHeader import org.matrix.android.sdk.internal.network.token.AccessTokenProvider internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor { @@ -28,7 +29,7 @@ internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTok // Add the access token to all requests if it is set accessTokenProvider.getToken()?.let { token -> val newRequestBuilder = request.newBuilder() - newRequestBuilder.header(HttpHeaders.Authorization, "Bearer $token") + newRequestBuilder.addAuthenticationHeader(token) request = newRequestBuilder.build() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt index 1c395c2d615..aef2e63b2cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt @@ -17,9 +17,11 @@ package org.matrix.android.sdk.internal.network.httpclient import okhttp3.OkHttpClient +import okhttp3.Request import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.internal.network.AccessTokenInterceptor +import org.matrix.android.sdk.internal.network.HttpHeaders import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor import org.matrix.android.sdk.internal.network.ssl.CertUtil import org.matrix.android.sdk.internal.network.token.AccessTokenProvider @@ -66,3 +68,10 @@ internal fun OkHttpClient.Builder.applyMatrixConfiguration(matrixConfiguration: return this } + +fun Request.Builder.addAuthenticationHeader(accessToken: String?): Request.Builder { + if (accessToken != null) { + header(HttpHeaders.Authorization, "Bearer $accessToken") + } + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index bbf260a3f13..217ef438211 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -34,8 +34,11 @@ import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.util.md5 import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress +import org.matrix.android.sdk.internal.network.httpclient.addAuthenticationHeader +import org.matrix.android.sdk.internal.network.token.AccessTokenProvider import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER import org.matrix.android.sdk.internal.util.file.AtomicFileCreator import org.matrix.android.sdk.internal.util.time.Clock @@ -54,6 +57,7 @@ internal class DefaultFileService @Inject constructor( private val okHttpClient: OkHttpClient, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val clock: Clock, + @Authenticated private val accessTokenProvider: AccessTokenProvider, ) : FileService { // Legacy folder, will be deleted @@ -124,21 +128,26 @@ internal class DefaultFileService @Inject constructor( val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null) if (!cachedFiles.file.exists()) { - val resolvedUrl = contentUrlResolver.resolveForDownload(url, elementToDecrypt) ?: throw IllegalArgumentException("url is null") + val resolvedMethod = contentUrlResolver.resolveForDownload(url, elementToDecrypt) ?: throw IllegalArgumentException("url is null") - val request = when (resolvedUrl) { + val request = when (resolvedMethod) { is ContentUrlResolver.ResolvedMethod.GET -> { - Request.Builder() - .url(resolvedUrl.url) + val requestBuilder = Request.Builder() + .url(resolvedMethod.url) .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) - .build() + + if (contentUrlResolver.requiresAuthentication(resolvedMethod.url)) { + val accessToken = accessTokenProvider.getToken() + requestBuilder.addAuthenticationHeader(accessToken) + } + requestBuilder.build() } is ContentUrlResolver.ResolvedMethod.POST -> { Request.Builder() - .url(resolvedUrl.url) + .url(resolvedMethod.url) .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) - .post(resolvedUrl.jsonBody.toRequestBody("application/json".toMediaType())) + .post(resolvedMethod.jsonBody.toRequestBody("application/json".toMediaType())) .build() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 992ea650cf7..b2d5be2946f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -67,6 +67,7 @@ import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.internal.auth.SSO_UIA_FALLBACK_PATH import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.database.tools.RealmDebugTools +import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.ContentScannerDatabase import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.IdentityDatabase @@ -131,6 +132,8 @@ internal class DefaultSession @Inject constructor( private val eventStreamService: Lazy, @UnauthenticatedWithCertificate private val unauthenticatedWithCertificateOkHttpClient: Lazy, + @Authenticated + private val authenticatedOkHttpClient: Lazy, private val sessionState: SessionState, ) : Session, GlobalErrorHandler.Listener { @@ -234,6 +237,10 @@ internal class DefaultSession @Inject constructor( return unauthenticatedWithCertificateOkHttpClient.get() } + override fun getAuthenticatedOkHttpClient(): OkHttpClient { + return authenticatedOkHttpClient.get() + } + override fun addListener(listener: Session.Listener) { sessionListeners.addListener(listener) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 54834f42635..d6e1d36b54e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -83,6 +83,7 @@ import org.matrix.android.sdk.internal.session.events.DefaultEventService import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.media.DefaultIsAuthenticatedMediaSupported import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor @@ -365,6 +366,10 @@ internal abstract class SessionModule { @IntoSet abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver + @Binds + @IntoSet + abstract fun bindIsMediaAuthenticated(observer: DefaultIsAuthenticatedMediaSupported): SessionLifecycleObserver + @Binds @IntoSet abstract fun bindIntegrationManager(manager: IntegrationManager): SessionLifecycleObserver diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt index ad2b9d0905e..dd277f69562 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt @@ -25,16 +25,18 @@ import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils import org.matrix.android.sdk.internal.session.contentscanner.model.toJson +import org.matrix.android.sdk.internal.session.media.IsAuthenticatedMediaSupported import org.matrix.android.sdk.internal.util.ensureTrailingSlash import javax.inject.Inject internal class DefaultContentUrlResolver @Inject constructor( homeServerConnectionConfig: HomeServerConnectionConfig, - private val scannerService: ContentScannerService + private val scannerService: ContentScannerService, + private val isAuthenticatedMediaSupported: IsAuthenticatedMediaSupported, ) : ContentUrlResolver { private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash() - + private val authenticatedMediaApiPath = baseUrl + NetworkConstants.URI_API_PREFIX_PATH_V1 + "media/" override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload" override fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt?): ContentUrlResolver.ResolvedMethod? { @@ -80,15 +82,20 @@ internal class DefaultContentUrlResolver @Inject constructor( } } + override fun requiresAuthentication(resolvedUrl: String): Boolean { + return resolvedUrl.startsWith(authenticatedMediaApiPath) + } + private fun resolve( contentUrl: String, toThumbnail: Boolean, params: String = "" ): String { var serverAndMediaId = contentUrl.removeMxcPrefix() - val apiPath = if (scannerService.isScannerEnabled()) { NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + } else if (isAuthenticatedMediaSupported()) { + NetworkConstants.URI_API_PREFIX_PATH_V1 + "media/" } else { NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index f007f22366d..5314237c3c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -20,9 +20,11 @@ import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions +import org.matrix.android.sdk.internal.auth.version.doesServerSupportAuthenticatedMedia import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin import org.matrix.android.sdk.internal.auth.version.doesServerSupportRedactionOfRelatedEvents @@ -38,8 +40,9 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor +import org.matrix.android.sdk.internal.session.media.AuthenticatedMediaAPI import org.matrix.android.sdk.internal.session.media.GetMediaConfigResult -import org.matrix.android.sdk.internal.session.media.MediaAPI +import org.matrix.android.sdk.internal.session.media.UnauthenticatedMediaAPI import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.wellknown.GetWellknownTask @@ -55,7 +58,8 @@ internal interface GetHomeServerCapabilitiesTask : Task val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) - doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time } } @@ -85,18 +88,22 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } }.getOrNull() - val mediaConfig = runCatching { - executeRequest(globalErrorReceiver) { - mediaAPI.getMediaConfig() - } - }.getOrNull() - val versions = runCatching { executeRequest(null) { capabilitiesAPI.getVersions() } }.getOrNull() + val mediaConfig = runCatching { + executeRequest(globalErrorReceiver) { + if (versions?.doesServerSupportAuthenticatedMedia().orFalse()) { + authenticatedMediaAPI.getMediaConfig() + } else { + unauthenticatedMediaAPI.getMediaConfig() + } + } + }.getOrNull() + // Domain may include a port (eg, matrix.org:8080) // Per https://spec.matrix.org/latest/client-server-api/#well-known-uri we should extract the hostname from the server name // So we take everything before the last : as the domain for the well-known task. @@ -155,6 +162,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( getVersionResult.doesServerSupportRemoteToggleOfPushNotifications() homeServerCapabilitiesEntity.canRedactEventWithRelations = getVersionResult.doesServerSupportRedactionOfRelatedEvents() + homeServerCapabilitiesEntity.canUseAuthenticatedMedia = + getVersionResult.doesServerSupportAuthenticatedMedia() } if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/AuthenticatedMediaAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/AuthenticatedMediaAPI.kt new file mode 100644 index 00000000000..417d97abbae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/AuthenticatedMediaAPI.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.http.GET +import retrofit2.http.Query + +/** + * Implementation of the media repository API using the new Authenticated media API. + */ +internal interface AuthenticatedMediaAPI : MediaAPI { + /** + * Retrieve the configuration of the content repository + * Ref: https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediaconfig + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "media/config") + override suspend fun getMediaConfig(): GetMediaConfigResult + + /** + * Get information about a URL for the client. Typically this is called when a client + * sees a URL in a message and wants to render a preview for the user. + * Ref: https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediapreview_url + * @param url Required. The URL to get a preview of. + * @param ts The preferred point in time to return a preview for. The server may return a newer version + * if it does not have the requested version available. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "media/preview_url") + override suspend fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): JsonDict +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultIsAuthenticatedMediaSupported.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultIsAuthenticatedMediaSupported.kt new file mode 100644 index 00000000000..90b9d1ff88a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultIsAuthenticatedMediaSupported.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.RealmResults +import org.matrix.android.sdk.internal.database.RealmLiveEntityObserver +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultIsAuthenticatedMediaSupported @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, +) : + IsAuthenticatedMediaSupported, + RealmLiveEntityObserver(monarchy.realmConfiguration) { + + override fun invoke(): Boolean { + return canUseAuthenticatedMedia + } + + override val query = Monarchy.Query { + it.where(HomeServerCapabilitiesEntity::class.java) + } + + override fun onChange(results: RealmResults) { + canUseAuthenticatedMedia = results.canUseAuthenticatedMedia() + Timber.d("canUseAuthenticatedMedia: $canUseAuthenticatedMedia") + } + + private var canUseAuthenticatedMedia = getInitialValue() + + private fun getInitialValue(): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + query.createQuery(realm).findAll().canUseAuthenticatedMedia() + } + } + + private fun RealmResults.canUseAuthenticatedMedia(): Boolean { + return firstOrNull()?.canUseAuthenticatedMedia ?: false + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt index a31f0d60655..fafd6f2753a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt @@ -41,7 +41,7 @@ internal interface GetPreviewUrlTask : Task { + + private val defaultClient = OkHttpClient() + + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return AuthenticatedGlideUrlLoader(context, defaultClient) + } + + override fun teardown() = Unit +} + +class AuthenticatedGlideUrlLoader( + context: Context, + private val defaultClient: OkHttpClient +) : + ModelLoader { + + private val activeSessionHolder = context.singletonEntryPoint().activeSessionHolder() + private val client: OkHttpClient + get() = activeSessionHolder.getSafeActiveSession() + ?.getAuthenticatedOkHttpClient() + ?: defaultClient + + private val callFactory = Call.Factory { request -> client.newCall(request) } + + override fun handles(model: GlideUrl): Boolean { + if (!activeSessionHolder.hasActiveSession()) return false + val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() + val stringUrl = model.toStringUrl() + return contentUrlResolver.requiresAuthentication(stringUrl) + } + + override fun buildLoadData(model: GlideUrl, width: Int, height: Int, options: Options): ModelLoader.LoadData { + val fetcher = OkHttpStreamFetcher(callFactory, model) + return ModelLoader.LoadData(model, fetcher) + } +} diff --git a/vector/src/main/java/im/vector/app/core/glide/FactoryUrl.kt b/vector/src/main/java/im/vector/app/core/glide/FactoryUrl.kt deleted file mode 100644 index d52d5814ad3..00000000000 --- a/vector/src/main/java/im/vector/app/core/glide/FactoryUrl.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2020 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 im.vector.app.core.glide - -import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader -import com.bumptech.glide.load.model.GlideUrl -import com.bumptech.glide.load.model.ModelLoader -import com.bumptech.glide.load.model.ModelLoaderFactory -import com.bumptech.glide.load.model.MultiModelLoaderFactory -import im.vector.app.ActiveSessionDataSource -import okhttp3.OkHttpClient -import java.io.InputStream - -class FactoryUrl(private val activeSessionDataSource: ActiveSessionDataSource) : ModelLoaderFactory { - - override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { - val client = activeSessionDataSource.currentValue?.orNull()?.getOkHttpClient() ?: OkHttpClient() - return OkHttpUrlLoader(client) - } - - override fun teardown() { - // Do nothing, this instance doesn't own the client. - } -} diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/ImageContentRendererDataLoader.kt similarity index 91% rename from vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt rename to vector/src/main/java/im/vector/app/core/glide/ImageContentRendererDataLoader.kt index 92c95d30624..521eb083856 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/ImageContentRendererDataLoader.kt @@ -32,15 +32,14 @@ import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient import timber.log.Timber import java.io.IOException import java.io.InputStream -class VectorGlideModelLoaderFactory(private val context: Context) : ModelLoaderFactory { +class ImageContentRendererDataLoaderFactory(private val context: Context) : ModelLoaderFactory { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { - return VectorGlideModelLoader(context) + return ImageContentRendererDataLoader(context) } override fun teardown() { @@ -48,7 +47,7 @@ class VectorGlideModelLoaderFactory(private val context: Context) : ModelLoaderF } } -class VectorGlideModelLoader(private val context: Context) : +class ImageContentRendererDataLoader(private val context: Context) : ModelLoader { override fun handles(model: ImageContentRenderer.Data): Boolean { // Always handle @@ -56,11 +55,11 @@ class VectorGlideModelLoader(private val context: Context) : } override fun buildLoadData(model: ImageContentRenderer.Data, width: Int, height: Int, options: Options): ModelLoader.LoadData? { - return ModelLoader.LoadData(ObjectKey(model), VectorGlideDataFetcher(context, model, width, height)) + return ModelLoader.LoadData(ObjectKey(model), ImageContentRendererDataFetcher(context, model, width, height)) } } -class VectorGlideDataFetcher( +class ImageContentRendererDataFetcher( context: Context, private val data: ImageContentRenderer.Data, private val width: Int, @@ -71,8 +70,6 @@ class VectorGlideDataFetcher( private val localFilesHelper = LocalFilesHelper(context) private val activeSessionHolder = context.singletonEntryPoint().activeSessionHolder() - private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient() - override fun getDataClass(): Class { return InputStream::class.java } diff --git a/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt b/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt index 59bffd95fdf..67a79ca8b32 100644 --- a/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt +++ b/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt @@ -38,7 +38,7 @@ class MyAppGlideModule : AppGlideModule() { registry.append( ImageContentRenderer.Data::class.java, InputStream::class.java, - VectorGlideModelLoaderFactory(context) + ImageContentRendererDataLoaderFactory(context) ) registry.append( AvatarPlaceholder::class.java,