Merge pull request #8868 from element-hq/feature/fga/authenticated_media

Feature/fga/authenticated media
This commit is contained in:
ganfra 2024-07-19 16:30:40 +02:00 committed by GitHub
commit 95e1bcbf64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 438 additions and 108 deletions

View file

@ -19,7 +19,7 @@ def markwon = "4.6.2"
def moshi = "1.15.1" def moshi = "1.15.1"
def lifecycle = "2.8.3" def lifecycle = "2.8.3"
def flowBinding = "1.2.0" def flowBinding = "1.2.0"
def flipper = "0.190.0" def flipper = "0.259.0"
def epoxy = "5.0.0" def epoxy = "5.0.0"
def mavericks = "3.0.9" def mavericks = "3.0.9"
def glide = "4.16.0" def glide = "4.16.0"

View file

@ -17,7 +17,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath "io.realm:realm-gradle-plugin:10.16.0" classpath "io.realm:realm-gradle-plugin:10.18.0"
} }
} }

View file

@ -296,6 +296,11 @@ interface Session {
*/ */
fun getOkHttpClient(): OkHttpClient 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. * A global session listener to get notified for some events.
*/ */

View file

@ -61,6 +61,8 @@ interface ContentUrlResolver {
*/ */
fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String? fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String?
fun requiresAuthentication(resolvedUrl: String): Boolean
sealed class ResolvedMethod { sealed class ResolvedMethod {
data class GET(val url: String) : ResolvedMethod() data class GET(val url: String) : ResolvedMethod()
data class POST(val url: String, val jsonBody: String) : ResolvedMethod() data class POST(val url: String, val jsonBody: String) : ResolvedMethod()

View file

@ -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. * 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, val disableNetworkConstraint: Boolean? = null,
/**
* True if the home server supports authenticated media.
*/
val canUseAuthenticatedMedia: Boolean = false,
) { ) {
enum class RoomCapabilitySupport { enum class RoomCapabilitySupport {

View file

@ -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.network.executeRequest
import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver 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.contentscanner.DisabledContentScannerService
import org.matrix.android.sdk.internal.session.media.IsAuthenticatedMediaSupported
internal class DefaultLoginWizard( internal class DefaultLoginWizard(
private val authAPI: AuthAPI, 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 var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
private val getProfileTask: GetProfileTask = DefaultGetProfileTask( private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
authAPI, authAPI = authAPI,
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig, DisabledContentScannerService()) contentUrlResolver = DefaultContentUrlResolver(
homeServerConnectionConfig = pendingSessionData.homeServerConnectionConfig,
scannerService = DisabledContentScannerService(),
isAuthenticatedMediaSupported = object : IsAuthenticatedMediaSupported {
override fun invoke() = false
}
)
) )
override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo { override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {

View file

@ -61,5 +61,6 @@ internal data class HomeServerVersion(
val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1) val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1)
val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0) val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
val v1_4_0 = HomeServerVersion(major = 1, minor = 4, patch = 0) val v1_4_0 = HomeServerVersion(major = 1, minor = 4, patch = 0)
val v1_11_0 = HomeServerVersion(major = 1, minor = 11, patch = 0)
} }
} }

View file

@ -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_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440" private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" 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") @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_QR_CODE_LOGIN = "org.matrix.msc3882"
private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771"
@ -142,6 +143,15 @@ internal fun Versions.doesServerSupportLogoutDevices(): Boolean {
return getMaxVersion() >= HomeServerVersion.r0_6_1 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 { private fun Versions.getMaxVersion(): HomeServerVersion {
return supportedVersions return supportedVersions
?.mapNotNull { HomeServerVersion.parse(it) } ?.mapNotNull { HomeServerVersion.parse(it) }

View file

@ -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.MigrateSessionTo053
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo054 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.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.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -80,7 +81,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 55L, schemaVersion = 56L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -145,5 +146,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 53) MigrateSessionTo053(realm).perform() if (oldVersion < 53) MigrateSessionTo053(realm).perform()
if (oldVersion < 54) MigrateSessionTo054(realm).perform() if (oldVersion < 54) MigrateSessionTo054(realm).perform()
if (oldVersion < 55) MigrateSessionTo055(realm).perform() if (oldVersion < 55) MigrateSessionTo055(realm).perform()
if (oldVersion < 56) MigrateSessionTo056(realm).perform()
} }
} }

