diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 56609610f1..8a95baf3cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -181,6 +182,11 @@ interface Session : */ fun widgetService(): WidgetService + /** + * Returns the media service associated with the session + */ + fun mediaService(): MediaService + /** * Returns the integration manager service associated with the session */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt index 6594b2d0b5..9040ec7d5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt @@ -29,14 +29,19 @@ interface MediaService { /** * Get Raw Url Preview data from the homeserver. There is no cache management for this request + * @param url The url to get the preview data from + * @param timestamp The optional timestamp */ - suspend fun getRawPreviewUrl(url: String): JsonDict + suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict /** * Get Url Preview data from the homeserver, or from cache, depending on the cache strategy - * @param url + * @param url The url to get the preview data from + * @param timestamp The optional timestamp. Note that this parameter is not taken into account + * if the data is already in cache and the cache strategy allow to use it + * @param cacheStrategy the cache strategy, see the type for more details */ - suspend fun getPreviewUrl(url: String, cacheStrategy: CacheStrategy): PreviewUrlData + suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData /** * Clear the cache of all retrieved UrlPreview data diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 25345e953c..c5f3f65a34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -102,6 +103,7 @@ internal class DefaultSession @Inject constructor( private val permalinkService: Lazy, private val secureStorageService: Lazy, private val profileService: Lazy, + private val mediaService: Lazy, private val widgetService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, @@ -263,6 +265,8 @@ internal class DefaultSession @Inject constructor( override fun widgetService(): WidgetService = widgetService.get() + override fun mediaService(): MediaService = mediaService.get() + override fun integrationManagerService() = integrationManagerService override fun callSignalingService(): CallSignalingService = callSignalingService.get() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt new file mode 100644 index 0000000000..004b622c64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt @@ -0,0 +1,40 @@ +/* + * 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 com.zhuinden.monarchy.Monarchy +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface ClearPreviewUrlCacheTask : Task + +internal class DefaultClearPreviewUrlCacheTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy +) : ClearPreviewUrlCacheTask { + + override suspend fun execute(params: Unit) { + monarchy.awaitTransaction { realm -> + realm.where() + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt new file mode 100644 index 0000000000..846b81ae72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt @@ -0,0 +1,46 @@ +/* + * 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.cache.CacheStrategy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.api.util.JsonDict +import javax.inject.Inject + +internal class DefaultMediaService @Inject constructor( + private val clearPreviewUrlCacheTask: ClearPreviewUrlCacheTask, + private val getPreviewUrlTask: GetPreviewUrlTask, + private val getRawPreviewUrlTask: GetRawPreviewUrlTask +) : MediaService { + override fun extractUrls(event: Event): List { + TODO("Not yet implemented") + } + + override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict { + return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp)) + } + + override suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData { + return getPreviewUrlTask.execute(GetPreviewUrlTask.Params(url, timestamp, cacheStrategy)) + } + + override suspend fun clearCache() { + clearPreviewUrlCacheTask.execute(Unit) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt new file mode 100644 index 0000000000..69cdfa8faa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt @@ -0,0 +1,122 @@ +/* + * 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 com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import java.util.Date +import javax.inject.Inject + +internal interface GetPreviewUrlTask : Task { + data class Params( + val url: String, + val timestamp: Long?, + val cacheStrategy: CacheStrategy + ) +} + +internal class DefaultGetPreviewUrlTask @Inject constructor( + private val mediaAPI: MediaAPI, + private val eventBus: EventBus, + @SessionDatabase private val monarchy: Monarchy +) : GetPreviewUrlTask { + + override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData { + return when (params.cacheStrategy) { + CacheStrategy.NoCache -> doRequest(params.url, params.timestamp) + is CacheStrategy.TtlCache -> doRequestWithCache( + params.url, + params.timestamp, + params.cacheStrategy.validityDurationInMillis, + params.cacheStrategy.strict + ) + CacheStrategy.InfiniteCache -> doRequestWithCache( + params.url, + params.timestamp, + Long.MAX_VALUE, + true + ) + } + } + + private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData { + return executeRequest(eventBus) { + apiCall = mediaAPI.getPreviewUrlData(url, timestamp) + } + .toPreviewUrlData(url) + } + + private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData { + return PreviewUrlData( + url = (get("og:url") as? String) ?: url, + siteName = get("og:site_name") as? String, + title = get("og:title") as? String, + description = get("og:description") as? String, + mxcUrl = get("og:image") as? String + ) + } + + private suspend fun doRequestWithCache(url: String, timestamp: Long?, validityDurationInMillis: Long, strict: Boolean): PreviewUrlData { + // Get data from cache + var dataFromCache: PreviewUrlData? = null + var isCacheValid = false + monarchy.doWithRealm { realm -> + val entity = PreviewUrlCacheEntity.get(realm, url) + dataFromCache = entity?.toDomain() + isCacheValid = entity != null && Date().time < entity.lastUpdatedTimestamp + validityDurationInMillis + } + + val finalDataFromCache = dataFromCache + if (finalDataFromCache != null && isCacheValid) { + return finalDataFromCache + } + + // No cache or outdated cache + val data = try { + doRequest(url, timestamp) + } catch (throwable: Throwable) { + // In case of error, we can return value from cache even if outdated + return finalDataFromCache + ?.takeIf { !strict } + ?: throw throwable + } + + // Store cache + monarchy.awaitTransaction { realm -> + val previewUrlCacheEntity = PreviewUrlCacheEntity.getOrCreate(realm, url) + previewUrlCacheEntity.urlFromServer = data.url + previewUrlCacheEntity.siteName = data.siteName + previewUrlCacheEntity.title = data.title + previewUrlCacheEntity.description = data.description + previewUrlCacheEntity.mxcUrl = data.mxcUrl + + previewUrlCacheEntity.lastUpdatedTimestamp = Date().time + } + + return data + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt new file mode 100644 index 0000000000..6c5dad2422 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt @@ -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.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetRawPreviewUrlTask : Task { + data class Params( + val url: String, + val timestamp: Long? + ) +} + +internal class DefaultGetRawPreviewUrlTask @Inject constructor( + private val mediaAPI: MediaAPI, + private val eventBus: EventBus +) : GetRawPreviewUrlTask { + + override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict { + return executeRequest(eventBus) { + apiCall = mediaAPI.getPreviewUrlData(params.url, params.timestamp) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt index 821d9b9875..bbb4f1e06a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt @@ -39,5 +39,5 @@ internal interface MediaAPI { * if it does not have the requested version available. */ @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url") - fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Int?): Call + fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): Call } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt index 84a82dc75a..bc58b3f444 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt @@ -16,8 +16,10 @@ package org.matrix.android.sdk.internal.session.media +import dagger.Binds import dagger.Module import dagger.Provides +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.internal.session.SessionScope import retrofit2.Retrofit @@ -34,6 +36,15 @@ internal abstract class MediaModule { } } -// @Binds -// abstract fun bindGetHomeServerCapabilitiesTask(task: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask + @Binds + abstract fun bindMediaService(service: DefaultMediaService): MediaService + + @Binds + abstract fun bindGetRawPreviewUrlTask(task: DefaultGetRawPreviewUrlTask): GetRawPreviewUrlTask + + @Binds + abstract fun bindGetPreviewUrlTask(task: DefaultGetPreviewUrlTask): GetPreviewUrlTask + + @Binds + abstract fun bindClearMediaCacheTask(task: DefaultClearPreviewUrlCacheTask): ClearPreviewUrlCacheTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt new file mode 100644 index 0000000000..dd1a9ead26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt @@ -0,0 +1,31 @@ +/* + * 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.session.media.PreviewUrlData +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity + +/** + * PreviewUrlCacheEntity -> PreviewUrlData + */ +internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData( + url = urlFromServer ?: url, + siteName = siteName, + title = title, + description = description, + mxcUrl = mxcUrl +)