Authenticated media : makes usage of API when server supports it

This commit is contained in:
ganfra 2024-07-09 11:57:55 +02:00
parent 7ad3ccfc60
commit da8c892f7a
24 changed files with 351 additions and 96 deletions

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

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

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

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

@ -40,7 +40,8 @@ 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.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.MediaAPIProvider
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
@ -56,7 +57,7 @@ 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 mediaAPIProvider: MediaAPIProvider,
@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,
@ -71,7 +72,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
} }
} }
@ -88,7 +88,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
val mediaConfig = runCatching { val mediaConfig = runCatching {
executeRequest(globalErrorReceiver) { executeRequest(globalErrorReceiver) {
mediaAPI.getMediaConfig() mediaAPIProvider.getMediaAPI().getMediaConfig()
} }
}.getOrNull() }.getOrNull()

View file

@ -0,0 +1,45 @@
/*
* 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 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,45 @@
/*
* 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 org.matrix.android.sdk.internal.session.media
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
class DefaultIsAuthenticatedMediaSupported @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
) : IsAuthenticatedMediaSupported {
private val canUseAuthenticatedMedia: Boolean by lazy {
canUseAuthenticatedMedia()
}
override fun invoke(): Boolean {
return canUseAuthenticatedMedia
}
private fun canUseAuthenticatedMedia(): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
HomeServerCapabilitiesEntity.get(realm)?.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,7 +30,7 @@ internal interface GetRawPreviewUrlTask : Task<GetRawPreviewUrlTask.Params, Json
} }
internal class DefaultGetRawPreviewUrlTask @Inject constructor( internal class DefaultGetRawPreviewUrlTask @Inject constructor(
private val mediaAPI: MediaAPI, private val mediaAPI: UnauthenticatedMediaAPI,
private val globalErrorReceiver: GlobalErrorReceiver private val globalErrorReceiver: GlobalErrorReceiver
) : GetRawPreviewUrlTask { ) : GetRawPreviewUrlTask {

View file

@ -0,0 +1,21 @@
/*
* 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 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 New Vector Ltd
* *
* 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 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 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 2020 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

@ -50,7 +50,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,68 @@
/*
* 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

@ -37,10 +37,10 @@ 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 +48,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 +56,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 +71,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,