View file

@ -51,6 +51,7 @@ internal object HomeServerCapabilitiesMapper {
externalAccountManagementUrl = entity.externalAccountManagementUrl, externalAccountManagementUrl = entity.externalAccountManagementUrl,
authenticationIssuer = entity.authenticationIssuer, authenticationIssuer = entity.authenticationIssuer,
disableNetworkConstraint = entity.disableNetworkConstraint, disableNetworkConstraint = entity.disableNetworkConstraint,
canUseAuthenticatedMedia = entity.canUseAuthenticatedMedia,
) )
} }

View file

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

View file

@ -38,6 +38,7 @@ internal open class HomeServerCapabilitiesEntity(
var externalAccountManagementUrl: String? = null, var externalAccountManagementUrl: String? = null,
var authenticationIssuer: String? = null, var authenticationIssuer: String? = null,
var disableNetworkConstraint: Boolean? = null, var disableNetworkConstraint: Boolean? = null,
var canUseAuthenticatedMedia: Boolean = false,
) : RealmObject() { ) : RealmObject() {
companion object companion object

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.network
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import org.matrix.android.sdk.internal.network.httpclient.addAuthenticationHeader
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor { 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 // Add the access token to all requests if it is set
accessTokenProvider.getToken()?.let { token -> accessTokenProvider.getToken()?.let { token ->
val newRequestBuilder = request.newBuilder() val newRequestBuilder = request.newBuilder()
newRequestBuilder.header(HttpHeaders.Authorization, "Bearer $token") newRequestBuilder.addAuthenticationHeader(token)
request = newRequestBuilder.build() request = newRequestBuilder.build()
} }

View file

@ -17,9 +17,11 @@
package org.matrix.android.sdk.internal.network.httpclient package org.matrix.android.sdk.internal.network.httpclient
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.internal.network.AccessTokenInterceptor 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.interceptors.CurlLoggingInterceptor
import org.matrix.android.sdk.internal.network.ssl.CertUtil import org.matrix.android.sdk.internal.network.ssl.CertUtil
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
@ -66,3 +68,10 @@ internal fun OkHttpClient.Builder.applyMatrixConfiguration(matrixConfiguration:
return this return this
} }
fun Request.Builder.addAuthenticationHeader(accessToken: String?): Request.Builder {
if (accessToken != null) {
header(HttpHeaders.Authorization, "Bearer $accessToken")
}
return this
}

View file

@ -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.session.file.FileService
import org.matrix.android.sdk.api.util.md5 import org.matrix.android.sdk.api.util.md5
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments 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.SessionDownloadsDirectory
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress 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.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
import org.matrix.android.sdk.internal.util.file.AtomicFileCreator import org.matrix.android.sdk.internal.util.file.AtomicFileCreator
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
@ -54,6 +57,7 @@ internal class DefaultFileService @Inject constructor(
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val clock: Clock, private val clock: Clock,
@Authenticated private val accessTokenProvider: AccessTokenProvider,
) : FileService { ) : FileService {
// Legacy folder, will be deleted // Legacy folder, will be deleted
@ -124,21 +128,26 @@ internal class DefaultFileService @Inject constructor(
val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null) val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null)
if (!cachedFiles.file.exists()) { 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 -> { is ContentUrlResolver.ResolvedMethod.GET -> {
Request.Builder() val requestBuilder = Request.Builder()
.url(resolvedUrl.url) .url(resolvedMethod.url)
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, 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 -> { is ContentUrlResolver.ResolvedMethod.POST -> {
Request.Builder() Request.Builder()
.url(resolvedUrl.url) .url(resolvedMethod.url)
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
.post(resolvedUrl.jsonBody.toRequestBody("application/json".toMediaType())) .post(resolvedMethod.jsonBody.toRequestBody("application/json".toMediaType()))
.build() .build()
} }
} }

View file

@ -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.SSO_UIA_FALLBACK_PATH
import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools 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.ContentScannerDatabase
import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.IdentityDatabase import org.matrix.android.sdk.internal.di.IdentityDatabase
@ -131,6 +132,8 @@ internal class DefaultSession @Inject constructor(
private val eventStreamService: Lazy<EventStreamService>, private val eventStreamService: Lazy<EventStreamService>,
@UnauthenticatedWithCertificate @UnauthenticatedWithCertificate
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>, private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>,
@Authenticated
private val authenticatedOkHttpClient: Lazy<OkHttpClient>,
private val sessionState: SessionState, private val sessionState: SessionState,
) : Session, ) : Session,
GlobalErrorHandler.Listener { GlobalErrorHandler.Listener {
@ -234,6 +237,10 @@ internal class DefaultSession @Inject constructor(
return unauthenticatedWithCertificateOkHttpClient.get() return unauthenticatedWithCertificateOkHttpClient.get()
} }
override fun getAuthenticatedOkHttpClient(): OkHttpClient {
return authenticatedOkHttpClient.get()
}
override fun addListener(listener: Session.Listener) { override fun addListener(listener: Session.Listener) {
sessionListeners.addListener(listener) sessionListeners.addListener(listener)
} }

View file

@ -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.homeserver.DefaultHomeServerCapabilitiesService
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService 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.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.openid.DefaultOpenIdService
import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
@ -365,6 +366,10 @@ internal abstract class SessionModule {
@IntoSet @IntoSet
abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver
@Binds
@IntoSet
abstract fun bindIsMediaAuthenticated(observer: DefaultIsAuthenticatedMediaSupported): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindIntegrationManager(manager: IntegrationManager): SessionLifecycleObserver abstract fun bindIntegrationManager(manager: IntegrationManager): SessionLifecycleObserver

View file

@ -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.network.NetworkConstants
import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils 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.contentscanner.model.toJson
import org.matrix.android.sdk.internal.session.media.IsAuthenticatedMediaSupported
import org.matrix.android.sdk.internal.util.ensureTrailingSlash import org.matrix.android.sdk.internal.util.ensureTrailingSlash
import javax.inject.Inject import javax.inject.Inject
internal class DefaultContentUrlResolver @Inject constructor( internal class DefaultContentUrlResolver @Inject constructor(
homeServerConnectionConfig: HomeServerConnectionConfig, homeServerConnectionConfig: HomeServerConnectionConfig,
private val scannerService: ContentScannerService private val scannerService: ContentScannerService,
private val isAuthenticatedMediaSupported: IsAuthenticatedMediaSupported,
) : ContentUrlResolver { ) : ContentUrlResolver {
private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash() 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 val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"
override fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt?): ContentUrlResolver.ResolvedMethod? { 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( private fun resolve(
contentUrl: String, contentUrl: String,
toThumbnail: Boolean, toThumbnail: Boolean,
params: String = "" params: String = ""
): String { ): String {
var serverAndMediaId = contentUrl.removeMxcPrefix() var serverAndMediaId = contentUrl.removeMxcPrefix()
val apiPath = if (scannerService.isScannerEnabled()) { val apiPath = if (scannerService.isScannerEnabled()) {
NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE
} else if (isAuthenticatedMediaSupported()) {
NetworkConstants.URI_API_PREFIX_PATH_V1 + "media/"
} else { } else {
NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0
} }

View file

@ -20,9 +20,11 @@ import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult 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.extensions.orTrue
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities 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.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.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin
import org.matrix.android.sdk.internal.auth.version.doesServerSupportRedactionOfRelatedEvents 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.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor 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.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.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
@ -55,7 +58,8 @@ internal interface GetHomeServerCapabilitiesTask : Task<GetHomeServerCapabilitie
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val capabilitiesAPI: CapabilitiesAPI, private val capabilitiesAPI: CapabilitiesAPI,
private val mediaAPI: MediaAPI, private val unauthenticatedMediaAPI: UnauthenticatedMediaAPI,
private val authenticatedMediaAPI: AuthenticatedMediaAPI,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val globalErrorReceiver: GlobalErrorReceiver, private val globalErrorReceiver: GlobalErrorReceiver,
private val getWellknownTask: GetWellknownTask, private val getWellknownTask: GetWellknownTask,
@ -70,7 +74,6 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (!doRequest) { if (!doRequest) {
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
} }
} }
@ -85,18 +88,22 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
} }
}.getOrNull() }.getOrNull()
val mediaConfig = runCatching {
executeRequest(globalErrorReceiver) {
mediaAPI.getMediaConfig()
}
}.getOrNull()
val versions = runCatching { val versions = runCatching {
executeRequest(null) { executeRequest(null) {
capabilitiesAPI.getVersions() capabilitiesAPI.getVersions()
} }
}.getOrNull() }.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) // 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 // 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. // 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() getVersionResult.doesServerSupportRemoteToggleOfPushNotifications()
homeServerCapabilitiesEntity.canRedactEventWithRelations = homeServerCapabilitiesEntity.canRedactEventWithRelations =
getVersionResult.doesServerSupportRedactionOfRelatedEvents() getVersionResult.doesServerSupportRedactionOfRelatedEvents()
homeServerCapabilitiesEntity.canUseAuthenticatedMedia =
getVersionResult.doesServerSupportAuthenticatedMedia()
} }
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {

View file

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

View file

@ -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<HomeServerCapabilitiesEntity>(monarchy.realmConfiguration) {
override fun invoke(): Boolean {
return canUseAuthenticatedMedia
}
override val query = Monarchy.Query {
it.where(HomeServerCapabilitiesEntity::class.java)
}
override fun onChange(results: RealmResults<HomeServerCapabilitiesEntity>) {
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<HomeServerCapabilitiesEntity>.canUseAuthenticatedMedia(): Boolean {
return firstOrNull()?.canUseAuthenticatedMedia ?: false
}
}

View file

@ -41,7 +41,7 @@ internal interface GetPreviewUrlTask : Task<GetPreviewUrlTask.Params, PreviewUrl
} }
internal class DefaultGetPreviewUrlTask @Inject constructor( internal class DefaultGetPreviewUrlTask @Inject constructor(
private val mediaAPI: MediaAPI, private val mediaAPIProvider: MediaAPIProvider,
private val globalErrorReceiver: GlobalErrorReceiver, private val globalErrorReceiver: GlobalErrorReceiver,
@SessionDatabase private val monarchy: Monarchy @SessionDatabase private val monarchy: Monarchy
) : GetPreviewUrlTask { ) : GetPreviewUrlTask {
@ -66,7 +66,7 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData { private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData {
return executeRequest(globalErrorReceiver) { return executeRequest(globalErrorReceiver) {
mediaAPI.getPreviewUrlData(url, timestamp) mediaAPIProvider.getMediaAPI().getPreviewUrlData(url, timestamp)
} }
.toPreviewUrlData(url) .toPreviewUrlData(url)
} }

View file

@ -30,13 +30,13 @@ internal interface GetRawPreviewUrlTask : Task<GetRawPreviewUrlTask.Params, Json
} }
internal class DefaultGetRawPreviewUrlTask @Inject constructor( internal class DefaultGetRawPreviewUrlTask @Inject constructor(
private val mediaAPI: MediaAPI, private val mediaAPIProvider: MediaAPIProvider,
private val globalErrorReceiver: GlobalErrorReceiver private val globalErrorReceiver: GlobalErrorReceiver
) : GetRawPreviewUrlTask { ) : GetRawPreviewUrlTask {
override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict { override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict {
return executeRequest(globalErrorReceiver) { return executeRequest(globalErrorReceiver) {
mediaAPI.getPreviewUrlData(params.url, params.timestamp) mediaAPIProvider.getMediaAPI().getPreviewUrlData(params.url, params.timestamp)
} }
} }
} }

