+
+ /**
+ * 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, timestamp: Long?): JsonDict
+
+ /**
+ * Get Url Preview data from the homeserver, or from cache, depending on the cache strategy
+ * @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, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData
+
+ /**
+ * Clear the cache of all retrieved UrlPreview data
+ */
+ suspend fun clearCache()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt
new file mode 100644
index 0000000000..33fc8b052b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.api.session.media
+
+/**
+ * Facility data class to get the common field of a PreviewUrl response form the server
+ *
+ * Example of return data for the url `https://matrix.org`:
+ *
+ * {
+ * "matrix:image:size": 112805,
+ * "og:description": "Matrix is an open standard for interoperable, decentralised, real-time communication",
+ * "og:image": "mxc://matrix.org/2020-12-03_uFqjagCCTJbaaJxb",
+ * "og:image:alt": "Matrix is an open standard for interoperable, decentralised, real-time communication",
+ * "og:image:height": 467,
+ * "og:image:type": "image/jpeg",
+ * "og:image:width": 911,
+ * "og:locale": "en_US",
+ * "og:site_name": "Matrix.org",
+ * "og:title": "Matrix.org",
+ * "og:type": "website",
+ * "og:url": "https://matrix.org"
+ * }
+ *
+ */
+data class PreviewUrlData(
+ // Value of field "og:url". If not provided, this is the value passed in parameter
+ val url: String,
+ // Value of field "og:site_name"
+ val siteName: String?,
+ // Value of field "og:title"
+ val title: String?,
+ // Value of field "og:description"
+ val description: String?,
+ // Value of field "og:image"
+ val mxcUrl: String?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt
index 859f7fd104..73e27b64e3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt
@@ -20,6 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
@JsonClass(generateAdapter = true)
@@ -54,5 +55,5 @@ data class MessageImageContent(
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageImageInfoContent {
override val mimeType: String?
- get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*"
+ get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: MimeTypes.Images
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
index 74e3faf38a..444366e912 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
@@ -18,13 +18,11 @@ package org.matrix.android.sdk.api.session.room.state
import android.net.Uri
import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
-import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional
@@ -33,41 +31,41 @@ interface StateService {
/**
* Update the topic of the room
*/
- fun updateTopic(topic: String, callback: MatrixCallback): Cancelable
+ suspend fun updateTopic(topic: String)
/**
* Update the name of the room
*/
- fun updateName(name: String, callback: MatrixCallback): Cancelable
+ suspend fun updateName(name: String)
/**
* Update the canonical alias of the room
* @param alias the canonical alias, or null to reset the canonical alias of this room
* @param altAliases the alternative aliases for this room. It should include the canonical alias if any.
*/
- fun updateCanonicalAlias(alias: String?, altAliases: List, callback: MatrixCallback): Cancelable
+ suspend fun updateCanonicalAlias(alias: String?, altAliases: List)
/**
* Update the history readability of the room
*/
- fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable
+ suspend fun updateHistoryReadability(readability: RoomHistoryVisibility)
/**
* Update the join rule and/or the guest access
*/
- fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, callback: MatrixCallback): Cancelable
+ suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?)
/**
* Update the avatar of the room
*/
- fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable
+ suspend fun updateAvatar(avatarUri: Uri, fileName: String)
/**
* Delete the avatar of the room
*/
- fun deleteAvatar(callback: MatrixCallback): Cancelable
+ suspend fun deleteAvatar()
- fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback): Cancelable
+ suspend fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict)
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt
new file mode 100644
index 0000000000..c74999b4ab
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.api.util
+
+import org.matrix.android.sdk.api.extensions.orFalse
+
+// The Android SDK does not provide constant for mime type, add some of them here
+object MimeTypes {
+ const val Any: String = "*/*"
+ const val OctetStream = "application/octet-stream"
+
+ const val Images = "image/*"
+
+ const val Png = "image/png"
+ const val BadJpg = "image/jpg"
+ const val Jpeg = "image/jpeg"
+ const val Gif = "image/gif"
+
+ fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this
+
+ fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse()
+ fun String?.isMimeTypeVideo() = this?.startsWith("video/").orFalse()
+ fun String?.isMimeTypeAudio() = this?.startsWith("audio/").orFalse()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 973388da49..b970ec60e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -20,6 +20,7 @@ import io.realm.DynamicRealm
import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
+import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import timber.log.Timber
import javax.inject.Inject
@@ -27,7 +28,7 @@ import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object {
- const val SESSION_STORE_SCHEMA_VERSION = 5L
+ const val SESSION_STORE_SCHEMA_VERSION = 6L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@@ -38,6 +39,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm)
+ if (oldVersion <= 5) migrateTo6(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@@ -89,4 +91,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
?.removeField("adminE2EByDefault")
?.removeField("preferredJitsiDomain")
}
+
+ private fun migrateTo6(realm: DynamicRealm) {
+ Timber.d("Step 5 -> 6")
+ realm.schema.create("PreviewUrlCacheEntity")
+ .addField(PreviewUrlCacheEntityFields.URL, String::class.java)
+ .setRequired(PreviewUrlCacheEntityFields.URL, true)
+ .addPrimaryKey(PreviewUrlCacheEntityFields.URL)
+ .addField(PreviewUrlCacheEntityFields.URL_FROM_SERVER, String::class.java)
+ .addField(PreviewUrlCacheEntityFields.SITE_NAME, String::class.java)
+ .addField(PreviewUrlCacheEntityFields.TITLE, String::class.java)
+ .addField(PreviewUrlCacheEntityFields.DESCRIPTION, String::class.java)
+ .addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java)
+ .addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java)
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt
new file mode 100644
index 0000000000..b1e0b64405
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.database.model
+
+import io.realm.RealmObject
+import io.realm.annotations.PrimaryKey
+
+internal open class PreviewUrlCacheEntity(
+ @PrimaryKey
+ var url: String = "",
+
+ var urlFromServer: String? = null,
+ var siteName: String? = null,
+ var title: String? = null,
+ var description: String? = null,
+ var mxcUrl: String? = null,
+
+ var lastUpdatedTimestamp: Long = 0L
+) : RealmObject() {
+
+ companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index f62312f8fc..bca2c42c9e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -48,6 +48,7 @@ import io.realm.annotations.RealmModule
PushRulesEntity::class,
PushRuleEntity::class,
PushConditionEntity::class,
+ PreviewUrlCacheEntity::class,
PusherEntity::class,
PusherDataEntity::class,
ReadReceiptsSummaryEntity::class,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt
new file mode 100644
index 0000000000..a139c17439
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.database.query
+
+import io.realm.Realm
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
+import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
+
+/**
+ * Get the current PreviewUrlCacheEntity, return null if it does not exist
+ */
+internal fun PreviewUrlCacheEntity.Companion.get(realm: Realm, url: String): PreviewUrlCacheEntity? {
+ return realm.where()
+ .equalTo(PreviewUrlCacheEntityFields.URL, url)
+ .findFirst()
+}
+
+/**
+ * Get the current PreviewUrlCacheEntity, create one if it does not exist
+ */
+internal fun PreviewUrlCacheEntity.Companion.getOrCreate(realm: Realm, url: String): PreviewUrlCacheEntity {
+ return get(realm, url) ?: realm.createObject(url)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
index d3f08fde36..f959104e11 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
@@ -71,9 +71,6 @@ internal interface MatrixComponent {
@CacheDirectory
fun cacheDir(): File
- @ExternalFilesDirectory
- fun externalFilesDir(): File?
-
fun olmManager(): OlmManager
fun taskExecutor(): TaskExecutor
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt
index 71cbd8f1a1..b58fb3e683 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt
@@ -57,13 +57,6 @@ internal object MatrixModule {
return context.cacheDir
}
- @JvmStatic
- @Provides
- @ExternalFilesDirectory
- fun providesExternalFilesDir(context: Context): File? {
- return context.getExternalFilesDir(null)
- }
-
@JvmStatic
@Provides
@MatrixScope
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt
index e6cec7f7ac..2535a5347a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt
@@ -16,14 +16,15 @@
package org.matrix.android.sdk.internal.network
-import org.matrix.android.sdk.api.failure.Failure
-import org.matrix.android.sdk.api.failure.shouldBeRetried
-import org.matrix.android.sdk.internal.network.ssl.CertUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus
+import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.failure.shouldBeRetried
+import org.matrix.android.sdk.internal.network.ssl.CertUtil
import retrofit2.Call
import retrofit2.awaitResponse
+import timber.log.Timber
import java.io.IOException
internal suspend inline fun executeRequest(eventBus: EventBus?,
@@ -49,6 +50,9 @@ internal class Request(private val eventBus: EventBus?) {
throw response.toFailure(eventBus)
}
} catch (exception: Throwable) {
+ // Log some details about the request which has failed
+ Timber.e("Exception when executing request ${apiCall.request().method} ${apiCall.request().url.toString().substringBefore("?")}")
+
// Check if this is a certificateException
CertUtil.getCertificateException(exception)
// TODO Support certificate error once logged
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultCleanRawCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/CleanRawCacheTask.kt
similarity index 100%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultCleanRawCacheTask.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/CleanRawCacheTask.kt
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt
index 3b0d7546e5..42b826de16 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt
@@ -16,7 +16,7 @@
package org.matrix.android.sdk.internal.raw
-import org.matrix.android.sdk.api.raw.RawCacheStrategy
+import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.raw.RawService
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -25,15 +25,15 @@ internal class DefaultRawService @Inject constructor(
private val getUrlTask: GetUrlTask,
private val cleanRawCacheTask: CleanRawCacheTask
) : RawService {
- override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String {
- return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy))
+ override suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String {
+ return getUrlTask.execute(GetUrlTask.Params(url, cacheStrategy))
}
override suspend fun getWellknown(userId: String): String {
val homeServerDomain = userId.substringAfter(":")
return getUrl(
"https://$homeServerDomain/.well-known/matrix/client",
- RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
+ CacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt
similarity index 86%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt
index 1f4ca6d627..16633d90ef 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.raw
import com.zhuinden.monarchy.Monarchy
import okhttp3.ResponseBody
-import org.matrix.android.sdk.api.raw.RawCacheStrategy
+import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.database.query.getOrCreate
@@ -32,7 +32,7 @@ import javax.inject.Inject
internal interface GetUrlTask : Task {
data class Params(
val url: String,
- val rawCacheStrategy: RawCacheStrategy
+ val cacheStrategy: CacheStrategy
)
}
@@ -42,14 +42,14 @@ internal class DefaultGetUrlTask @Inject constructor(
) : GetUrlTask {
override suspend fun execute(params: GetUrlTask.Params): String {
- return when (params.rawCacheStrategy) {
- RawCacheStrategy.NoCache -> doRequest(params.url)
- is RawCacheStrategy.TtlCache -> doRequestWithCache(
+ return when (params.cacheStrategy) {
+ CacheStrategy.NoCache -> doRequest(params.url)
+ is CacheStrategy.TtlCache -> doRequestWithCache(
params.url,
- params.rawCacheStrategy.validityDurationInMillis,
- params.rawCacheStrategy.strict
+ params.cacheStrategy.validityDurationInMillis,
+ params.cacheStrategy.strict
)
- RawCacheStrategy.InfiniteCache -> doRequestWithCache(
+ CacheStrategy.InfiniteCache -> doRequestWithCache(
params.url,
Long.MAX_VALUE,
true
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
index 861ae7c7ee..07cde3da60 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
@@ -21,6 +21,10 @@ import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import arrow.core.Try
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import okhttp3.Request
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
@@ -29,35 +33,21 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
-import org.matrix.android.sdk.internal.di.CacheDirectory
-import org.matrix.android.sdk.internal.di.ExternalFilesDirectory
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.internal.util.md5
import org.matrix.android.sdk.internal.util.toCancelable
import org.matrix.android.sdk.internal.util.writeToFile
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okio.buffer
-import okio.sink
-import okio.source
import timber.log.Timber
import java.io.File
import java.io.IOException
-import java.io.InputStream
-import java.net.URLEncoder
import javax.inject.Inject
internal class DefaultFileService @Inject constructor(
private val context: Context,
- @CacheDirectory
- private val cacheDirectory: File,
- @ExternalFilesDirectory
- private val externalFilesDirectory: File?,
@SessionDownloadsDirectory
private val sessionCacheDirectory: File,
private val contentUrlResolver: ContentUrlResolver,
@@ -67,9 +57,17 @@ internal class DefaultFileService @Inject constructor(
private val taskExecutor: TaskExecutor
) : FileService {
- private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName())
+ // Legacy folder, will be deleted
+ private val legacyFolder = File(sessionCacheDirectory, "MF")
+ // Folder to store downloaded files (not decrypted)
+ private val downloadFolder = File(sessionCacheDirectory, "F")
+ // Folder to store decrypted files
+ private val decryptedFolder = File(downloadFolder, "D")
- private val downloadFolder = File(sessionCacheDirectory, "MF")
+ init {
+ // Clear the legacy downloaded files
+ legacyFolder.deleteRecursively()
+ }
/**
* Retain ongoing downloads to avoid re-downloading and already downloading file
@@ -81,28 +79,26 @@ internal class DefaultFileService @Inject constructor(
* Download file in the cache folder, and eventually decrypt it
* TODO looks like files are copied 3 times
*/
- override fun downloadFile(downloadMode: FileService.DownloadMode,
- id: String,
- fileName: String,
+ override fun downloadFile(fileName: String,
mimeType: String?,
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback): Cancelable {
- val unwrappedUrl = url ?: return NoOpCancellable.also {
+ url ?: return NoOpCancellable.also {
callback.onFailure(IllegalArgumentException("url is null"))
}
- Timber.v("## FileService downloadFile $unwrappedUrl")
+ Timber.v("## FileService downloadFile $url")
synchronized(ongoing) {
- val existing = ongoing[unwrappedUrl]
+ val existing = ongoing[url]
if (existing != null) {
Timber.v("## FileService downloadFile is already downloading.. ")
existing.add(callback)
return NoOpCancellable
} else {
// mark as tracked
- ongoing[unwrappedUrl] = ArrayList()
+ ongoing[url] = ArrayList()
// and proceed to download
}
}
@@ -110,15 +106,15 @@ internal class DefaultFileService @Inject constructor(
return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) {
Try {
- if (!downloadFolder.exists()) {
- downloadFolder.mkdirs()
+ if (!decryptedFolder.exists()) {
+ decryptedFolder.mkdirs()
}
// ensure we use unique file name by using URL (mapped to suitable file name)
// Also we need to add extension for the FileProvider, if not it lot's of app that it's
// shared with will not function well (even if mime type is passed in the intent)
- File(downloadFolder, fileForUrl(unwrappedUrl, mimeType))
- }.flatMap { destFile ->
- if (!destFile.exists()) {
+ getFiles(url, fileName, mimeType, elementToDecrypt != null)
+ }.flatMap { cachedFiles ->
+ if (!cachedFiles.file.exists()) {
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
val request = Request.Builder()
@@ -141,79 +137,153 @@ internal class DefaultFileService @Inject constructor(
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
- if (elementToDecrypt != null) {
- Timber.v("## FileService: decrypt file")
- val decryptSuccess = destFile.outputStream().buffered().use {
- MXEncryptedAttachments.decryptAttachment(
- source.inputStream(),
- elementToDecrypt,
- it
- )
- }
- response.close()
- if (!decryptSuccess) {
- return@flatMap Try.Failure(IllegalStateException("Decryption error"))
- }
- } else {
- writeToFile(source.inputStream(), destFile)
- response.close()
- }
+ // Write the file to cache (encrypted version if the file is encrypted)
+ writeToFile(source.inputStream(), cachedFiles.file)
+ response.close()
} else {
Timber.v("## FileService: cache hit for $url")
}
- Try.just(copyFile(destFile, downloadMode))
+ Try.just(cachedFiles)
}
- }.fold({
- callback.onFailure(it)
- // notify concurrent requests
- val toNotify = synchronized(ongoing) {
- ongoing[unwrappedUrl]?.also {
- ongoing.remove(unwrappedUrl)
+ }.flatMap { cachedFiles ->
+ // Decrypt if necessary
+ if (cachedFiles.decryptedFile != null) {
+ if (!cachedFiles.decryptedFile.exists()) {
+ Timber.v("## FileService: decrypt file")
+ // Ensure the parent folder exists
+ cachedFiles.decryptedFile.parentFile?.mkdirs()
+ val decryptSuccess = cachedFiles.file.inputStream().use { inputStream ->
+ cachedFiles.decryptedFile.outputStream().buffered().use { outputStream ->
+ MXEncryptedAttachments.decryptAttachment(
+ inputStream,
+ elementToDecrypt,
+ outputStream
+ )
+ }
+ }
+ if (!decryptSuccess) {
+ return@flatMap Try.Failure(IllegalStateException("Decryption error"))
+ }
+ } else {
+ Timber.v("## FileService: cache hit for decrypted file")
}
+ Try.just(cachedFiles.decryptedFile)
+ } else {
+ // Clear file
+ Try.just(cachedFiles.file)
}
- toNotify?.forEach { otherCallbacks ->
- tryOrNull { otherCallbacks.onFailure(it) }
- }
- }, { file ->
- callback.onSuccess(file)
- // notify concurrent requests
- val toNotify = synchronized(ongoing) {
- ongoing[unwrappedUrl]?.also {
- ongoing.remove(unwrappedUrl)
+ }.fold(
+ { throwable ->
+ callback.onFailure(throwable)
+ // notify concurrent requests
+ val toNotify = synchronized(ongoing) {
+ ongoing[url]?.also {
+ ongoing.remove(url)
+ }
+ }
+ toNotify?.forEach { otherCallbacks ->
+ tryOrNull { otherCallbacks.onFailure(throwable) }
+ }
+ },
+ { file ->
+ callback.onSuccess(file)
+ // notify concurrent requests
+ val toNotify = synchronized(ongoing) {
+ ongoing[url]?.also {
+ ongoing.remove(url)
+ }
+ }
+ Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
+ toNotify?.forEach { otherCallbacks ->
+ tryOrNull { otherCallbacks.onSuccess(file) }
+ }
}
- }
- Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
- toNotify?.forEach { otherCallbacks ->
- tryOrNull { otherCallbacks.onSuccess(file) }
- }
- })
+ )
}.toCancelable()
}
- fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) {
- val file = File(downloadFolder, fileForUrl(url, mimeType))
- val source = inputStream.source().buffer()
- file.sink().buffer().let { sink ->
- source.use { input ->
- sink.use { output ->
- output.writeAll(input)
+ fun storeDataFor(mxcUrl: String,
+ filename: String?,
+ mimeType: String?,
+ originalFile: File,
+ encryptedFile: File?) {
+ val files = getFiles(mxcUrl, filename, mimeType, encryptedFile != null)
+ if (encryptedFile != null) {
+ // We switch the two files here, original file it the decrypted file
+ files.decryptedFile?.let { originalFile.copyTo(it) }
+ encryptedFile.copyTo(files.file)
+ } else {
+ // Just copy the original file
+ originalFile.copyTo(files.file)
+ }
+ }
+
+ private fun safeFileName(fileName: String?, mimeType: String?): String {
+ return buildString {
+ // filename has to be safe for the Android System
+ val result = fileName
+ ?.replace("[^a-z A-Z0-9\\\\.\\-]".toRegex(), "_")
+ ?.takeIf { it.isNotEmpty() }
+ ?: DEFAULT_FILENAME
+ append(result)
+ // Check that the extension is correct regarding the mimeType
+ val extensionFromMime = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
+ if (extensionFromMime != null) {
+ // Compare
+ val fileExtension = result.substringAfterLast(delimiter = ".", missingDelimiterValue = "")
+ if (fileExtension.isEmpty() || fileExtension != extensionFromMime) {
+ // Missing extension, or diff in extension, add the one provided by the mimetype
+ append(".")
+ append(extensionFromMime)
}
}
}
}
- private fun fileForUrl(url: String, mimeType: String?): String {
- val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
- return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName()
+ override fun isFileInCache(mxcUrl: String?,
+ fileName: String,
+ mimeType: String?,
+ elementToDecrypt: ElementToDecrypt?): Boolean {
+ return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE
}
- override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean {
- return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists()
+ internal data class CachedFiles(
+ // This is the downloaded file. Can be clear or encrypted
+ val file: File,
+ // This is the decrypted file. Null if the original file is not encrypted
+ val decryptedFile: File?
+ ) {
+ fun getClearFile(): File = decryptedFile ?: file
}
- override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState {
- if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE
+ private fun getFiles(mxcUrl: String,
+ fileName: String?,
+ mimeType: String?,
+ isEncrypted: Boolean): CachedFiles {
+ val hashFolder = mxcUrl.md5()
+ val safeFileName = safeFileName(fileName, mimeType)
+ return if (isEncrypted) {
+ // Encrypted file
+ CachedFiles(
+ File(downloadFolder, "$hashFolder/$ENCRYPTED_FILENAME"),
+ File(decryptedFolder, "$hashFolder/$safeFileName")
+ )
+ } else {
+ // Clear file
+ CachedFiles(
+ File(downloadFolder, "$hashFolder/$safeFileName"),
+ null
+ )
+ }
+ }
+
+ override fun fileState(mxcUrl: String?,
+ fileName: String,
+ mimeType: String?,
+ elementToDecrypt: ElementToDecrypt?): FileService.FileState {
+ mxcUrl ?: return FileService.FileState.UNKNOWN
+ if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE
val isDownloading = synchronized(ongoing) {
ongoing[mxcUrl] != null
}
@@ -224,26 +294,18 @@ internal class DefaultFileService @Inject constructor(
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION
* (if not other app won't be able to access it)
*/
- override fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? {
+ override fun getTemporarySharableURI(mxcUrl: String?,
+ fileName: String,
+ mimeType: String?,
+ elementToDecrypt: ElementToDecrypt?): Uri? {
+ mxcUrl ?: return null
// this string could be extracted no?
val authority = "${context.packageName}.mx-sdk.fileprovider"
- val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType))
+ val targetFile = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).getClearFile()
if (!targetFile.exists()) return null
return FileProvider.getUriForFile(context, authority, targetFile)
}
- private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
- // TODO some of this seems outdated, will need to be re-worked
- return when (downloadMode) {
- FileService.DownloadMode.TO_EXPORT ->
- file.copyTo(File(externalFilesDirectory, file.name), true)
- FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
- file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
- FileService.DownloadMode.FOR_INTERNAL_USE ->
- file
- }
- }
-
override fun getCacheSize(): Int {
return downloadFolder.walkTopDown()
.onEnter {
@@ -256,4 +318,14 @@ internal class DefaultFileService @Inject constructor(
override fun clearCache() {
downloadFolder.deleteRecursively()
}
+
+ override fun clearDecryptedCache() {
+ decryptedFolder.deleteRecursively()
+ }
+
+ companion object {
+ private const val ENCRYPTED_FILENAME = "encrypted.bin"
+ // The extension would be added from the mimetype
+ private const val DEFAULT_FILENAME = "file"
+ }
}
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/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
index e6fd5a7a0c..659fcc8f5c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
@@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.session.group.GroupModule
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule
import org.matrix.android.sdk.internal.session.identity.IdentityModule
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule
+import org.matrix.android.sdk.internal.session.media.MediaModule
import org.matrix.android.sdk.internal.session.openid.OpenIdModule
import org.matrix.android.sdk.internal.session.profile.ProfileModule
import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
@@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
GroupModule::class,
ContentModule::class,
CacheModule::class,
+ MediaModule::class,
CryptoModule::class,
PushersModule::class,
OpenIdModule::class,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
index 32949d60c4..96b44917bd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
@@ -50,6 +50,7 @@ import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
import org.matrix.android.sdk.internal.di.Authenticated
+import org.matrix.android.sdk.internal.di.CacheDirectory
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
@@ -169,9 +170,9 @@ internal abstract class SessionModule {
@JvmStatic
@Provides
@SessionDownloadsDirectory
- fun providesCacheDir(@SessionId sessionId: String,
- context: Context): File {
- return File(context.cacheDir, "downloads/$sessionId")
+ fun providesDownloadsCacheDir(@SessionId sessionId: String,
+ @CacheDirectory cacheFile: File): File {
+ return File(cacheFile, "downloads/$sessionId")
}
@JvmStatic
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt
index b5de26b39d..1ebe5b2eb6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt
@@ -20,6 +20,9 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
-data class ContentUploadResponse(
+internal data class ContentUploadResponse(
+ /**
+ * Required. The MXC URI to the uploaded content.
+ */
@Json(name = "content_uri") val contentUri: String
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt
index 8c3aad6a1f..4b31db59b1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt
@@ -20,6 +20,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
+import org.matrix.android.sdk.api.util.MimeTypes
import timber.log.Timber
import java.io.ByteArrayOutputStream
@@ -58,7 +59,7 @@ internal object ThumbnailExtractor {
height = thumbnailHeight,
size = thumbnailSize.toLong(),
bytes = outputStream.toByteArray(),
- mimeType = "image/jpeg"
+ mimeType = MimeTypes.Jpeg
)
thumbnail.recycle()
outputStream.reset()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
index 4a30d6c1e6..672d407d25 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
+import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
@@ -151,7 +152,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
params.attachment.size
)
- if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) {
+ if (attachment.type == ContentAttachmentData.Type.IMAGE
+ // Do not compress gif
+ && attachment.mimeType != MimeTypes.Gif
+ && params.compressBeforeSending) {
fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
.also { compressedFile ->
// Get new Bitmap size
@@ -174,14 +178,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
}
}
+ val encryptedFile: File?
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("## FileService: Encrypt file")
- val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
+ encryptedFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
.also { filesToDelete.add(it) }
uploadedFileEncryptedFileInfo =
- MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total ->
+ MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), encryptedFile) { read, total ->
notifyTracker(params) {
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
}
@@ -190,18 +195,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
Timber.v("## FileService: Uploading file")
fileUploader
- .uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener)
+ .uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener)
} else {
Timber.v("## FileService: Clear file")
+ encryptedFile = null
fileUploader
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
}
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
try {
- context.contentResolver.openInputStream(attachment.queryUri)?.let {
- fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it)
- }
+ fileService.storeDataFor(
+ mxcUrl = contentUploadResponse.contentUri,
+ filename = params.attachment.name,
+ mimeType = params.attachment.getSafeMimeType(),
+ originalFile = workingFile,
+ encryptedFile = encryptedFile
+ )
Timber.v("## FileService: cache storage updated")
} catch (failure: Throwable) {
Timber.e(failure, "## FileService: Failed to update file cache")
@@ -252,7 +262,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${params.attachment.name}",
- "application/octet-stream",
+ MimeTypes.OctetStream,
thumbnailProgressListener)
UploadThumbnailResult(
contentUploadResponse.contentUri,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
similarity index 100%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt
similarity index 100%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt
index 39b6608de3..8242edac84 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt
@@ -22,19 +22,12 @@ import retrofit2.Call
import retrofit2.http.GET
internal interface CapabilitiesAPI {
-
/**
* Request the homeserver capabilities
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities")
fun getCapabilities(): Call
- /**
- * Request the upload capabilities
- */
- @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
- fun getUploadCapabilities(): Call
-
/**
* Request the versions
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
similarity index 89%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index 8d289dfda5..f3686b02d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -29,6 +29,8 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor
+import org.matrix.android.sdk.internal.session.media.GetMediaConfigResult
+import org.matrix.android.sdk.internal.session.media.MediaAPI
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
@@ -40,6 +42,7 @@ internal interface GetHomeServerCapabilitiesTask : Task
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val capabilitiesAPI: CapabilitiesAPI,
+ private val mediaAPI: MediaAPI,
@SessionDatabase private val monarchy: Monarchy,
private val eventBus: EventBus,
private val getWellknownTask: GetWellknownTask,
@@ -67,9 +70,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
}
}.getOrNull()
- val uploadCapabilities = runCatching {
- executeRequest(eventBus) {
- apiCall = capabilitiesAPI.getUploadCapabilities()
+ val mediaConfig = runCatching {
+ executeRequest(eventBus) {
+ apiCall = mediaAPI.getMediaConfig()
}
}.getOrNull()
@@ -83,11 +86,11 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig))
}.getOrNull()
- insertInDb(capabilities, uploadCapabilities, versions, wellknownResult)
+ insertInDb(capabilities, mediaConfig, versions, wellknownResult)
}
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?,
- getUploadCapabilitiesResult: GetUploadCapabilitiesResult?,
+ getMediaConfigResult: GetMediaConfigResult?,
getVersionResult: Versions?,
getWellknownResult: WellknownResult?) {
monarchy.awaitTransaction { realm ->
@@ -97,8 +100,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
}
- if (getUploadCapabilitiesResult != null) {
- homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
+ if (getMediaConfigResult != null) {
+ homeServerCapabilitiesEntity.maxUploadFileSize = getMediaConfigResult.maxUploadSize
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
}
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..1a400ccfcf
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 androidx.collection.LruCache
+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 org.matrix.android.sdk.internal.util.getOrPut
+import javax.inject.Inject
+
+internal class DefaultMediaService @Inject constructor(
+ private val clearPreviewUrlCacheTask: ClearPreviewUrlCacheTask,
+ private val getPreviewUrlTask: GetPreviewUrlTask,
+ private val getRawPreviewUrlTask: GetRawPreviewUrlTask,
+ private val urlsExtractor: UrlsExtractor
+) : MediaService {
+ // Cache of extracted URLs
+ private val extractedUrlsCache = LruCache>(1_000)
+
+ override fun extractUrls(event: Event): List {
+ return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) }
+ }
+
+ private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}"
+
+ 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() {
+ extractedUrlsCache.evictAll()
+ clearPreviewUrlCacheTask.execute(Unit)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt
similarity index 86%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt
index 92903bf96e..fece6c06c6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt
@@ -5,7 +5,7 @@
* 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
+ * 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,
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package org.matrix.android.sdk.internal.session.homeserver
+package org.matrix.android.sdk.internal.session.media
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
-internal data class GetUploadCapabilitiesResult(
+internal data class GetMediaConfigResult(
/**
* The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content.
* If not listed or null, the size limit should be treated as unknown.
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
new file mode 100644
index 0000000000..bbb4f1e06a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.Call
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+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")
+ fun getMediaConfig(): Call
+
+ /**
+ * 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")
+ 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
new file mode 100644
index 0000000000..bc58b3f444
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt
@@ -0,0 +1,50 @@
+/*
+ * 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 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
+
+@Module
+internal abstract class MediaModule {
+
+ @Module
+ companion object {
+ @Provides
+ @JvmStatic
+ @SessionScope
+ fun providesMediaAPI(retrofit: Retrofit): MediaAPI {
+ return retrofit.create(MediaAPI::class.java)
+ }
+ }
+
+ @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
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt
new file mode 100644
index 0000000000..9d374c3428
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt
@@ -0,0 +1,44 @@
+/*
+ * 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 android.util.Patterns
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageType
+import javax.inject.Inject
+
+internal class UrlsExtractor @Inject constructor() {
+ // Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later
+ private val urlRegex = Patterns.WEB_URL.toRegex()
+
+ fun extract(event: Event): List {
+ return event.takeIf { it.getClearType() == EventType.MESSAGE }
+ ?.getClearContent()
+ ?.toModel()
+ ?.takeIf { it.msgType == MessageType.MSGTYPE_TEXT || it.msgType == MessageType.MSGTYPE_EMOTE }
+ ?.body
+ ?.let { urlRegex.findAll(it) }
+ ?.map { it.value }
+ ?.filter { it.startsWith("https://") || it.startsWith("http://") }
+ ?.distinct()
+ ?.toList()
+ .orEmpty()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
index 5265e4f17d..500d43408e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.UserThreePidEntity
@@ -80,7 +81,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback): Cancelable {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) {
- val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg")
+ val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg)
setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
userStore.updateAvatar(userId, response.contentUri)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
index 79ff9db087..fb840b4eb3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
@@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
@@ -96,7 +97,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
fileUploader.uploadFromUri(
uri = avatarUri,
filename = UUID.randomUUID().toString(),
- mimeType = "image/jpeg")
+ mimeType = MimeTypes.Jpeg)
}
?.let { response ->
Event(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 5a71ff7b76..8828f3dfed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -177,7 +177,7 @@ internal class DefaultSendService @AssistedInject constructor(
val attachmentData = ContentAttachmentData(
size = messageContent.info!!.size,
mimeType = messageContent.info.mimeType!!,
- name = messageContent.body,
+ name = messageContent.getFileName(),
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.FILE
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
index 6015d945c4..b546584450 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
@@ -20,7 +20,6 @@ import android.net.Uri
import androidx.lifecycle.LiveData
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
-import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
@@ -32,22 +31,15 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.state.StateService
-import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.task.configureWith
-import org.matrix.android.sdk.internal.task.launchToCallback
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
-import org.matrix.android.sdk.internal.util.awaitCallback
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
private val stateEventDataSource: StateEventDataSource,
- private val taskExecutor: TaskExecutor,
private val sendStateTask: SendStateTask,
- private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val fileUploader: FileUploader,
private val addRoomAliasTask: AddRoomAliasTask
) : StateService {
@@ -73,45 +65,38 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey)
}
- override fun sendStateEvent(
+ override suspend fun sendStateEvent(
eventType: String,
stateKey: String?,
- body: JsonDict,
- callback: MatrixCallback
- ): Cancelable {
+ body: JsonDict
+ ) {
val params = SendStateTask.Params(
roomId = roomId,
stateKey = stateKey,
eventType = eventType,
body = body
)
- return sendStateTask
- .configureWith(params) {
- this.callback = callback
- }
- .executeBy(taskExecutor)
+ sendStateTask.execute(params)
}
- override fun updateTopic(topic: String, callback: MatrixCallback): Cancelable {
- return sendStateEvent(
+ override suspend fun updateTopic(topic: String) {
+ sendStateEvent(
eventType = EventType.STATE_ROOM_TOPIC,
body = mapOf("topic" to topic),
- callback = callback,
stateKey = null
)
}
- override fun updateName(name: String, callback: MatrixCallback): Cancelable {
- return sendStateEvent(
+ override suspend fun updateName(name: String) {
+ sendStateEvent(
eventType = EventType.STATE_ROOM_NAME,
body = mapOf("name" to name),
- callback = callback,
stateKey = null
)
}
- override fun updateCanonicalAlias(alias: String?, altAliases: List, callback: MatrixCallback): Cancelable {
- return sendStateEvent(
+ override suspend fun updateCanonicalAlias(alias: String?, altAliases: List) {
+ sendStateEvent(
eventType = EventType.STATE_ROOM_CANONICAL_ALIAS,
body = RoomCanonicalAliasContent(
canonicalAlias = alias,
@@ -123,64 +108,48 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
// Sort for the cleanup
.sorted()
).toContent(),
- callback = callback,
stateKey = null
)
}
- override fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable {
- return sendStateEvent(
+ override suspend fun updateHistoryReadability(readability: RoomHistoryVisibility) {
+ sendStateEvent(
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
body = mapOf("history_visibility" to readability),
- callback = callback,
stateKey = null
)
}
- override fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, callback: MatrixCallback): Cancelable {
- return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
- if (joinRules != null) {
- awaitCallback {
- sendStateEvent(
- eventType = EventType.STATE_ROOM_JOIN_RULES,
- body = RoomJoinRulesContent(joinRules).toContent(),
- callback = it,
- stateKey = null
- )
- }
- }
- if (guestAccess != null) {
- awaitCallback {
- sendStateEvent(
- eventType = EventType.STATE_ROOM_GUEST_ACCESS,
- body = RoomGuestAccessContent(guestAccess).toContent(),
- callback = it,
- stateKey = null
- )
- }
- }
+ override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) {
+ if (joinRules != null) {
+ sendStateEvent(
+ eventType = EventType.STATE_ROOM_JOIN_RULES,
+ body = RoomJoinRulesContent(joinRules).toContent(),
+ stateKey = null
+ )
+ }
+ if (guestAccess != null) {
+ sendStateEvent(
+ eventType = EventType.STATE_ROOM_GUEST_ACCESS,
+ body = RoomGuestAccessContent(guestAccess).toContent(),
+ stateKey = null
+ )
}
}
- override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable {
- return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
- val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg")
- awaitCallback {
- sendStateEvent(
- eventType = EventType.STATE_ROOM_AVATAR,
- body = mapOf("url" to response.contentUri),
- callback = it,
- stateKey = null
- )
- }
- }
+ override suspend fun updateAvatar(avatarUri: Uri, fileName: String) {
+ val response = fileUploader.uploadFromUri(avatarUri, fileName, MimeTypes.Jpeg)
+ sendStateEvent(
+ eventType = EventType.STATE_ROOM_AVATAR,
+ body = mapOf("url" to response.contentUri),
+ stateKey = null
+ )
}
- override fun deleteAvatar(callback: MatrixCallback): Cancelable {
- return sendStateEvent(
+ override suspend fun deleteAvatar() {
+ sendStateEvent(
eventType = EventType.STATE_ROOM_AVATAR,
body = emptyMap(),
- callback = callback,
stateKey = null
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt
similarity index 100%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
similarity index 100%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
index 74cba5e796..424c24663c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
@@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.failure.isTokenError
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
import org.matrix.android.sdk.internal.session.sync.SyncTask
-import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.android.sdk.internal.util.Debouncer
import org.matrix.android.sdk.internal.util.createUIHandler
@@ -50,14 +49,13 @@ private const val RETRY_WAIT_TIME_MS = 10_000L
private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
- private val typingUsersTracker: DefaultTypingUsersTracker,
private val networkConnectivityChecker: NetworkConnectivityChecker,
private val backgroundDetectionObserver: BackgroundDetectionObserver,
private val activeCallHandler: ActiveCallHandler
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.Idle
- private var liveState = MutableLiveData(state)
+ private var liveState = MutableLiveData(state)
private val lock = Object()
private val syncScope = CoroutineScope(SupervisorJob())
private val debouncer = Debouncer(createUIHandler())
@@ -231,7 +229,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
return
}
state = newState
- debouncer.debounce("post_state", Runnable {
+ debouncer.debounce("post_state", {
liveState.value = newState
}, 150)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt
index 4dc54d3b19..fb5e3a5774 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt
@@ -25,6 +25,9 @@ import java.io.InputStream
*/
@WorkerThread
fun writeToFile(inputStream: InputStream, outputFile: File) {
+ // Ensure the parent folder exists, else it will crash
+ outputFile.parentFile?.mkdirs()
+
outputFile.outputStream().use {
inputStream.copyTo(it)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt
new file mode 100644
index 0000000000..0998601db6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.util
+
+import androidx.collection.LruCache
+
+@Suppress("NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER")
+internal inline fun LruCache.getOrPut(key: K, defaultValue: () -> V): V {
+ return get(key) ?: defaultValue().also { put(key, it) }
+}
diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt
index 71bd3ccc05..9a7cf1eb76 100644
--- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt
+++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt
@@ -28,7 +28,6 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.media.ImageContentRenderer
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.MatrixCallback
-import org.matrix.android.sdk.api.session.file.FileService
import timber.log.Timber
import java.io.File
import java.io.IOException
@@ -110,11 +109,9 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
}
// Use the file vector service, will avoid flickering and redownload after upload
fileService.downloadFile(
- downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
- mimeType = data.mimeType,
- id = data.eventId,
- url = data.url,
fileName = data.filename,
+ mimeType = data.mimeType,
+ url = data.url,
elementToDecrypt = data.elementToDecrypt,
callback = object : MatrixCallback {
override fun onSuccess(data: File) {
diff --git a/vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt b/vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt
index 7ab2271c57..f14c9b834d 100644
--- a/vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt
+++ b/vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt
@@ -20,17 +20,11 @@ import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import im.vector.app.core.utils.getFileExtension
+import org.matrix.android.sdk.api.util.MimeTypes
+import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
import timber.log.Timber
import java.io.InputStream
-/**
- * Mime types
- */
-const val MIME_TYPE_JPEG = "image/jpeg"
-const val MIME_TYPE_JPG = "image/jpg"
-const val MIME_TYPE_IMAGE_ALL = "image/*"
-const val MIME_TYPE_ALL_CONTENT = "*/*"
-
data class Resource(
var mContentStream: InputStream? = null,
var mMimeType: String? = null
@@ -55,7 +49,7 @@ data class Resource(
* @return true if the opened resource is a jpeg one.
*/
fun isJpegResource(): Boolean {
- return MIME_TYPE_JPEG == mMimeType || MIME_TYPE_JPG == mMimeType
+ return mMimeType.normalizeMimeType() == MimeTypes.Jpeg
}
}
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt
index 169f24520b..3c48637e74 100644
--- a/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt
+++ b/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt
@@ -1,19 +1,17 @@
/*
-
- * Copyright 2019 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.
-
+ * Copyright 2019 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.ui.views
diff --git a/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt
index a5e0005c2a..bb38150797 100644
--- a/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt
@@ -1,19 +1,17 @@
/*
-
- * Copyright 2019 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.
-
+ * Copyright 2019 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.utils
diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
index 4c6aa51348..45db8ea91d 100644
--- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
@@ -48,6 +48,10 @@ import okio.buffer
import okio.sink
import okio.source
import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.util.MimeTypes
+import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeAudio
+import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage
+import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeVideo
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
@@ -138,7 +142,7 @@ fun openFileSelection(activity: Activity,
fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultipleSelection)
fileIntent.addCategory(Intent.CATEGORY_OPENABLE)
- fileIntent.type = "*/*"
+ fileIntent.type = MimeTypes.Any
try {
activityResultLauncher
@@ -182,7 +186,7 @@ fun openCamera(activity: Activity, titlePrefix: String, requestCode: Int): Strin
// The Galaxy S not only requires the name of the file to output the image to, but will also not
// set the mime type of the picture it just took (!!!). We assume that the Galaxy S takes image/jpegs
// so the attachment uploader doesn't freak out about there being no mimetype in the content database.
- values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
+ values.put(MediaStore.Images.Media.MIME_TYPE, MimeTypes.Jpeg)
var dummyUri: Uri? = null
try {
dummyUri = activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
@@ -344,10 +348,10 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
}
val externalContentUri = when {
- mediaMimeType?.startsWith("image/") == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- mediaMimeType?.startsWith("video/") == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
- mediaMimeType?.startsWith("audio/") == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
- else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI
+ mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI
}
val uri = context.contentResolver.insert(externalContentUri, values)
@@ -365,7 +369,7 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String
notificationUtils.buildDownloadFileNotification(
uri,
filename,
- mediaMimeType ?: "application/octet-stream"
+ mediaMimeType ?: MimeTypes.OctetStream
).let { notification ->
notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification)
}
@@ -385,10 +389,10 @@ private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: Str
GlobalScope.launch(Dispatchers.IO) {
val dest = when {
- mediaMimeType?.startsWith("image/") == true -> Environment.DIRECTORY_PICTURES
- mediaMimeType?.startsWith("video/") == true -> Environment.DIRECTORY_MOVIES
- mediaMimeType?.startsWith("audio/") == true -> Environment.DIRECTORY_MUSIC
- else -> Environment.DIRECTORY_DOWNLOADS
+ mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES
+ mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES
+ mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC
+ else -> Environment.DIRECTORY_DOWNLOADS
}
val downloadDir = Environment.getExternalStoragePublicDirectory(dest)
try {
@@ -405,7 +409,7 @@ private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: Str
savedFile.name,
title,
true,
- mediaMimeType ?: "application/octet-stream",
+ mediaMimeType ?: MimeTypes.OctetStream,
savedFile.absolutePath,
savedFile.length(),
true)
diff --git a/vector/src/main/java/im/vector/app/core/utils/Handler.kt b/vector/src/main/java/im/vector/app/core/utils/Handler.kt
index c7ec97f53e..fe8760a522 100644
--- a/vector/src/main/java/im/vector/app/core/utils/Handler.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/Handler.kt
@@ -1,19 +1,17 @@
/*
-
- * Copyright 2019 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.
-
+ * Copyright 2019 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.utils
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt
index 9c9d8f8017..4e8dcaacb7 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt
@@ -23,6 +23,9 @@ import im.vector.lib.multipicker.entity.MultiPickerFileType
import im.vector.lib.multipicker.entity.MultiPickerImageType
import im.vector.lib.multipicker.entity.MultiPickerVideoType
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
+import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeAudio
+import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage
+import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeVideo
import timber.log.Timber
fun MultiPickerContactType.toContactAttachment(): ContactAttachment {
@@ -59,10 +62,10 @@ fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData {
private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type {
return when {
- mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
- mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
- mimeType?.startsWith("audio/") == true -> ContentAttachmentData.Type.AUDIO
- else -> ContentAttachmentData.Type.FILE
+ mimeType?.isMimeTypeImage() == true -> ContentAttachmentData.Type.IMAGE
+ mimeType?.isMimeTypeVideo() == true -> ContentAttachmentData.Type.VIDEO
+ mimeType?.isMimeTypeAudio() == true -> ContentAttachmentData.Type.AUDIO
+ else -> ContentAttachmentData.Type.FILE
}
}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt
index bd13c0dac4..e35ab96365 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt
@@ -17,11 +17,19 @@
package im.vector.app.features.attachments
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
+import org.matrix.android.sdk.api.util.MimeTypes
+
+private val listOfPreviewableMimeTypes = listOf(
+ MimeTypes.Jpeg,
+ MimeTypes.BadJpg,
+ MimeTypes.Png,
+ MimeTypes.Gif
+)
fun ContentAttachmentData.isPreviewable(): Boolean {
// For now the preview only supports still image
return type == ContentAttachmentData.Type.IMAGE
- && listOf("image/jpeg", "image/png", "image/jpg").contains(getSafeMimeType() ?: "")
+ && listOfPreviewableMimeTypes.contains(getSafeMimeType() ?: "")
}
data class GroupedContentAttachmentData(
diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt
index bd06f8cf0b..853f9f8997 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt
@@ -17,12 +17,14 @@
package im.vector.app.features.attachments.preview
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
+import org.matrix.android.sdk.api.util.MimeTypes
+import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage
/**
* All images are editable, expect Gif
*/
fun ContentAttachmentData.isEditable(): Boolean {
return type == ContentAttachmentData.Type.IMAGE
- && getSafeMimeType()?.startsWith("image/") == true
- && getSafeMimeType() != "image/gif"
+ && getSafeMimeType()?.isMimeTypeImage() == true
+ && getSafeMimeType() != MimeTypes.Gif
}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index 680ec17415..90d128320b 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -71,12 +71,18 @@ class HomeActivityViewModel @AssistedInject constructor(
private var onceTrusted = false
init {
+ cleanupFiles()
observeInitialSync()
mayBeInitializeCrossSigning()
checkSessionPushIsOn()
observeCrossSigningReset()
}
+ private fun cleanupFiles() {
+ // Mitigation: delete all cached decrypted files each time the application is started.
+ activeSessionHolder.getSafeActiveSession()?.fileService()?.clearDecryptedCache()
+ }
+
private fun observeCrossSigningReset() {
val safeActiveSession = activeSessionHolder.getSafeActiveSession() ?: return
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
index 8891218a11..e034e373f3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
@@ -98,4 +98,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction()
object QuickActionSetTopic : RoomDetailAction()
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction()
+
+ // Preview URL
+ data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
index bc0f5ce6dc..aff29bb7a3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
@@ -140,6 +140,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
+import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillImageSpan
@@ -165,7 +166,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.*
-import kotlinx.android.synthetic.main.merge_composer_layout.view.*
+import kotlinx.android.synthetic.main.composer_layout.view.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser
@@ -174,7 +175,6 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@@ -185,7 +185,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
-import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@@ -194,7 +193,6 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
-import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import timber.log.Timber
@@ -1103,18 +1101,6 @@ class RoomDetailFragment @Inject constructor(
}
}
- private val writingFileActivityResultLauncher = registerForPermissionsResult { allGranted ->
- if (allGranted) {
- val pendingUri = roomDetailViewModel.pendingUri
- if (pendingUri != null) {
- roomDetailViewModel.pendingUri = null
- sendUri(pendingUri)
- }
- } else {
- cleanUpAfterPermissionNotGranted()
- }
- }
-
private fun setupComposer() {
val composerEditText = composerLayout.composerEditText
autoCompleter.setup(composerEditText)
@@ -1160,14 +1146,7 @@ class RoomDetailFragment @Inject constructor(
}
override fun onRichContentSelected(contentUri: Uri): Boolean {
- // We need WRITE_EXTERNAL permission
- return if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, requireActivity(), writingFileActivityResultLauncher)) {
- sendUri(contentUri)
- } else {
- roomDetailViewModel.pendingUri = contentUri
- // Always intercept when we request some permission
- true
- }
+ return sendUri(contentUri)
}
}
}
@@ -1198,11 +1177,9 @@ class RoomDetailFragment @Inject constructor(
}
private fun sendUri(uri: Uri): Boolean {
- roomDetailViewModel.preventAttachmentPreview = true
val shareIntent = Intent(Intent.ACTION_SEND, uri)
val isHandled = attachmentsHelper.handleShareIntent(requireContext(), shareIntent)
if (!isHandled) {
- roomDetailViewModel.preventAttachmentPreview = false
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
}
return isHandled
@@ -1564,7 +1541,6 @@ class RoomDetailFragment @Inject constructor(
private fun cleanUpAfterPermissionNotGranted() {
// Reset all pending data
roomDetailViewModel.pendingAction = null
- roomDetailViewModel.pendingUri = null
attachmentsHelper.pendingType = null
}
@@ -1640,6 +1616,10 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(itemAction)
}
+ override fun getPreviewUrlRetriever(): PreviewUrlRetriever {
+ return roomDetailViewModel.previewUrlRetriever
+ }
+
override fun onRoomCreateLinkClicked(url: String) {
permalinkHandler
.launch(requireContext(), url, object : NavigationInterceptor {
@@ -1662,17 +1642,20 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
}
+ override fun onPreviewUrlClicked(url: String) {
+ onUrlClicked(url, url)
+ }
+
+ override fun onPreviewUrlCloseClicked(eventId: String, url: String) {
+ roomDetailViewModel.handle(RoomDetailAction.DoNotShowPreviewUrlFor(eventId, url))
+ }
+
private fun onShareActionClicked(action: EventSharedAction.Share) {
if (action.messageContent is MessageTextContent) {
shareText(requireContext(), action.messageContent.body)
} else if (action.messageContent is MessageWithAttachmentContent) {
session.fileService().downloadFile(
- downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- id = action.eventId,
- fileName = action.messageContent.body,
- mimeType = action.messageContent.mimeType,
- url = action.messageContent.getFileUrl(),
- elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
+ messageContent = action.messageContent,
callback = object : MatrixCallback {
override fun onSuccess(data: File) {
if (isAdded) {
@@ -1702,12 +1685,7 @@ class RoomDetailFragment @Inject constructor(
return
}
session.fileService().downloadFile(
- downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- id = action.eventId,
- fileName = action.messageContent.body,
- mimeType = action.messageContent.mimeType,
- url = action.messageContent.getFileUrl(),
- elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
+ messageContent = action.messageContent,
callback = object : MatrixCallback {
override fun onSuccess(data: File) {
if (isAdded) {
@@ -1969,24 +1947,18 @@ class RoomDetailFragment @Inject constructor(
// AttachmentsHelper.Callback
override fun onContentAttachmentsReady(attachments: List) {
- if (roomDetailViewModel.preventAttachmentPreview) {
- roomDetailViewModel.preventAttachmentPreview = false
- roomDetailViewModel.handle(RoomDetailAction.SendMedia(attachments, false))
- } else {
- val grouped = attachments.toGroupedContentAttachmentData()
- if (grouped.notPreviewables.isNotEmpty()) {
- // Send the not previewable attachments right now (?)
- roomDetailViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false))
- }
- if (grouped.previewables.isNotEmpty()) {
- val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables))
- contentAttachmentActivityResultLauncher.launch(intent)
- }
+ val grouped = attachments.toGroupedContentAttachmentData()
+ if (grouped.notPreviewables.isNotEmpty()) {
+ // Send the not previewable attachments right now (?)
+ roomDetailViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false))
+ }
+ if (grouped.previewables.isNotEmpty()) {
+ val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables))
+ contentAttachmentActivityResultLauncher.launch(intent)
}
}
override fun onAttachmentsProcessFailed() {
- roomDetailViewModel.preventAttachmentPreview = false
Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
index a83dddc9ac..21858438b9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
@@ -40,6 +40,7 @@ import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
+import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import im.vector.app.features.raw.wellknown.getElementWellknown
@@ -69,7 +70,6 @@ import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
@@ -80,7 +80,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
-import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
@@ -92,7 +91,6 @@ import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.toOptional
-import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import org.matrix.android.sdk.internal.util.awaitCallback
@@ -128,15 +126,12 @@ class RoomDetailViewModel @AssistedInject constructor(
private var timelineEvents = PublishRelay.create>()
val timeline = room.createTimeline(eventId, timelineSettings)
+ // Same lifecycle than the ViewModel (survive to screen rotation)
+ val previewUrlRetriever = PreviewUrlRetriever(session)
+
// Slot to keep a pending action during permission request
var pendingAction: RoomDetailAction? = null
- // Slot to keep a pending uri during permission request
- var pendingUri: Uri? = null
-
- // Slot to store if we want to prevent preview of attachment
- var preventAttachmentPreview = false
-
private var trackUnreadMessages = AtomicBoolean(false)
private var mostRecentDisplayedEvent: TimelineEvent? = null
@@ -286,15 +281,18 @@ class RoomDetailViewModel @AssistedInject constructor(
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
)
}
+ is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
}.exhaustive
}
+ private fun handleDoNotShowPreviewUrlFor(action: RoomDetailAction.DoNotShowPreviewUrlFor) {
+ previewUrlRetriever.doNotShowPreviewUrlFor(action.eventId, action.url)
+ }
+
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
viewModelScope.launch(Dispatchers.IO) {
try {
- awaitCallback {
- room.updateAvatar(action.newAvatarUri, action.newAvatarFileName, it)
- }
+ room.updateAvatar(action.newAvatarUri, action.newAvatarFileName)
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
@@ -854,8 +852,8 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
- launchSlashCommandFlow {
- room.updateTopic(changeTopic.topic, it)
+ launchSlashCommandFlowSuspendable {
+ room.updateTopic(changeTopic.topic)
}
}
@@ -876,9 +874,9 @@ class RoomDetailViewModel @AssistedInject constructor(
?.content
?.toModel() ?: return
- launchSlashCommandFlow {
+ launchSlashCommandFlowSuspendable {
currentPowerLevelsContent.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel)
- room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent(), it)
+ room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent())
}
}
@@ -920,6 +918,19 @@ class RoomDetailViewModel @AssistedInject constructor(
lambda.invoke(matrixCallback)
}
+ private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) {
+ _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
+ viewModelScope.launch {
+ val event = try {
+ block()
+ RoomDetailViewEvents.SlashCommandResultOk
+ } catch (failure: Exception) {
+ RoomDetailViewEvents.SlashCommandResultError(failure)
+ }
+ _viewEvents.post(event)
+ }
+ }
+
private fun handleSendReaction(action: RoomDetailAction.SendReaction) {
room.sendReaction(action.targetEventId, action.reaction)
}
@@ -1010,10 +1021,10 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
- val mxcUrl = action.messageFileContent.getFileUrl()
+ val mxcUrl = action.messageFileContent.getFileUrl() ?: return
val isLocalSendingFile = action.senderId == session.myUserId
- && mxcUrl?.startsWith("content://") ?: false
- val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false
+ && mxcUrl.startsWith("content://")
+ val isDownloaded = session.fileService().isFileInCache(action.messageFileContent)
if (isLocalSendingFile) {
tryOrNull { Uri.parse(mxcUrl) }?.let {
_viewEvents.post(RoomDetailViewEvents.OpenFile(
@@ -1024,7 +1035,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
} else if (isDownloaded) {
// we can open it
- session.fileService().getTemporarySharableURI(mxcUrl!!, action.messageFileContent.mimeType)?.let { uri ->
+ session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri ->
_viewEvents.post(RoomDetailViewEvents.OpenFile(
action.messageFileContent.mimeType,
uri,
@@ -1033,12 +1044,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
} else {
session.fileService().downloadFile(
- downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
- id = action.eventId,
- fileName = action.messageFileContent.getFileName(),
- mimeType = action.messageFileContent.mimeType,
- url = mxcUrl,
- elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
+ messageContent = action.messageFileContent,
callback = object : MatrixCallback {
override fun onSuccess(data: File) {
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
@@ -1350,6 +1356,17 @@ class RoomDetailViewModel @AssistedInject constructor(
override fun onTimelineUpdated(snapshot: List) {
timelineEvents.accept(snapshot)
+
+ // PreviewUrl
+ if (vectorPreferences.showUrlPreviews()) {
+ withState { state ->
+ snapshot
+ .takeIf { state.asyncRoomSummary.invoke()?.isEncrypted == false }
+ ?.forEach {
+ previewUrlRetriever.getPreviewUrl(it.root, viewModelScope)
+ }
+ }
+ }
}
override fun onTimelineFailure(throwable: Throwable) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt
index af0e1a91f0..f232e9a65e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt
@@ -36,7 +36,7 @@ import androidx.transition.TransitionSet
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.app.R
-import kotlinx.android.synthetic.main.merge_composer_layout.view.*
+import kotlinx.android.synthetic.main.composer_layout.view.*
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
/**
@@ -86,7 +86,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
get() = composerEditText.text
init {
- inflate(context, R.layout.merge_composer_layout, this)
+ inflate(context, R.layout.composer_layout, this)
ButterKnife.bind(this)
collapse(false)
composerEditText.callback = object : ComposerEditText.Callback {
@@ -110,20 +110,20 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
}
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
- if (currentConstraintSetId == R.layout.constraint_set_composer_layout_compact) {
+ if (currentConstraintSetId == R.layout.composer_layout_constraint_set_compact) {
// ignore we good
return
}
- currentConstraintSetId = R.layout.constraint_set_composer_layout_compact
+ currentConstraintSetId = R.layout.composer_layout_constraint_set_compact
applyNewConstraintSet(animate, transitionComplete)
}
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
- if (currentConstraintSetId == R.layout.constraint_set_composer_layout_expanded) {
+ if (currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded) {
// ignore we good
return
}
- currentConstraintSetId = R.layout.constraint_set_composer_layout_expanded
+ currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded
applyNewConstraintSet(animate, transitionComplete)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index bddc7fa126..ba3ffe3174 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -48,6 +48,7 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
+import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
import im.vector.app.features.settings.VectorPreferences
@@ -76,7 +77,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val backgroundHandler: Handler
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
- interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback {
+ interface Callback :
+ BaseCallback,
+ ReactionPillCallback,
+ AvatarCallback,
+ UrlClickCallback,
+ ReadReceiptsCallback,
+ PreviewUrlCallback {
fun onLoadMore(direction: Timeline.Direction)
fun onEventInvisible(event: TimelineEvent)
fun onEventVisible(event: TimelineEvent)
@@ -91,6 +98,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// TODO move all callbacks to this?
fun onTimelineItemAction(itemAction: RoomDetailAction)
+
+ fun getPreviewUrlRetriever(): PreviewUrlRetriever
}
interface ReactionPillCallback {
@@ -118,6 +127,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onUrlLongClicked(url: String): Boolean
}
+ interface PreviewUrlCallback {
+ fun onPreviewUrlClicked(url: String)
+ fun onPreviewUrlCloseClicked(eventId: String, url: String)
+ }
+
// Map eventId to adapter position
private val adapterPositionMapping = HashMap()
private val modelCache = arrayListOf()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
index f77e39c245..e88c1f3797 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt
@@ -82,10 +82,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
when (cryptoError) {
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
span {
- apply {
- drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
- image(it, "baseline")
- }
+ drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
+ image(it, "baseline")
+ +" "
}
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) {
textStyle = "italic"
@@ -95,10 +94,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
}
else -> {
span {
- apply {
- drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
- image(it, "baseline")
- }
+ drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
+ image(it, "baseline")
+ +" "
}
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) {
textStyle = "italic"
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 2b067ccf3f..27696f5b28 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -84,9 +84,11 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL
+import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
+import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import javax.inject.Inject
@@ -144,16 +146,16 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent()
// val ev = all.toModel()
return when (messageContent) {
- is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
- is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
- is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
+ is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
+ is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
+ is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
+ is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
}
@@ -164,7 +166,7 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
return when (messageContent.optionType) {
- OPTION_TYPE_POLL -> {
+ OPTION_TYPE_POLL -> {
MessagePollItem_()
.attributes(attributes)
.callback(callback)
@@ -204,7 +206,12 @@ class MessageItemFactory @Inject constructor(
return MessageFileItem_()
.attributes(attributes)
.izLocalFile(fileUrl.isLocalFile())
- .izDownloaded(session.fileService().isFileInCache(fileUrl, messageContent.mimeType))
+ .izDownloaded(session.fileService().isFileInCache(
+ fileUrl,
+ messageContent.getFileName(),
+ messageContent.mimeType,
+ messageContent.encryptedFileInfo?.toElementToDecrypt())
+ )
.mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
@@ -264,7 +271,7 @@ class MessageItemFactory @Inject constructor(
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.izLocalFile(messageContent.getFileUrl().isLocalFile())
- .izDownloaded(session.fileService().isFileInCache(mxcUrl, messageContent.mimeType))
+ .izDownloaded(session.fileService().isFileInCache(messageContent))
.mxcUrl(mxcUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
@@ -305,7 +312,7 @@ class MessageItemFactory @Inject constructor(
.leftGuideline(avatarSizeProvider.leftGuideline)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
- .playable(messageContent.info?.mimeType == "image/gif")
+ .playable(messageContent.info?.mimeType == MimeTypes.Gif)
.highlighted(highlight)
.mediaData(data)
.apply {
@@ -371,7 +378,7 @@ class MessageItemFactory @Inject constructor(
val codeVisitor = CodeVisitor()
codeVisitor.visit(localFormattedBody)
when (codeVisitor.codeKind) {
- CodeVisitor.Kind.BLOCK -> {
+ CodeVisitor.Kind.BLOCK -> {
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
if (codeFormattedBlock == null) {
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
@@ -387,7 +394,7 @@ class MessageItemFactory @Inject constructor(
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
}
}
- CodeVisitor.Kind.NONE -> {
+ CodeVisitor.Kind.NONE -> {
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
}
}
@@ -424,6 +431,9 @@ class MessageItemFactory @Inject constructor(
}
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
.searchForPills(isFormatted)
+ .previewUrlRetriever(callback?.getPreviewUrlRetriever())
+ .imageContentRenderer(imageContentRenderer)
+ .previewUrlCallback(callback)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
@@ -529,6 +539,9 @@ class MessageItemFactory @Inject constructor(
}
}
.leftGuideline(avatarSizeProvider.leftGuideline)
+ .previewUrlRetriever(callback?.getPreviewUrlRetriever())
+ .imageContentRenderer(imageContentRenderer)
+ .previewUrlCallback(callback)
.attributes(attributes)
.highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback))
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index f7a1a18d9f..8a8bf364e1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -1,19 +1,17 @@
/*
-
- * Copyright 2019 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.
-
+ * Copyright 2019 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.features.home.room.detail.timeline.helper
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
index 3297f14622..c120fa671c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
@@ -1,19 +1,17 @@
/*
-
- * Copyright 2019 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.
-
+ * Copyright 2019 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.features.home.room.detail.timeline.helper
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
index feba62dea3..66d9808d2b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
@@ -23,7 +23,12 @@ import androidx.core.widget.TextViewCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
+import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
+import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
+import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlUiState
+import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
+import im.vector.app.features.media.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageTextItem : AbsMessageItem() {
@@ -37,10 +42,27 @@ abstract class MessageTextItem : AbsMessageItem() {
@EpoxyAttribute
var useBigFont: Boolean = false
+ @EpoxyAttribute
+ var previewUrlRetriever: PreviewUrlRetriever? = null
+
+ @EpoxyAttribute
+ var previewUrlCallback: TimelineEventController.PreviewUrlCallback? = null
+
+ @EpoxyAttribute
+ var imageContentRenderer: ImageContentRenderer? = null
+
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var movementMethod: MovementMethod? = null
+ private val previewUrlViewUpdater = PreviewUrlViewUpdater()
+
override fun bind(holder: Holder) {
+ // Preview URL
+ previewUrlViewUpdater.previewUrlView = holder.previewUrlView
+ previewUrlViewUpdater.imageContentRenderer = imageContentRenderer
+ previewUrlRetriever?.addListener(attributes.informationData.eventId, previewUrlViewUpdater)
+ holder.previewUrlView.delegate = previewUrlCallback
+
if (useBigFont) {
holder.messageView.textSize = 44F
} else {
@@ -65,12 +87,29 @@ abstract class MessageTextItem : AbsMessageItem() {
holder.messageView.setTextFuture(textFuture)
}
+ override fun unbind(holder: Holder) {
+ super.unbind(holder)
+ previewUrlViewUpdater.previewUrlView = null
+ previewUrlViewUpdater.imageContentRenderer = null
+ previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater)
+ }
+
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val messageView by bind(R.id.messageTextView)
+ val previewUrlView by bind(R.id.messageUrlPreview)
}
+ inner class PreviewUrlViewUpdater : PreviewUrlRetriever.PreviewUrlRetrieverListener {
+ var previewUrlView: PreviewUrlView? = null
+ var imageContentRenderer: ImageContentRenderer? = null
+
+ override fun onStateUpdated(state: PreviewUrlUiState) {
+ val safeImageContentRenderer = imageContentRenderer ?: return
+ previewUrlView?.render(state, safeImageContentRenderer)
+ }
+ }
companion object {
private const val STUB_ID = R.id.messageContentTextStub
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt
new file mode 100644
index 0000000000..695661feeb
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.features.home.room.detail.timeline.url
+
+import im.vector.app.BuildConfig
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.cache.CacheStrategy
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.events.model.Event
+
+class PreviewUrlRetriever(session: Session) {
+ private val mediaService = session.mediaService()
+
+ private val data = mutableMapOf()
+ private val listeners = mutableMapOf>()
+
+ // In memory list
+ private val blockedUrl = mutableSetOf()
+
+ fun getPreviewUrl(event: Event, coroutineScope: CoroutineScope) {
+ val eventId = event.eventId ?: return
+
+ synchronized(data) {
+ if (data[eventId] == null) {
+ // Keep only the first URL for the moment
+ val url = mediaService.extractUrls(event)
+ .firstOrNull()
+ ?.takeIf { it !in blockedUrl }
+ if (url == null) {
+ updateState(eventId, PreviewUrlUiState.NoUrl)
+ } else {
+ updateState(eventId, PreviewUrlUiState.Loading)
+ }
+ url
+ } else {
+ // Already handled
+ null
+ }
+ }?.let { urlToRetrieve ->
+ coroutineScope.launch {
+ runCatching {
+ mediaService.getPreviewUrl(
+ url = urlToRetrieve,
+ timestamp = null,
+ cacheStrategy = if (BuildConfig.DEBUG) CacheStrategy.NoCache else CacheStrategy.TtlCache(CACHE_VALIDITY, false)
+ )
+ }.fold(
+ {
+ synchronized(data) {
+ // Blocked after the request has been sent?
+ if (urlToRetrieve in blockedUrl) {
+ updateState(eventId, PreviewUrlUiState.NoUrl)
+ } else {
+ updateState(eventId, PreviewUrlUiState.Data(eventId, urlToRetrieve, it))
+ }
+ }
+ },
+ {
+ synchronized(data) {
+ updateState(eventId, PreviewUrlUiState.Error(it))
+ }
+ }
+ )
+ }
+ }
+ }
+
+ fun doNotShowPreviewUrlFor(eventId: String, url: String) {
+ blockedUrl.add(url)
+
+ // Notify the listener
+ synchronized(data) {
+ data[eventId]
+ ?.takeIf { it is PreviewUrlUiState.Data && it.url == url }
+ ?.let {
+ updateState(eventId, PreviewUrlUiState.NoUrl)
+ }
+ }
+ }
+
+ private fun updateState(eventId: String, state: PreviewUrlUiState) {
+ data[eventId] = state
+ // Notify the listener
+ listeners[eventId].orEmpty().forEach {
+ it.onStateUpdated(state)
+ }
+ }
+
+ // Called by the Epoxy item during binding
+ fun addListener(key: String, listener: PreviewUrlRetrieverListener) {
+ listeners.getOrPut(key) { mutableSetOf() }.add(listener)
+
+ // Give the current state if any
+ synchronized(data) {
+ listener.onStateUpdated(data[key] ?: PreviewUrlUiState.Unknown)
+ }
+ }
+
+ // Called by the Epoxy item during unbinding
+ fun removeListener(key: String, listener: PreviewUrlRetrieverListener) {
+ listeners[key]?.remove(listener)
+ }
+
+ interface PreviewUrlRetrieverListener {
+ fun onStateUpdated(state: PreviewUrlUiState)
+ }
+
+ companion object {
+ // One week in millis
+ private const val CACHE_VALIDITY: Long = 7 * 24 * 3_600 * 1_000
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt
new file mode 100644
index 0000000000..a8f8f7b0cb
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.features.home.room.detail.timeline.url
+
+import org.matrix.android.sdk.api.session.media.PreviewUrlData
+
+/**
+ * The state representing a preview url UI state for an Event
+ */
+sealed class PreviewUrlUiState {
+ // No info
+ object Unknown : PreviewUrlUiState()
+
+ // The event does not contain any URLs
+ object NoUrl : PreviewUrlUiState()
+
+ // Loading
+ object Loading : PreviewUrlUiState()
+
+ // Error
+ data class Error(val throwable: Throwable) : PreviewUrlUiState()
+
+ // PreviewUrl data
+ data class Data(val eventId: String,
+ val url: String,
+ val previewUrlData: PreviewUrlData) : PreviewUrlUiState()
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt
new file mode 100755
index 0000000000..9d8f438683
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.features.home.room.detail.timeline.url
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
+import butterknife.BindView
+import butterknife.ButterKnife
+import im.vector.app.R
+import im.vector.app.core.extensions.setTextOrHide
+import im.vector.app.features.home.room.detail.timeline.TimelineEventController
+import im.vector.app.features.media.ImageContentRenderer
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.media.PreviewUrlData
+
+/**
+ * A View to display a PreviewUrl and some other state
+ */
+class PreviewUrlView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
+
+ @BindView(R.id.url_preview_title)
+ lateinit var titleView: TextView
+
+ @BindView(R.id.url_preview_image)
+ lateinit var imageView: ImageView
+
+ @BindView(R.id.url_preview_description)
+ lateinit var descriptionView: TextView
+
+ @BindView(R.id.url_preview_site)
+ lateinit var siteView: TextView
+
+ @BindView(R.id.url_preview_close)
+ lateinit var closeView: View
+
+ var delegate: TimelineEventController.PreviewUrlCallback? = null
+
+ init {
+ setupView()
+ }
+
+ private var state: PreviewUrlUiState = PreviewUrlUiState.Unknown
+
+ /**
+ * This methods is responsible for rendering the view according to the newState
+ *
+ * @param newState the newState representing the view
+ */
+ fun render(newState: PreviewUrlUiState,
+ imageContentRenderer: ImageContentRenderer,
+ force: Boolean = false) {
+ if (newState == state && !force) {
+ return
+ }
+
+ state = newState
+
+ hideAll()
+ when (newState) {
+ PreviewUrlUiState.Unknown,
+ PreviewUrlUiState.NoUrl -> renderHidden()
+ PreviewUrlUiState.Loading -> renderLoading()
+ is PreviewUrlUiState.Error -> renderHidden()
+ is PreviewUrlUiState.Data -> renderData(newState.previewUrlData, imageContentRenderer)
+ }
+ }
+
+ override fun onClick(v: View?) {
+ when (val finalState = state) {
+ is PreviewUrlUiState.Data -> delegate?.onPreviewUrlClicked(finalState.url)
+ else -> Unit
+ }
+ }
+
+ private fun onCloseClick() {
+ when (val finalState = state) {
+ is PreviewUrlUiState.Data -> delegate?.onPreviewUrlCloseClicked(finalState.eventId, finalState.url)
+ else -> Unit
+ }
+ }
+
+ // PRIVATE METHODS ****************************************************************************************************************************************
+
+ private fun setupView() {
+ inflate(context, R.layout.url_preview, this)
+ ButterKnife.bind(this)
+
+ setOnClickListener(this)
+ closeView.setOnClickListener { onCloseClick() }
+ }
+
+ private fun renderHidden() {
+ isVisible = false
+ }
+
+ private fun renderLoading() {
+ // Just hide for the moment
+ isVisible = false
+ }
+
+ private fun renderData(previewUrlData: PreviewUrlData, imageContentRenderer: ImageContentRenderer) {
+ isVisible = true
+ titleView.setTextOrHide(previewUrlData.title)
+ imageView.isVisible = previewUrlData.mxcUrl?.let { imageContentRenderer.render(it, imageView) }.orFalse()
+ descriptionView.setTextOrHide(previewUrlData.description)
+ siteView.setTextOrHide(previewUrlData.siteName.takeIf { it != previewUrlData.title })
+ }
+
+ /**
+ * Hide all views that are not visible in all state
+ */
+ private fun hideAll() {
+ titleView.isVisible = false
+ imageView.isVisible = false
+ descriptionView.isVisible = false
+ siteView.isVisible = false
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt
index e23b905919..90b17f80d7 100644
--- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt
+++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt
@@ -153,12 +153,10 @@ abstract class BaseAttachmentProvider(
} else {
target.onVideoFileLoading(info.uid)
fileService.downloadFile(
- downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
- id = data.eventId,
- mimeType = data.mimeType,
- elementToDecrypt = data.elementToDecrypt,
fileName = data.filename,
+ mimeType = data.mimeType,
url = data.url,
+ elementToDecrypt = data.elementToDecrypt,
callback = object : MatrixCallback {
override fun onSuccess(data: File) {
target.onVideoFileReady(info.uid, data)
diff --git a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt
index 18312b4aa0..328d8f943e 100644
--- a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt
+++ b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt
@@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.util.MimeTypes
import java.io.File
class DataAttachmentRoomProvider(
@@ -38,7 +39,7 @@ class DataAttachmentRoomProvider(
return getItem(position).let {
when (it) {
is ImageContentRenderer.Data -> {
- if (it.mimeType == "image/gif") {
+ if (it.mimeType == MimeTypes.Gif) {
AttachmentInfo.AnimatedImage(
uid = it.eventId,
url = it.url ?: "",
@@ -77,11 +78,9 @@ class DataAttachmentRoomProvider(
override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
val item = getItem(position)
fileService.downloadFile(
- downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- id = item.eventId,
fileName = item.filename,
mimeType = item.mimeType,
- url = item.url ?: "",
+ url = item.url,
elementToDecrypt = item.elementToDecrypt,
callback = object : MatrixCallback {
override fun onSuccess(data: File) {
diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt
index 187c2e85c3..4670c82db1 100644
--- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt
+++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt
@@ -23,6 +23,7 @@ import android.view.View
import android.widget.ImageView
import androidx.core.view.updateLayoutParams
import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestListener
@@ -83,6 +84,19 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
STICKER
}
+ /**
+ * For url preview
+ */
+ fun render(mxcUrl: String, imageView: ImageView): Boolean {
+ val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
+ val imageUrl = contentUrlResolver.resolveFullSize(mxcUrl) ?: return false
+
+ GlideApp.with(imageView)
+ .load(imageUrl)
+ .into(imageView)
+ return true
+ }
+
/**
* For gallery
*/
@@ -129,6 +143,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
GlideApp
.with(contextView)
.load(data)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
} else {
// Clear image
val resolvedUrl = resolveUrl(data)
@@ -183,6 +198,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
GlideApp
.with(imageView)
.load(data)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
} else {
// Clear image
val resolvedUrl = resolveUrl(data)
@@ -214,20 +230,22 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
- fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest {
+ private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest {
return createGlideRequest(data, mode, GlideApp.with(imageView), size)
}
fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest {
return if (data.elementToDecrypt != null) {
// Encrypted image
- glideRequests.load(data)
+ glideRequests
+ .load(data)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
} else {
// Clear image
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) {
Mode.FULL_SIZE,
- Mode.STICKER -> resolveUrl(data)
+ Mode.STICKER -> resolveUrl(data)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
// Fallback to base url
@@ -295,7 +313,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
finalHeight = min(maxImageWidth * height / width, maxImageHeight)
finalWidth = finalHeight * width / height
}
- Mode.STICKER -> {
+ Mode.STICKER -> {
// limit on width
val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2)
finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp)
diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt
index 1e2761dde0..53c5dac9ad 100644
--- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt
+++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt
@@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import java.io.File
@@ -56,7 +57,7 @@ class RoomEventsAttachmentProvider(
allowNonMxcUrls = it.root.sendState.isSending()
)
- if (content.mimeType == "image/gif") {
+ if (content.mimeType == MimeTypes.Gif) {
AttachmentInfo.AnimatedImage(
uid = it.eventId,
url = content.url ?: "",
@@ -125,8 +126,6 @@ class RoomEventsAttachmentProvider(
as? MessageWithAttachmentContent
?: return@let
fileService.downloadFile(
- downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- id = timelineEvent.eventId,
fileName = messageContent.body,
mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(),
diff --git a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt
index f8cd09ce2f..d8eddc7331 100644
--- a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt
+++ b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt
@@ -27,7 +27,6 @@ import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.utils.isLocalFile
import kotlinx.android.parcel.Parcelize
import org.matrix.android.sdk.api.MatrixCallback
-import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import timber.log.Timber
import java.io.File
@@ -76,8 +75,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
activeSessionHolder.getActiveSession().fileService()
.downloadFile(
- downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
- id = data.eventId,
fileName = data.filename,
mimeType = data.mimeType,
url = data.url,
@@ -116,8 +113,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
activeSessionHolder.getActiveSession().fileService()
.downloadFile(
- downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
- id = data.eventId,
fileName = data.filename,
mimeType = data.mimeType,
url = data.url,
diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
index 96248187aa..7be7624a48 100755
--- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
+++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
@@ -46,6 +46,7 @@ import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
import org.matrix.android.sdk.api.Matrix
+import org.matrix.android.sdk.api.util.MimeTypes
import timber.log.Timber
import java.io.File
import java.io.IOException
@@ -274,7 +275,7 @@ class BugReporter @Inject constructor(
// add the gzipped files
for (file in gzippedFiles) {
- builder.addFormDataPart("compressed-log", file.name, file.asRequestBody("application/octet-stream".toMediaTypeOrNull()))
+ builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull()))
}
mBugReportFiles.addAll(gzippedFiles)
@@ -295,7 +296,7 @@ class BugReporter @Inject constructor(
}
builder.addFormDataPart("file",
- logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody("application/octet-stream".toMediaTypeOrNull()))
+ logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull()))
} catch (e: Exception) {
Timber.e(e, "## sendBugReport() : fail to write screenshot$e")
}
diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt
index 78562ea351..39b5884308 100644
--- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt
@@ -166,9 +166,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
viewModelScope.launch {
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
try {
- awaitCallback {
- room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent(), it)
- }
+ room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent())
_viewEvents.post(RoomMemberProfileViewEvents.OnSetPowerLevelSuccess)
} catch (failure: Throwable) {
_viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt
index 5873d9ce8a..f470eeefc2 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt
@@ -30,7 +30,6 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
@@ -301,21 +300,20 @@ class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: Roo
private fun updateCanonicalAlias(canonicalAlias: String?, alternativeAliases: List, closeForm: Boolean) {
postLoading(true)
- room.updateCanonicalAlias(canonicalAlias, alternativeAliases, object : MatrixCallback {
- override fun onSuccess(data: Unit) {
+ viewModelScope.launch {
+ try {
+ room.updateCanonicalAlias(canonicalAlias, alternativeAliases)
setState {
copy(
isLoading = false,
publishManuallyState = if (closeForm) RoomAliasViewState.AddAliasState.Closed else publishManuallyState
)
}
- }
-
- override fun onFailure(failure: Throwable) {
+ } catch (failure: Throwable) {
postLoading(false)
_viewEvents.post(RoomAliasViewEvents.Failure(failure))
}
- })
+ }
}
private fun handleAddLocalAlias() = withState { state ->
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt
index 9e402c675b..9f15e62b3b 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt
@@ -30,7 +30,6 @@ import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction
import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
@@ -197,8 +196,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
room.sendStateEvent(
eventType = EventType.STATE_ROOM_THIRD_PARTY_INVITE,
stateKey = action.stateKey,
- body = emptyMap(),
- callback = NoOpMatrixCallback()
+ body = emptyMap()
)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt
index 763eed5474..b62b633a36 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt
@@ -30,10 +30,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.model.message.MessageType
-import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
-import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
@@ -134,12 +131,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
try {
val file = awaitCallback {
session.fileService().downloadFile(
- downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- id = action.uploadEvent.eventId,
- fileName = action.uploadEvent.contentWithAttachmentContent.body,
- url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
- mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
- elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
+ messageContent = action.uploadEvent.contentWithAttachmentContent,
callback = it
)
}
@@ -155,12 +147,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
try {
val file = awaitCallback {
session.fileService().downloadFile(
- downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
- id = action.uploadEvent.eventId,
- fileName = action.uploadEvent.contentWithAttachmentContent.body,
- mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
- url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
- elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
+ messageContent = action.uploadEvent.contentWithAttachmentContent,
callback = it)
}
_viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body))
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index 9d6ed0246c..c50692df82 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -783,6 +783,15 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false)
}
+ /**
+ * Tells if the user wants to see URL previews in the timeline
+ *
+ * @return true if the user wants to see URL previews in the timeline
+ */
+ fun showUrlPreviews(): Boolean {
+ return defaultPrefs.getBoolean(SETTINGS_SHOW_URL_PREVIEW_KEY, true)
+ }
+
/**
* Enable or disable the analytics tracking.
*
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt
index a84a10f74c..841a239701 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt
@@ -22,7 +22,6 @@ import android.widget.CheckedTextView
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
-import androidx.preference.SwitchPreference
import im.vector.app.R
import im.vector.app.core.extensions.restart
import im.vector.app.core.preference.VectorListPreference
@@ -64,9 +63,9 @@ class VectorSettingsPreferencesFragment @Inject constructor(
}
// Url preview
+ /*
+ TODO Note: we keep the setting client side for now
findPreference(VectorPreferences.SETTINGS_SHOW_URL_PREVIEW_KEY)!!.let {
- /*
- TODO
it.isChecked = session.isURLPreviewEnabled
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
@@ -100,8 +99,8 @@ class VectorSettingsPreferencesFragment @Inject constructor(
false
}
- */
}
+ */
// update keep medias period
findPreference(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY)!!.let {
diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt
index a4d759250d..3906ea687c 100644
--- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt
+++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt
@@ -21,6 +21,9 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
@@ -310,12 +313,13 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
val params = HashMap()
params["status"] = status
- room.sendStateEvent(
- eventType = EventType.PLUMBING,
- stateKey = null,
- body = params,
- callback = createWidgetAPICallback(widgetPostAPIMediator, eventData)
- )
+ launchWidgetAPIAction(widgetPostAPIMediator, eventData) {
+ room.sendStateEvent(
+ eventType = EventType.PLUMBING,
+ stateKey = null,
+ body = params
+ )
+ }
}
/**
@@ -333,12 +337,14 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
Timber.d(description)
val content = eventData["content"] as JsonDict
val stateKey = "_$userId"
- room.sendStateEvent(
- eventType = EventType.BOT_OPTIONS,
- stateKey = stateKey,
- body = content,
- callback = createWidgetAPICallback(widgetPostAPIMediator, eventData)
- )
+
+ launchWidgetAPIAction(widgetPostAPIMediator, eventData) {
+ room.sendStateEvent(
+ eventType = EventType.BOT_OPTIONS,
+ stateKey = stateKey,
+ body = content
+ )
+ }
}
/**
@@ -456,4 +462,19 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
private fun createWidgetAPICallback(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict): WidgetAPICallback {
return WidgetAPICallback(widgetPostAPIMediator, eventData, stringProvider)
}
+
+ private fun launchWidgetAPIAction(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict, block: suspend () -> Unit): Job {
+ return GlobalScope.launch {
+ kotlin.runCatching {
+ block()
+ }.fold(
+ onSuccess = {
+ widgetPostAPIMediator.sendSuccess(eventData)
+ },
+ onFailure = {
+ widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData)
+ }
+ )
+ }
+ }
}
diff --git a/vector/src/main/res/drawable/bg_send.xml b/vector/src/main/res/drawable/bg_send.xml
index 4b357d7ab1..8ab95bf5c5 100644
--- a/vector/src/main/res/drawable/bg_send.xml
+++ b/vector/src/main/res/drawable/bg_send.xml
@@ -1,6 +1,10 @@
- -
+
-
diff --git a/vector/src/main/res/drawable/ic_close_24dp.xml b/vector/src/main/res/drawable/ic_close_24dp.xml
new file mode 100644
index 0000000000..d69c331210
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_close_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/layout/merge_composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml
similarity index 97%
rename from vector/src/main/res/layout/merge_composer_layout.xml
rename to vector/src/main/res/layout/composer_layout.xml
index ea2bc1bf30..cb0b37d844 100644
--- a/vector/src/main/res/layout/merge_composer_layout.xml
+++ b/vector/src/main/res/layout/composer_layout.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- tools:constraintSet="@layout/constraint_set_composer_layout_compact"
+ tools:constraintSet="@layout/composer_layout_constraint_set_compact"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
similarity index 88%
rename from vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
rename to vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
index 6b9fbd4885..a4dfcf019c 100644
--- a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml
+++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
@@ -10,7 +10,7 @@
app:layout_constraintStart_toStartOf="parent">
-
-
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml
similarity index 89%
rename from vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml
rename to vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml
index f52b072ece..8a76c0547e 100644
--- a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml
+++ b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml
@@ -10,7 +10,7 @@
app:layout_constraintStart_toStartOf="parent">
+ app:layout_constraintEnd_toEndOf="@id/related_message_background"
+ app:layout_constraintStart_toStartOf="@+id/related_message_background"
+ app:layout_constraintTop_toTopOf="@id/related_message_background" />
+ app:layout_constraintBottom_toBottomOf="@id/related_message_background"
+ app:layout_constraintEnd_toEndOf="@id/related_message_background"
+ app:layout_constraintStart_toStartOf="@+id/related_message_background" />
-
-
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml
index 15016e0abe..cfde244217 100644
--- a/vector/src/main/res/layout/item_timeline_event_base.xml
+++ b/vector/src/main/res/layout/item_timeline_event_base.xml
@@ -87,7 +87,6 @@
android:id="@+id/messageContentTextStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
- android:inflatedId="@id/messageTextView"
android:layout="@layout/item_timeline_event_text_message_stub"
tools:visibility="visible" />
diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml
index 59396db0e5..7bdd0dd1e3 100644
--- a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml
@@ -1,9 +1,26 @@
-
+ android:orientation="vertical">
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/url_preview.xml b/vector/src/main/res/layout/url_preview.xml
new file mode 100644
index 0000000000..a8c287b471
--- /dev/null
+++ b/vector/src/main/res/layout/url_preview.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml
index 0412a03103..647ada65be 100644
--- a/vector/src/main/res/values/colors_riotx.xml
+++ b/vector/src/main/res/values/colors_riotx.xml
@@ -107,6 +107,13 @@
#FFA1B2D1
#FFA1B2D1
+
+ #FF8D99A5
+
+ #FF8D99A5
+
+ #FF8D99A5
+
#FF61708B
#FFA1B2D1
diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml
index 18ced0a071..ab0ecbe4e9 100644
--- a/vector/src/main/res/values/theme_black.xml
+++ b/vector/src/main/res/values/theme_black.xml
@@ -18,6 +18,7 @@
- @color/riotx_header_panel_text_secondary_black
- @color/riotx_text_primary_black
- @color/riotx_text_secondary_black
+ - @color/riotx_text_tertiary_black
- @color/riotx_text_primary_body_contrast_black
- @color/riotx_android_secondary_black
- @color/riotx_search_placeholder_black
diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml
index cdd5cde488..6ebf8e2b9b 100644
--- a/vector/src/main/res/values/theme_dark.xml
+++ b/vector/src/main/res/values/theme_dark.xml
@@ -16,6 +16,7 @@
- @color/riotx_header_panel_text_secondary_dark
- @color/riotx_text_primary_dark
- @color/riotx_text_secondary_dark
+ - @color/riotx_text_tertiary_dark
- @color/riotx_text_primary_body_contrast_dark
- @color/riotx_android_secondary_dark
- @color/riotx_search_placeholder_dark
diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml
index 3c1505bb60..d7b91a37a7 100644
--- a/vector/src/main/res/values/theme_light.xml
+++ b/vector/src/main/res/values/theme_light.xml
@@ -16,6 +16,7 @@
- @color/riotx_header_panel_text_secondary_light
- @color/riotx_text_primary_light
- @color/riotx_text_secondary_light
+ - @color/riotx_text_tertiary_light
- @color/riotx_text_primary_body_contrast_light
- @color/riotx_android_secondary_light
- @color/riotx_search_placeholder_light
diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml
index a162bf28fb..ad4cf8e3ed 100644
--- a/vector/src/main/res/xml/vector_settings_preferences.xml
+++ b/vector/src/main/res/xml/vector_settings_preferences.xml
@@ -57,8 +57,7 @@
android:defaultValue="true"
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"
android:summary="@string/settings_inline_url_preview_summary"
- android:title="@string/settings_inline_url_preview"
- app:isPreferenceVisible="@bool/false_not_implemented" />
+ android:title="@string/settings_inline_url_preview" />