View file

@ -0,0 +1,21 @@
/*
* 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
interface IsAuthenticatedMediaSupported {
operator fun invoke(): Boolean
}

View file

@ -1,11 +1,11 @@
/* /*
* Copyright 2020 The Matrix.org Foundation C.I.C. * Copyright (C) 2024 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -17,26 +17,11 @@
package org.matrix.android.sdk.internal.session.media package org.matrix.android.sdk.internal.session.media
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.network.NetworkConstants
import retrofit2.http.GET
import retrofit2.http.Query
/**
* This defines some method to interact with the media repository.
*/
internal interface MediaAPI { internal interface MediaAPI {
/**
* Retrieve the configuration of the content repository
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-config
*/
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
suspend fun getMediaConfig(): GetMediaConfigResult suspend fun getMediaConfig(): GetMediaConfigResult
suspend fun getPreviewUrlData(url: String, ts: Long?): JsonDict
/**
* 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://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-preview-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_MEDIA_PREFIX_PATH_R0 + "preview_url")
suspend fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): JsonDict
} }

View file

@ -0,0 +1,34 @@
/*
* 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 javax.inject.Inject
internal class MediaAPIProvider @Inject constructor(
private val isAuthenticatedMediaSupported: IsAuthenticatedMediaSupported,
private val authenticatedMediaAPI: AuthenticatedMediaAPI,
private val unauthenticatedMediaAPI: UnauthenticatedMediaAPI,
) {
fun getMediaAPI(): MediaAPI {
return if (isAuthenticatedMediaSupported()) {
authenticatedMediaAPI
} else {
unauthenticatedMediaAPI
}
}
}

View file

@ -31,11 +31,21 @@ internal abstract class MediaModule {
@Provides @Provides
@JvmStatic @JvmStatic
@SessionScope @SessionScope
fun providesMediaAPI(retrofit: Retrofit): MediaAPI { fun providesUnauthenticatedMediaAPI(retrofit: Retrofit): UnauthenticatedMediaAPI {
return retrofit.create(MediaAPI::class.java) return retrofit.create(UnauthenticatedMediaAPI::class.java)
}
@Provides
@JvmStatic
@SessionScope
fun providesAuthenticatedMediaAPI(retrofit: Retrofit): AuthenticatedMediaAPI {
return retrofit.create(AuthenticatedMediaAPI::class.java)
} }
} }
@Binds
abstract fun bindIsAuthenticatedMediaSupported(isAuthenticatedMediaSupported: DefaultIsAuthenticatedMediaSupported): IsAuthenticatedMediaSupported
@Binds @Binds
abstract fun bindMediaService(service: DefaultMediaService): MediaService abstract fun bindMediaService(service: DefaultMediaService): MediaService

View file

@ -0,0 +1,42 @@
/*
* 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
internal interface UnauthenticatedMediaAPI : MediaAPI {
/**
* Retrieve the configuration of the content repository
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-config
*/
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "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://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-preview-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_MEDIA_PREFIX_PATH_R0 + "preview_url")
override suspend fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): JsonDict
}

View file

@ -399,7 +399,7 @@ dependencies {
exclude group: 'com.facebook.fbjni', module: 'fbjni' exclude group: 'com.facebook.fbjni', module: 'fbjni'
} }
debugImplementation 'com.facebook.soloader:soloader:0.10.5' debugImplementation 'com.facebook.soloader:soloader:0.10.5'
debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.3.0"
gplayImplementation "com.google.android.gms:play-services-location:21.3.0" gplayImplementation "com.google.android.gms:play-services-location:21.3.0"
// UnifiedPush gplay flavor only // UnifiedPush gplay flavor only

View file

@ -16,7 +16,6 @@
package im.vector.app.core.di package im.vector.app.core.di
import android.content.Context
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
@ -50,7 +49,6 @@ class ActiveSessionHolder @Inject constructor(
private val imageManager: ImageManager, private val imageManager: ImageManager,
private val guardServiceStarter: GuardServiceStarter, private val guardServiceStarter: GuardServiceStarter,
private val sessionInitializer: SessionInitializer, private val sessionInitializer: SessionInitializer,
private val applicationContext: Context,
private val authenticationService: AuthenticationService, private val authenticationService: AuthenticationService,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase, private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,

View file

@ -21,8 +21,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.glide.GlideImageLoader import com.github.piasy.biv.loader.glide.GlideImageLoader
import im.vector.app.ActiveSessionDataSource import im.vector.app.core.glide.AuthenticatedGlideUrlLoaderFactory
import im.vector.app.core.glide.FactoryUrl
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
@ -32,16 +31,16 @@ import javax.inject.Inject
*/ */
class ImageManager @Inject constructor( class ImageManager @Inject constructor(
private val context: Context, private val context: Context,
private val activeSessionDataSource: ActiveSessionDataSource
) { ) {
fun onSessionStarted(session: Session) { fun onSessionStarted(session: Session) {
// Do this call first // Do this call first
BigImageViewer.initialize(GlideImageLoader.with(context, session.getOkHttpClient())) val glideImageLoader = GlideImageLoader.with(context, session.getOkHttpClient())
BigImageViewer.initialize(glideImageLoader)
val glide = Glide.get(context) val glide = Glide.get(context)
// And this one. FIXME But are losing what BigImageViewer has done to add a Progress listener // And this one. It'll be tried first, otherwise it'll use the one initialised by GlideImageLoader.
glide.registry.replace(GlideUrl::class.java, InputStream::class.java, FactoryUrl(activeSessionDataSource)) glide.registry.prepend(GlideUrl::class.java, InputStream::class.java, AuthenticatedGlideUrlLoaderFactory(context))
} }
} }

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2024 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 android.content.Context
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.Options
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.core.extensions.singletonEntryPoint
import okhttp3.Call
import okhttp3.OkHttpClient
import java.io.InputStream
class AuthenticatedGlideUrlLoaderFactory(private val context: Context) : ModelLoaderFactory<GlideUrl, InputStream> {
private val defaultClient = OkHttpClient()
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrl, InputStream> {
return AuthenticatedGlideUrlLoader(context, defaultClient)
}
override fun teardown() = Unit
}
class AuthenticatedGlideUrlLoader(
context: Context,
private val defaultClient: OkHttpClient
) :
ModelLoader<GlideUrl, InputStream> {
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<InputStream> {
val fetcher = OkHttpStreamFetcher(callFactory, model)
return ModelLoader.LoadData(model, fetcher)
}
}

View file

@ -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<GlideUrl, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrl, InputStream> {
val client = activeSessionDataSource.currentValue?.orNull()?.getOkHttpClient() ?: OkHttpClient()
return OkHttpUrlLoader(client)
}
override fun teardown() {
// Do nothing, this instance doesn't own the client.
}
}

View file

@ -32,15 +32,14 @@ import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
class VectorGlideModelLoaderFactory(private val context: Context) : ModelLoaderFactory<ImageContentRenderer.Data, InputStream> { class ImageContentRendererDataLoaderFactory(private val context: Context) : ModelLoaderFactory<ImageContentRenderer.Data, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ImageContentRenderer.Data, InputStream> { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ImageContentRenderer.Data, InputStream> {
return VectorGlideModelLoader(context) return ImageContentRendererDataLoader(context)
} }
override fun teardown() { 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<ImageContentRenderer.Data, InputStream> { ModelLoader<ImageContentRenderer.Data, InputStream> {
override fun handles(model: ImageContentRenderer.Data): Boolean { override fun handles(model: ImageContentRenderer.Data): Boolean {
// Always handle // 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<InputStream>? { override fun buildLoadData(model: ImageContentRenderer.Data, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
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, context: Context,
private val data: ImageContentRenderer.Data, private val data: ImageContentRenderer.Data,
private val width: Int, private val width: Int,
@ -71,8 +70,6 @@ class VectorGlideDataFetcher(
private val localFilesHelper = LocalFilesHelper(context) private val localFilesHelper = LocalFilesHelper(context)
private val activeSessionHolder = context.singletonEntryPoint().activeSessionHolder() private val activeSessionHolder = context.singletonEntryPoint().activeSessionHolder()
private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient()
override fun getDataClass(): Class<InputStream> { override fun getDataClass(): Class<InputStream> {
return InputStream::class.java return InputStream::class.java
} }

View file

@ -38,7 +38,7 @@ class MyAppGlideModule : AppGlideModule() {
registry.append( registry.append(
ImageContentRenderer.Data::class.java, ImageContentRenderer.Data::class.java,
InputStream::class.java, InputStream::class.java,
VectorGlideModelLoaderFactory(context) ImageContentRendererDataLoaderFactory(context)
) )
registry.append( registry.append(
AvatarPlaceholder::class.java, AvatarPlaceholder::class.java,