From f6cc05634ff3852be95b20a35c783bb1c0e59660 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Nov 2020 16:38:46 +0100 Subject: [PATCH 01/89] Send task: small rework and cleanup --- .../room/send/queue/EventSenderProcessor.kt | 6 +---- .../session/room/send/queue/QueueMemento.kt | 1 - .../session/room/send/queue/QueuedTask.kt | 12 +++++++--- .../room/send/queue/RedactQueuedTask.kt | 22 +++++++------------ .../room/send/queue/SendEventQueuedTask.kt | 10 ++------- 5 files changed, 20 insertions(+), 31 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index 62e225c624..b8f6e52674 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -106,11 +106,7 @@ internal class EventSenderProcessor @Inject constructor( // non blocking add to queue sendingQueue.add(task) markAsManaged(task) - return object : Cancelable { - override fun cancel() { - task.cancel() - } - } + return task } companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt index e69c65ec4c..dfbac347d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.room.send.queue import android.content.Context -import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.send.SendState diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt index bccbc97ff4..e5c1cf7435 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt @@ -16,14 +16,20 @@ package org.matrix.android.sdk.internal.session.room.send.queue -abstract class QueuedTask { +import org.matrix.android.sdk.api.util.Cancelable + +abstract class QueuedTask : Cancelable { var retryCount = 0 + private var hasBeenCancelled: Boolean = false + abstract suspend fun execute() abstract fun onTaskFailed() - abstract fun isCancelled() : Boolean + open fun isCancelled() = hasBeenCancelled - abstract fun cancel() + final override fun cancel() { + hasBeenCancelled = true + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt index a3c19a1f7c..e2fb978cd0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt @@ -22,18 +22,16 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository internal class RedactQueuedTask( - val toRedactEventId: String, + private val toRedactEventId: String, val redactionLocalEchoId: String, - val roomId: String, - val reason: String?, - val redactEventTask: RedactEventTask, - val localEchoRepository: LocalEchoRepository, - val cancelSendTracker: CancelSendTracker + private val roomId: String, + private val reason: String?, + private val redactEventTask: RedactEventTask, + private val localEchoRepository: LocalEchoRepository, + private val cancelSendTracker: CancelSendTracker ) : QueuedTask() { - private var _isCancelled: Boolean = false - - override fun toString() = "[RedactEventRunnableTask $redactionLocalEchoId]" + override fun toString() = "[RedactQueuedTask $redactionLocalEchoId]" override suspend fun execute() { redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason)) @@ -44,10 +42,6 @@ internal class RedactQueuedTask( } override fun isCancelled(): Boolean { - return _isCancelled || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId) - } - - override fun cancel() { - _isCancelled = true + return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt index 21a4145a9d..f934aad67b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt @@ -33,9 +33,7 @@ internal class SendEventQueuedTask( val cancelSendTracker: CancelSendTracker ) : QueuedTask() { - private var _isCancelled: Boolean = false - - override fun toString() = "[SendEventRunnableTask ${event.eventId}]" + override fun toString() = "[SendEventQueuedTask ${event.eventId}]" override suspend fun execute() { sendEventTask.execute(SendEventTask.Params(event, encrypt)) @@ -56,10 +54,6 @@ internal class SendEventQueuedTask( } override fun isCancelled(): Boolean { - return _isCancelled || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId) - } - - override fun cancel() { - _isCancelled = true + return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId) } } From cd983de058557ae0bd9902138d80f863dfa78323 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 30 Nov 2020 10:08:31 +0100 Subject: [PATCH 02/89] Fix cancellation of sending event (#2438) --- CHANGES.md | 2 +- .../session/room/send/DefaultSendService.kt | 2 ++ .../room/send/queue/EventSenderProcessor.kt | 14 ++++++++++++++ .../internal/session/room/send/queue/QueuedTask.kt | 8 +++++++- .../session/room/send/queue/RedactQueuedTask.kt | 2 +- .../session/room/send/queue/SendEventQueuedTask.kt | 2 +- 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e48281081b..7f626da497 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Improvements 🙌: - Bugfix 🐛: - - + - Fix cancellation of sending event (#2438) Translations 🗣: - 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 b13ce15da6..5a71ff7b76 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 @@ -210,6 +210,8 @@ internal class DefaultSendService @AssistedInject constructor( override fun cancelSend(eventId: String) { cancelSendTracker.markLocalEchoForCancel(eventId, roomId) + // This is maybe the current task, so cancel it too + eventSenderProcessor.cancel(eventId, roomId) taskExecutor.executorScope.launch { localEchoRepository.deleteFailedEcho(roomId, eventId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index b8f6e52674..5014d94558 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.room.send.queue +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.auth.data.SessionParams @@ -109,10 +110,18 @@ internal class EventSenderProcessor @Inject constructor( return task } + fun cancel(eventId: String, roomId: String) { + (currentTask as? SendEventQueuedTask) + ?.takeIf { it -> it.event.eventId == eventId && it.event.roomId == roomId } + ?.cancel() + } + companion object { private const val RETRY_WAIT_TIME_MS = 10_000L } + private var currentTask: QueuedTask? = null + private var sendingQueue = LinkedBlockingQueue() private var networkAvailableLock = Object() @@ -125,6 +134,7 @@ internal class EventSenderProcessor @Inject constructor( while (!isInterrupted) { Timber.v("## SendThread wait for task to process") val task = sendingQueue.take() + .also { currentTask = it } Timber.v("## SendThread Found task to process $task") if (task.isCancelled()) { @@ -179,6 +189,10 @@ internal class EventSenderProcessor @Inject constructor( task.onTaskFailed() throw InterruptedException() } + exception is CancellationException -> { + Timber.v("## SendThread task has been cancelled") + break@retryLoop + } else -> { Timber.v("## SendThread retryLoop Un-Retryable error, try next task") // this task is in error, check next one? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt index e5c1cf7435..9a7fcd8d91 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt @@ -23,7 +23,13 @@ abstract class QueuedTask : Cancelable { private var hasBeenCancelled: Boolean = false - abstract suspend fun execute() + suspend fun execute() { + if (!isCancelled()) { + doExecute() + } + } + + abstract suspend fun doExecute() abstract fun onTaskFailed() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt index e2fb978cd0..8e7ba2f155 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt @@ -33,7 +33,7 @@ internal class RedactQueuedTask( override fun toString() = "[RedactQueuedTask $redactionLocalEchoId]" - override suspend fun execute() { + override suspend fun doExecute() { redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason)) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt index f934aad67b..ea097082c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt @@ -35,7 +35,7 @@ internal class SendEventQueuedTask( override fun toString() = "[SendEventQueuedTask ${event.eventId}]" - override suspend fun execute() { + override suspend fun doExecute() { sendEventTask.execute(SendEventTask.Params(event, encrypt)) } From 42bc4d2445d2a5ee8f06a093ba8e11cc7dda992c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 12:08:29 +0100 Subject: [PATCH 03/89] Upgrade some dependencies and Kotlin version --- CHANGES.md | 2 +- build.gradle | 6 +++--- matrix-sdk-android/build.gradle | 2 +- multipicker/build.gradle | 2 +- vector/build.gradle | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f6f63db534..9bcd0f2eea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,7 +20,7 @@ SDK API changes ⚠️: - Build 🧱: - - + - Upgrade some dependencies and Kotlin version Test: - diff --git a/build.gradle b/build.gradle index 0c4b35b060..6dd61a720c 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ buildscript { // Ref: https://kotlinlang.org/releases.html - ext.kotlin_version = '1.4.10' - ext.kotlin_coroutines_version = "1.3.9" + ext.kotlin_version = '1.4.20' + ext.kotlin_coroutines_version = "1.4.1" repositories { google() jcenter() @@ -12,7 +12,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.1' classpath 'com.google.gms:google-services:4.3.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1' diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 29c709844a..1923ab2606 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -146,7 +146,7 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.3.0' + implementation 'androidx.exifinterface:exifinterface:1.3.1' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' diff --git a/multipicker/build.gradle b/multipicker/build.gradle index b6e500e493..6e22c8207e 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -44,7 +44,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.2.0' implementation "androidx.fragment:fragment:1.3.0-beta01" - implementation 'androidx.exifinterface:exifinterface:1.3.0' + implementation 'androidx.exifinterface:exifinterface:1.3.1' // Log implementation 'com.jakewharton.timber:timber:4.7.1' diff --git a/vector/build.gradle b/vector/build.gradle index 561e1fd824..811189c693 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -317,7 +317,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version" - implementation 'androidx.constraintlayout:constraintlayout:2.0.2' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.0.0" implementation 'androidx.core:core-ktx:1.3.2' @@ -366,7 +366,7 @@ dependencies { // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.3.0-alpha02' + implementation 'com.google.android.material:material:1.3.0-alpha04' implementation 'me.gujun.android:span:1.7' implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:html:$markwon_version" @@ -374,7 +374,7 @@ dependencies { implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:1.1.1' implementation "androidx.autofill:autofill:$autofill_version" - implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta10' + implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // Custom Tab implementation 'androidx.browser:browser:1.2.0' @@ -418,7 +418,7 @@ dependencies { kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0' // gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:20.3.0') { + gplayImplementation('com.google.firebase:firebase-messaging:21.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' From cc5264a587d704b9d1f7563cb7149f0c7eef6ee3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 12:38:07 +0100 Subject: [PATCH 04/89] Try to clarify the code --- .../android/sdk/rx/RxCallbackBuilders.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt index f6dbe3d160..d5bb53e9c9 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt @@ -21,34 +21,34 @@ import org.matrix.android.sdk.api.util.Cancelable import io.reactivex.Completable import io.reactivex.Single -fun singleBuilder(builder: (callback: MatrixCallback) -> Cancelable): Single = Single.create { - val callback: MatrixCallback = object : MatrixCallback { +fun singleBuilder(builder: (MatrixCallback) -> Cancelable): Single = Single.create { emitter -> + val callback = object : MatrixCallback { override fun onSuccess(data: T) { - it.onSuccess(data) + emitter.onSuccess(data) } override fun onFailure(failure: Throwable) { - it.tryOnError(failure) + emitter.tryOnError(failure) } } val cancelable = builder(callback) - it.setCancellable { + emitter.setCancellable { cancelable.cancel() } } -fun completableBuilder(builder: (callback: MatrixCallback) -> Cancelable): Completable = Completable.create { - val callback: MatrixCallback = object : MatrixCallback { +fun completableBuilder(builder: (MatrixCallback) -> Cancelable): Completable = Completable.create { emitter -> + val callback = object : MatrixCallback { override fun onSuccess(data: T) { - it.onComplete() + emitter.onComplete() } override fun onFailure(failure: Throwable) { - it.tryOnError(failure) + emitter.tryOnError(failure) } } val cancelable = builder(callback) - it.setCancellable { + emitter.setCancellable { cancelable.cancel() } } From c426364618b95f26716598b57e0fb2ac7691c41c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 12:45:31 +0100 Subject: [PATCH 05/89] Remove unused dependencies --- attachment-viewer/build.gradle | 1 - matrix-sdk-android-rx/build.gradle | 2 +- matrix-sdk-android/build.gradle | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 91ddd519df..59ba6c4500 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -66,7 +66,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:1.3.0-beta01" implementation "androidx.recyclerview:recyclerview:1.1.0" implementation 'com.google.android.material:material:1.2.1' diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 3d62758065..37f41d0a2a 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -36,9 +36,9 @@ android { dependencies { implementation project(":matrix-sdk-android") implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:1.3.0-beta01" implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + // Paging implementation "androidx.paging:paging-runtime-ktx:2.1.2" diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 1923ab2606..d961560c17 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -125,7 +125,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.appcompat:appcompat:1.2.0" - implementation "androidx.fragment:fragment:1.3.0-beta01" implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" From 89a7ec6d4b78208c440cd4b0bcf1b0323fe6c166 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 12:46:15 +0100 Subject: [PATCH 06/89] Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable) --- CHANGES.md | 1 + multipicker/build.gradle | 2 +- vector/build.gradle | 2 +- vector/lint.xml | 3 +++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9bcd0f2eea..b16a6690bc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ SDK API changes ⚠️: Build 🧱: - Upgrade some dependencies and Kotlin version + - Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable) Test: - diff --git a/multipicker/build.gradle b/multipicker/build.gradle index 6e22c8207e..7c29a5539f 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -43,7 +43,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:1.3.0-beta01" + implementation "androidx.fragment:fragment-ktx:1.3.0-beta01" implementation 'androidx.exifinterface:exifinterface:1.3.1' // Log diff --git a/vector/build.gradle b/vector/build.gradle index 811189c693..c4164255db 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -362,7 +362,7 @@ dependencies { implementation "io.arrow-kt:arrow-core:$arrow_version" // Pref - implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.preference:preference-ktx:1.1.1' // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' diff --git a/vector/lint.xml b/vector/lint.xml index 4ac0f20e51..51d795b669 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -52,6 +52,9 @@ + + + From 9881c9f61c7e86f909cc034a61e85f4bfb78892c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 13:34:34 +0100 Subject: [PATCH 07/89] Fix compilation issue after library upgrade --- .../main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt | 4 +++- .../src/main/java/im/vector/app/features/pin/PinFragment.kt | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt index d5bb53e9c9..ec30a31f6d 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt @@ -24,7 +24,9 @@ import io.reactivex.Single fun singleBuilder(builder: (MatrixCallback) -> Cancelable): Single = Single.create { emitter -> val callback = object : MatrixCallback { override fun onSuccess(data: T) { - emitter.onSuccess(data) + // Add `!!` to fix the warning: + // "Type mismatch: type parameter with nullable bounds is used T is used where T was expected. This warning will become an error soon" + emitter.onSuccess(data!!) } override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt index 378c7b853d..1aa4846f38 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt @@ -74,6 +74,10 @@ class PinFragment @Inject constructor( Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show() } + override fun onPinCodeEnteredFirst(pinCode: String?): Boolean { + return false + } + override fun onCodeCreated(encodedCode: String) { lifecycleScope.launch { pinCodeStore.storeEncodedPin(encodedCode) From f0afd5ceea2f359f46ed4b2879b71d942ffd4d67 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 13:49:07 +0100 Subject: [PATCH 08/89] Update deprecated code --- .../settings/troubleshoot/TestFirebaseToken.kt | 6 +++--- .../gplay/java/im/vector/app/push/fcm/FcmHelper.kt | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt index 32888dafd7..fd541c0528 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt @@ -18,7 +18,7 @@ package im.vector.app.gplay.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AppCompatActivity -import com.google.firebase.iid.FirebaseInstanceId +import com.google.firebase.messaging.FirebaseMessaging import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.startAddGoogleAccountIntent @@ -36,7 +36,7 @@ class TestFirebaseToken @Inject constructor(private val context: AppCompatActivi override fun perform(activityResultLauncher: ActivityResultLauncher) { status = TestStatus.RUNNING try { - FirebaseInstanceId.getInstance().instanceId + FirebaseMessaging.getInstance().token .addOnCompleteListener(context) { task -> if (!task.isSuccessful) { val errorMsg = if (task.exception == null) "Unknown" else task.exception!!.localizedMessage @@ -57,7 +57,7 @@ class TestFirebaseToken @Inject constructor(private val context: AppCompatActivi } status = TestStatus.FAILED } else { - task.result?.token?.let { token -> + task.result?.let { token -> val tok = token.substring(0, Math.min(8, token.length)) + "********************" description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok) Timber.e("Retrieved FCM token success [$tok].") diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index 913eab211d..f3bdcafb1c 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -21,7 +21,7 @@ import android.widget.Toast import androidx.core.content.edit import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import com.google.firebase.iid.FirebaseInstanceId +import com.google.firebase.messaging.FirebaseMessaging import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultSharedPreferences @@ -71,14 +71,16 @@ object FcmHelper { // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' if (checkPlayServices(activity)) { try { - FirebaseInstanceId.getInstance().instanceId - .addOnSuccessListener(activity) { instanceIdResult -> - storeFcmToken(activity, instanceIdResult.token) + FirebaseMessaging.getInstance().token + .addOnSuccessListener { token -> + storeFcmToken(activity, token) if (registerPusher) { - pushersManager.registerPusherWithFcmKey(instanceIdResult.token) + pushersManager.registerPusherWithFcmKey(token) } } - .addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } + .addOnFailureListener { e -> + Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") + } } catch (e: Throwable) { Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } From 1058bfecf463133bc255bc2b2fed2913e2ad4706 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 13:54:59 +0100 Subject: [PATCH 09/89] Kotlinification --- .../troubleshoot/TestFirebaseToken.kt | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt index fd541c0528..1107737888 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt @@ -39,26 +39,30 @@ class TestFirebaseToken @Inject constructor(private val context: AppCompatActivi FirebaseMessaging.getInstance().token .addOnCompleteListener(context) { task -> if (!task.isSuccessful) { - val errorMsg = if (task.exception == null) "Unknown" else task.exception!!.localizedMessage // Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated) - if ("SERVICE_NOT_AVAILABLE".equals(errorMsg)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg) - } else if ("TOO_MANY_REGISTRATIONS".equals(errorMsg)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg) - } else if ("ACCOUNT_MISSING".equals(errorMsg)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg) - quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) { - override fun doFix() { - startAddGoogleAccountIntent(context, activityResultLauncher) - } + description = when (val errorMsg = task.exception?.localizedMessage ?: "Unknown") { + "SERVICE_NOT_AVAILABLE" -> { + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg) + } + "TOO_MANY_REGISTRATIONS" -> { + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg) + } + "ACCOUNT_MISSING" -> { + quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) { + override fun doFix() { + startAddGoogleAccountIntent(context, activityResultLauncher) + } + } + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg) + } + else -> { + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg) } - } else { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg) } status = TestStatus.FAILED } else { task.result?.let { token -> - val tok = token.substring(0, Math.min(8, token.length)) + "********************" + val tok = token.take(8) + "********************" description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok) Timber.e("Retrieved FCM token success [$tok].") // Ensure it is well store in our local storage From 6c56b5f45b4d9b6aa5e6e7286520a43e13fcae07 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 14:39:33 +0100 Subject: [PATCH 10/89] Fix warnings after library upgrade --- .../main/java/im/vector/app/core/platform/VectorViewModel.kt | 4 ++-- vector/src/main/java/im/vector/app/core/utils/DataSource.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt index 002dfcf068..d6f43beaf7 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt @@ -43,7 +43,7 @@ abstract class VectorViewModel Single.toAsync(stateReducer: S.(Async) -> S): Single> { setState { stateReducer(Loading()) } return map { Success(it) as Async } @@ -56,7 +56,7 @@ abstract class VectorViewModel Observable.toAsync(stateReducer: S.(Async) -> S): Observable> { setState { stateReducer(Loading()) } return map { Success(it) as Async } diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index 8a908ad1d4..06bdeb9277 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -44,7 +44,7 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD } override fun post(value: T) { - behaviorRelay.accept(value) + behaviorRelay.accept(value!!) } private fun createRelay(): BehaviorRelay { @@ -68,6 +68,6 @@ open class PublishDataSource : MutableDataSource { } override fun post(value: T) { - publishRelay.accept(value) + publishRelay.accept(value!!) } } From ee96d5c68f4ed3ecc4bb0f0af7763e5f204bccac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 14:59:09 +0100 Subject: [PATCH 11/89] Remove useless Factories --- .../preview/AttachmentsPreviewFragment.kt | 1 - .../preview/AttachmentsPreviewViewModel.kt | 21 +------------------ .../RoomHistoryVisibilityBottomSheet.kt | 1 - .../RoomHistoryVisibilityViewModel.kt | 20 +----------------- .../joinrule/RoomJoinRuleBottomSheet.kt | 1 - .../joinrule/RoomJoinRuleViewModel.kt | 20 +----------------- 6 files changed, 3 insertions(+), 61 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index ba0250724c..f67b0946cc 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -59,7 +59,6 @@ data class AttachmentsPreviewArgs( ) : Parcelable class AttachmentsPreviewFragment @Inject constructor( - val viewModelFactory: AttachmentsPreviewViewModel.Factory, private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController, private val attachmentBigPreviewController: AttachmentBigPreviewController, private val colorProvider: ColorProvider diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt index 59a0937d89..28d617e613 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt @@ -17,31 +17,12 @@ package im.vector.app.features.attachments.preview -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState) +class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState) : VectorViewModel(initialState) { - @AssistedInject.Factory - interface Factory { - fun create(initialState: AttachmentsPreviewViewState): AttachmentsPreviewViewModel - } - - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: AttachmentsPreviewViewState): AttachmentsPreviewViewModel? { - val fragment: AttachmentsPreviewFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } - override fun handle(action: AttachmentsPreviewAction) { when (action) { is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt index 813a58838c..c12dc621a9 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt @@ -37,7 +37,6 @@ class RoomHistoryVisibilityBottomSheet : BottomSheetGeneric(initialState) { - @AssistedInject.Factory - interface Factory { - fun create(initialState: RoomHistoryVisibilityState): RoomHistoryVisibilityViewModel - } - - companion object : MvRxViewModelFactory { - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomHistoryVisibilityState): RoomHistoryVisibilityViewModel? { - val fragment: RoomHistoryVisibilityBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.roomHistoryVisibilityViewModelFactory.create(state) - } - } - override fun handle(action: EmptyAction) { // No op } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt index 4996187029..66c6be6086 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt @@ -39,7 +39,6 @@ class RoomJoinRuleBottomSheet : BottomSheetGeneric(initialState) { - @AssistedInject.Factory - interface Factory { - fun create(initialState: RoomJoinRuleState): RoomJoinRuleViewModel - } - - companion object : MvRxViewModelFactory { - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomJoinRuleState): RoomJoinRuleViewModel? { - val fragment: RoomJoinRuleBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.roomJoinRuleViewModelFactory.create(state) - } - } - override fun handle(action: EmptyAction) { // No op } From f5af15454e71e271ff25fa9b234616c33031a5a8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 15:47:21 +0100 Subject: [PATCH 12/89] Create a generic class for ViewModel --- .../BottomSheetGenericViewModel.kt | 30 +++++++++++++++++++ .../RoomHistoryVisibilityViewModel.kt | 11 ++----- .../joinrule/RoomJoinRuleViewModel.kt | 11 ++----- 3 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt new file mode 100644 index 0000000000..6cc2c4c981 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.ui.bottomsheet + +import com.airbnb.mvrx.MvRxState +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel + +abstract class BottomSheetGenericViewModel(initialState: State) : + VectorViewModel(initialState) { + + override fun handle(action: EmptyAction) { + // No op + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt index 252865e4c5..c2a8ae967f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt @@ -16,14 +16,7 @@ package im.vector.app.features.roomprofile.settings.historyvisibility -import im.vector.app.core.platform.EmptyAction -import im.vector.app.core.platform.EmptyViewEvents -import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel class RoomHistoryVisibilityViewModel(initialState: RoomHistoryVisibilityState) - : VectorViewModel(initialState) { - - override fun handle(action: EmptyAction) { - // No op - } -} + : BottomSheetGenericViewModel(initialState) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt index 28c6f99f7c..4305bfa72d 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt @@ -16,14 +16,7 @@ package im.vector.app.features.roomprofile.settings.joinrule -import im.vector.app.core.platform.EmptyAction -import im.vector.app.core.platform.EmptyViewEvents -import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel class RoomJoinRuleViewModel(initialState: RoomJoinRuleState) - : VectorViewModel(initialState) { - - override fun handle(action: EmptyAction) { - // No op - } -} + : BottomSheetGenericViewModel(initialState) From c4577f28b286a8420c7f26b1ac55d9efc3210750 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 16:43:52 +0100 Subject: [PATCH 13/89] Remove unnecessary dependency (we have -ktx dependency) --- vector/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index c4164255db..62ff3951af 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -315,7 +315,6 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06" implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.0.0" From d889598b20d251929da3639904629ef368746886 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Dec 2020 17:04:54 +0100 Subject: [PATCH 14/89] Fix DefaultLocale lint issue --- .../SASDefaultVerificationTransaction.kt | 12 ++++++------ .../org/matrix/android/sdk/internal/util/Hash.kt | 3 ++- vector/lint.xml | 1 + .../java/im/vector/app/core/intent/VectorMimeType.kt | 3 ++- .../main/java/im/vector/app/core/utils/FileUtils.kt | 3 ++- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 22a190c68e..c7885ce449 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.extensions.toUnsignedInt import org.matrix.olm.OlmSAS import org.matrix.olm.OlmUtility import timber.log.Timber +import java.util.Locale /** * Represents an ongoing short code interactive key verification between two devices. @@ -344,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction( } protected fun hashUsingAgreedHashMethod(toHash: String): String? { - if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) { + if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) { val olmUtil = OlmUtility() val hashBytes = olmUtil.sha256(toHash) olmUtil.releaseUtility() @@ -354,12 +355,11 @@ internal abstract class SASDefaultVerificationTransaction( } private fun macUsingAgreedMethod(message: String, info: String): String? { - if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { - return getSAS().calculateMacLongKdf(message, info) - } else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { - return getSAS().calculateMac(message, info) + return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) { + SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info) + SAS_MAC_SHA256 -> getSAS().calculateMac(message, info) + else -> null } - return null } override fun getDecimalCodeRepresentation(): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt index 3d80ad01d5..e19b1bcca7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.util import java.security.MessageDigest +import java.util.Locale /** * Compute a Hash of a String, using md5 algorithm @@ -26,7 +27,7 @@ fun String.md5() = try { digest.update(toByteArray()) digest.digest() .joinToString("") { String.format("%02X", it) } - .toLowerCase() + .toLowerCase(Locale.ROOT) } catch (exc: Exception) { // Should not happen, but just in case hashCode().toString() diff --git a/vector/lint.xml b/vector/lint.xml index 51d795b669..572f937406 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -41,6 +41,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt index c8a2bf65d5..1299f4086b 100644 --- a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt +++ b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt @@ -21,6 +21,7 @@ import android.net.Uri import android.webkit.MimeTypeMap import im.vector.app.core.utils.getFileExtension import timber.log.Timber +import java.util.Locale /** * Returns the mimetype from a uri. @@ -44,7 +45,7 @@ fun getMimeTypeFromUri(context: Context, uri: Uri): String? { if (null != mimeType) { // the mimetype is sometimes in uppercase. - mimeType = mimeType.toLowerCase() + mimeType = mimeType.toLowerCase(Locale.ROOT) } } catch (e: Exception) { Timber.e(e, "Failed to open resource input stream") diff --git a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt index ab99ba61bd..aa36dd0959 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt @@ -19,6 +19,7 @@ package im.vector.app.core.utils import android.content.Context import timber.log.Timber import java.io.File +import java.util.Locale // Implementation should return true in case of success typealias ActionOnFile = (file: File) -> Boolean @@ -113,7 +114,7 @@ fun getFileExtension(fileUri: String): String? { val ext = filename.substring(dotPos + 1) if (ext.isNotBlank()) { - return ext.toLowerCase() + return ext.toLowerCase(Locale.ROOT) } } } From cafe86e67508ae5264985afecf357fe2cb921a32 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 09:56:26 +0100 Subject: [PATCH 15/89] Rework: create a MediaModule --- .../sdk/internal/session/SessionComponent.kt | 2 + .../session/homeserver/CapabilitiesAPI.kt | 7 ---- .../DefaultGetHomeServerCapabilitiesTask.kt | 17 ++++---- .../GetMediaConfigResult.kt} | 6 +-- .../sdk/internal/session/media/MediaAPI.kt | 30 ++++++++++++++ .../sdk/internal/session/media/MediaModule.kt | 39 +++++++++++++++++++ 6 files changed, 84 insertions(+), 17 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/{homeserver/GetUploadCapabilitiesResult.kt => media/GetMediaConfigResult.kt} (86%) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt 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/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/DefaultGetHomeServerCapabilitiesTask.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/DefaultGetHomeServerCapabilitiesTask.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/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/MediaAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt new file mode 100644 index 0000000000..dba693c724 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt @@ -0,0 +1,30 @@ +/* + * 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.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +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 +} 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..84a82dc75a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.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.session.media + +import dagger.Module +import dagger.Provides +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 bindGetHomeServerCapabilitiesTask(task: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask +} From 3e563a37a22de883b32877bed61abc5c0cf1d3c5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 13:43:04 +0100 Subject: [PATCH 16/89] Rework: Make RawCacheStrategy class more generic, to use it for other SDK API --- CHANGES.md | 2 +- .../CacheStrategy.kt} | 10 +++++----- .../org/matrix/android/sdk/api/raw/RawService.kt | 4 +++- .../sdk/internal/raw/DefaultGetUrlTask.kt | 16 ++++++++-------- .../sdk/internal/raw/DefaultRawService.kt | 8 ++++---- 5 files changed, 21 insertions(+), 19 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/{raw/RawCacheStrategy.kt => cache/CacheStrategy.kt} (83%) diff --git a/CHANGES.md b/CHANGES.md index f6f63db534..06af610ecb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ Translations 🗣: - SDK API changes ⚠️: - - + - RawCacheStrategy has been moved and renamed to CacheStrategy Build 🧱: - diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt similarity index 83% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt index f4eada559e..2880d851d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.raw +package org.matrix.android.sdk.api.cache -sealed class RawCacheStrategy { +sealed class CacheStrategy { // Data is always fetched from the server - object NoCache : RawCacheStrategy() + object NoCache : CacheStrategy() // Once data is retrieved, it is stored for the provided amount of time. // In case of error, and if strict is set to false, the cache can be returned if available - data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : RawCacheStrategy() + data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : CacheStrategy() // Once retrieved, the data is stored in cache and will be always get from the cache - object InfiniteCache : RawCacheStrategy() + object InfiniteCache : CacheStrategy() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt index 19549a338e..f1722b2189 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.raw +import org.matrix.android.sdk.api.cache.CacheStrategy + /** * Useful methods to fetch raw data from the server. The access token will not be used to fetched the data */ @@ -23,7 +25,7 @@ interface RawService { /** * Get a URL, either from cache or from the remote server, depending on the cache strategy */ - suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String + suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String /** * Specific case for the well-known file. Cache validity is 8 hours 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/DefaultGetUrlTask.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/DefaultGetUrlTask.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/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) ) } From 0c037184f857fc67a38c91022d5b5d6e971c4ce5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 13:46:25 +0100 Subject: [PATCH 17/89] Create a MediaService to handle UrlPreview request - WIP --- .idea/dictionaries/bmarty.xml | 1 + .../sdk/api/session/media/MediaService.kt | 45 +++++++++++++++++ .../sdk/api/session/media/PreviewUrlData.kt | 49 +++++++++++++++++++ .../sdk/internal/session/media/MediaAPI.kt | 13 +++++ 4 files changed, 108 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 5ad39614b7..8bf33df0ac 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -24,6 +24,7 @@ pbkdf pids pkcs + previewable riotx signin signout diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt new file mode 100644 index 0000000000..6594b2d0b5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt @@ -0,0 +1,45 @@ +/* + * 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 + +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.JsonDict + +interface MediaService { + /** + * Extract URLs from an Event. + * @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data + */ + fun extractUrls(event: Event): List + + /** + * Get Raw Url Preview data from the homeserver. There is no cache management for this request + */ + suspend fun getRawPreviewUrl(url: String): JsonDict + + /** + * Get Url Preview data from the homeserver, or from cache, depending on the cache strategy + * @param url + */ + suspend fun getPreviewUrl(url: String, 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..1b32aed7d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt @@ -0,0 +1,49 @@ +/* + * 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: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/internal/session/media/MediaAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt index dba693c724..821d9b9875 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt @@ -16,9 +16,11 @@ 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 { /** @@ -27,4 +29,15 @@ internal interface MediaAPI { */ @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: Int?): Call } From 8a35bfcc31e4d3b92878c346a154ddaa835cc3c7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 13:57:47 +0100 Subject: [PATCH 18/89] Rework: rename files using the interface name and not the implementation name --- .../raw/{DefaultCleanRawCacheTask.kt => CleanRawCacheTask.kt} | 0 .../sdk/internal/raw/{DefaultGetUrlTask.kt => GetUrlTask.kt} | 0 .../filter/{DefaultSaveFilterTask.kt => SaveFilterTask.kt} | 0 .../group/{DefaultGetGroupDataTask.kt => GetGroupDataTask.kt} | 0 ...ServerCapabilitiesTask.kt => GetHomeServerCapabilitiesTask.kt} | 0 .../{DefaultGetContextOfEventTask.kt => GetContextOfEventTask.kt} | 0 .../room/timeline/{DefaultPaginationTask.kt => PaginationTask.kt} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/{DefaultCleanRawCacheTask.kt => CleanRawCacheTask.kt} (100%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/{DefaultGetUrlTask.kt => GetUrlTask.kt} (100%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/{DefaultSaveFilterTask.kt => SaveFilterTask.kt} (100%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/{DefaultGetGroupDataTask.kt => GetGroupDataTask.kt} (100%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/{DefaultGetHomeServerCapabilitiesTask.kt => GetHomeServerCapabilitiesTask.kt} (100%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/{DefaultGetContextOfEventTask.kt => GetContextOfEventTask.kt} (100%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/{DefaultPaginationTask.kt => PaginationTask.kt} (100%) 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/DefaultGetUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt similarity index 100% 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 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/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt similarity index 100% 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 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 From 1109d9f88a1bc906941df6f1a8bbeb998719e9bc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 14:45:20 +0100 Subject: [PATCH 19/89] PreviewUrl create DB object and handle migration --- .../sdk/api/session/media/PreviewUrlData.kt | 2 + .../database/RealmSessionStoreMigration.kt | 18 ++++++++- .../database/model/PreviewUrlCacheEntity.kt | 36 +++++++++++++++++ .../database/model/SessionRealmModule.kt | 1 + .../query/PreviewUrlCacheEntityQueries.kt | 39 +++++++++++++++++++ 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt 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 index 1b32aed7d8..33fc8b052b 100644 --- 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 @@ -40,6 +40,8 @@ package org.matrix.android.sdk.api.session.media 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" 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) +} From bd5ac514efca81d8883182ae5de20466381c9dcf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 15:33:42 +0100 Subject: [PATCH 20/89] PreviewUrl: create the task and the service --- .../matrix/android/sdk/api/session/Session.kt | 6 + .../sdk/api/session/media/MediaService.kt | 11 +- .../sdk/internal/session/DefaultSession.kt | 4 + .../session/media/ClearPreviewUrlCacheTask.kt | 40 ++++++ .../session/media/DefaultMediaService.kt | 46 +++++++ .../session/media/GetPreviewUrlTask.kt | 122 ++++++++++++++++++ .../session/media/GetRawPreviewUrlTask.kt | 42 ++++++ .../sdk/internal/session/media/MediaAPI.kt | 2 +- .../sdk/internal/session/media/MediaModule.kt | 15 ++- .../session/media/PreviewUrlMapper.kt | 31 +++++ 10 files changed, 313 insertions(+), 6 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 56609610f1..8a95baf3cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -181,6 +182,11 @@ interface Session : */ fun widgetService(): WidgetService + /** + * Returns the media service associated with the session + */ + fun mediaService(): MediaService + /** * Returns the integration manager service associated with the session */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt index 6594b2d0b5..9040ec7d5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt @@ -29,14 +29,19 @@ interface MediaService { /** * Get Raw Url Preview data from the homeserver. There is no cache management for this request + * @param url The url to get the preview data from + * @param timestamp The optional timestamp */ - suspend fun getRawPreviewUrl(url: String): JsonDict + suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict /** * Get Url Preview data from the homeserver, or from cache, depending on the cache strategy - * @param url + * @param url The url to get the preview data from + * @param timestamp The optional timestamp. Note that this parameter is not taken into account + * if the data is already in cache and the cache strategy allow to use it + * @param cacheStrategy the cache strategy, see the type for more details */ - suspend fun getPreviewUrl(url: String, cacheStrategy: CacheStrategy): PreviewUrlData + suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData /** * Clear the cache of all retrieved UrlPreview data diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 25345e953c..c5f3f65a34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -102,6 +103,7 @@ internal class DefaultSession @Inject constructor( private val permalinkService: Lazy, private val secureStorageService: Lazy, private val profileService: Lazy, + private val mediaService: Lazy, private val widgetService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, @@ -263,6 +265,8 @@ internal class DefaultSession @Inject constructor( override fun widgetService(): WidgetService = widgetService.get() + override fun mediaService(): MediaService = mediaService.get() + override fun integrationManagerService() = integrationManagerService override fun callSignalingService(): CallSignalingService = callSignalingService.get() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt new file mode 100644 index 0000000000..004b622c64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import com.zhuinden.monarchy.Monarchy +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface ClearPreviewUrlCacheTask : Task + +internal class DefaultClearPreviewUrlCacheTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy +) : ClearPreviewUrlCacheTask { + + override suspend fun execute(params: Unit) { + monarchy.awaitTransaction { realm -> + realm.where() + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt new file mode 100644 index 0000000000..846b81ae72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.api.util.JsonDict +import javax.inject.Inject + +internal class DefaultMediaService @Inject constructor( + private val clearPreviewUrlCacheTask: ClearPreviewUrlCacheTask, + private val getPreviewUrlTask: GetPreviewUrlTask, + private val getRawPreviewUrlTask: GetRawPreviewUrlTask +) : MediaService { + override fun extractUrls(event: Event): List { + TODO("Not yet implemented") + } + + override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict { + return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp)) + } + + override suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData { + return getPreviewUrlTask.execute(GetPreviewUrlTask.Params(url, timestamp, cacheStrategy)) + } + + override suspend fun clearCache() { + clearPreviewUrlCacheTask.execute(Unit) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt new file mode 100644 index 0000000000..69cdfa8faa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import java.util.Date +import javax.inject.Inject + +internal interface GetPreviewUrlTask : Task { + data class Params( + val url: String, + val timestamp: Long?, + val cacheStrategy: CacheStrategy + ) +} + +internal class DefaultGetPreviewUrlTask @Inject constructor( + private val mediaAPI: MediaAPI, + private val eventBus: EventBus, + @SessionDatabase private val monarchy: Monarchy +) : GetPreviewUrlTask { + + override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData { + return when (params.cacheStrategy) { + CacheStrategy.NoCache -> doRequest(params.url, params.timestamp) + is CacheStrategy.TtlCache -> doRequestWithCache( + params.url, + params.timestamp, + params.cacheStrategy.validityDurationInMillis, + params.cacheStrategy.strict + ) + CacheStrategy.InfiniteCache -> doRequestWithCache( + params.url, + params.timestamp, + Long.MAX_VALUE, + true + ) + } + } + + private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData { + return executeRequest(eventBus) { + apiCall = mediaAPI.getPreviewUrlData(url, timestamp) + } + .toPreviewUrlData(url) + } + + private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData { + return PreviewUrlData( + url = (get("og:url") as? String) ?: url, + siteName = get("og:site_name") as? String, + title = get("og:title") as? String, + description = get("og:description") as? String, + mxcUrl = get("og:image") as? String + ) + } + + private suspend fun doRequestWithCache(url: String, timestamp: Long?, validityDurationInMillis: Long, strict: Boolean): PreviewUrlData { + // Get data from cache + var dataFromCache: PreviewUrlData? = null + var isCacheValid = false + monarchy.doWithRealm { realm -> + val entity = PreviewUrlCacheEntity.get(realm, url) + dataFromCache = entity?.toDomain() + isCacheValid = entity != null && Date().time < entity.lastUpdatedTimestamp + validityDurationInMillis + } + + val finalDataFromCache = dataFromCache + if (finalDataFromCache != null && isCacheValid) { + return finalDataFromCache + } + + // No cache or outdated cache + val data = try { + doRequest(url, timestamp) + } catch (throwable: Throwable) { + // In case of error, we can return value from cache even if outdated + return finalDataFromCache + ?.takeIf { !strict } + ?: throw throwable + } + + // Store cache + monarchy.awaitTransaction { realm -> + val previewUrlCacheEntity = PreviewUrlCacheEntity.getOrCreate(realm, url) + previewUrlCacheEntity.urlFromServer = data.url + previewUrlCacheEntity.siteName = data.siteName + previewUrlCacheEntity.title = data.title + previewUrlCacheEntity.description = data.description + previewUrlCacheEntity.mxcUrl = data.mxcUrl + + previewUrlCacheEntity.lastUpdatedTimestamp = Date().time + } + + return data + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt new file mode 100644 index 0000000000..6c5dad2422 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetRawPreviewUrlTask : Task { + data class Params( + val url: String, + val timestamp: Long? + ) +} + +internal class DefaultGetRawPreviewUrlTask @Inject constructor( + private val mediaAPI: MediaAPI, + private val eventBus: EventBus +) : GetRawPreviewUrlTask { + + override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict { + return executeRequest(eventBus) { + apiCall = mediaAPI.getPreviewUrlData(params.url, params.timestamp) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt index 821d9b9875..bbb4f1e06a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt @@ -39,5 +39,5 @@ internal interface MediaAPI { * if it does not have the requested version available. */ @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url") - fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Int?): Call + fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): Call } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt index 84a82dc75a..bc58b3f444 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt @@ -16,8 +16,10 @@ package org.matrix.android.sdk.internal.session.media +import dagger.Binds import dagger.Module import dagger.Provides +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.internal.session.SessionScope import retrofit2.Retrofit @@ -34,6 +36,15 @@ internal abstract class MediaModule { } } -// @Binds -// abstract fun bindGetHomeServerCapabilitiesTask(task: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask + @Binds + abstract fun bindMediaService(service: DefaultMediaService): MediaService + + @Binds + abstract fun bindGetRawPreviewUrlTask(task: DefaultGetRawPreviewUrlTask): GetRawPreviewUrlTask + + @Binds + abstract fun bindGetPreviewUrlTask(task: DefaultGetPreviewUrlTask): GetPreviewUrlTask + + @Binds + abstract fun bindClearMediaCacheTask(task: DefaultClearPreviewUrlCacheTask): ClearPreviewUrlCacheTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt new file mode 100644 index 0000000000..dd1a9ead26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity + +/** + * PreviewUrlCacheEntity -> PreviewUrlData + */ +internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData( + url = urlFromServer ?: url, + siteName = siteName, + title = title, + description = description, + mxcUrl = mxcUrl +) From dd150c6d7e78521d640849d88d732266d7302b7d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 18:14:06 +0100 Subject: [PATCH 21/89] Remove unnecessary non-null assertion --- .../android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt | 2 +- .../sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 606f57b467..eb8b8b9730 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -264,7 +264,7 @@ class KeysBackupTest : InstrumentedTest { assertNotNull(decryption) // - Check decryptKeyBackupData() returns stg val sessionData = keysBackup - .decryptKeyBackupData(keyBackupData!!, + .decryptKeyBackupData(keyBackupData, session.olmInboundGroupSession!!.sessionIdentifier(), cryptoTestData.roomId, decryption!!) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index 944d1036d3..b6e5ae7364 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -111,7 +111,7 @@ class KeysBackupTestHelper( Assert.assertTrue(keysBackup.isEnabled) stateObserver.stopAndCheckStates(null) - return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!) + return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version) } /** From be20f9b455d735a066139afe1e66a54e6cf92db3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 18:41:16 +0100 Subject: [PATCH 22/89] PreviewUrl: extract url from Content --- .../session/media/UrlsExtractorTest.kt | 91 +++++++++++++++++++ .../session/media/DefaultMediaService.kt | 17 +++- .../internal/session/media/UrlsExtractor.kt | 40 ++++++++ 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt new file mode 100644 index 0000000000..a3fc876de3 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt @@ -0,0 +1,91 @@ +/* + * 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.test.ext.junit.runners.AndroidJUnit4 +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.InstrumentedTest +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.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType + +@RunWith(AndroidJUnit4::class) +internal class UrlsExtractorTest : InstrumentedTest { + + private val urlsExtractor = UrlsExtractor() + + @Test + fun wrongEventTypeTest() { + createEvent(body = "https://matrix.org") + .copy(type = EventType.STATE_ROOM_GUEST_ACCESS) + .let { urlsExtractor.extract(it) } + .size shouldBeEqualTo 0 + } + + @Test + fun oneUrlTest() { + createEvent(body = "https://matrix.org") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org" + } + } + + @Test + fun oneUrlWithParamTest() { + createEvent(body = "https://matrix.org?foo=bar") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org?foo=bar" + } + } + + @Test + fun oneUrlInlinedTest() { + createEvent(body = "Hello https://matrix.org, how are you?") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org" + } + } + + @Test + fun twoUrlsTest() { + createEvent(body = "https://matrix.org https://example.org") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 2 + result[0] shouldBeEqualTo "https://matrix.org" + result[1] shouldBeEqualTo "https://example.org" + } + } + + private fun createEvent(body: String): Event = Event( + type = EventType.MESSAGE, + content = MessageTextContent( + msgType = MessageType.MSGTYPE_TEXT, + body = body + ).toContent() + ) +} 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 index 846b81ae72..084a88b8cf 100644 --- 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 @@ -16,6 +16,7 @@ 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 @@ -26,12 +27,23 @@ import javax.inject.Inject internal class DefaultMediaService @Inject constructor( private val clearPreviewUrlCacheTask: ClearPreviewUrlCacheTask, private val getPreviewUrlTask: GetPreviewUrlTask, - private val getRawPreviewUrlTask: GetRawPreviewUrlTask + 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 { - TODO("Not yet implemented") + val cacheKey = event.cacheKey() + return extractedUrlsCache.get(cacheKey) + ?: let { + urlsExtractor.extract(event) + .also { extractedUrlsCache.put(cacheKey, it) } + } } + private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}" + override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict { return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp)) } @@ -41,6 +53,7 @@ internal class DefaultMediaService @Inject constructor( } override suspend fun clearCache() { + extractedUrlsCache.evictAll() clearPreviewUrlCacheTask.execute(Unit) } } 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..789c57236b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt @@ -0,0 +1,40 @@ +/* + * 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 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 javax.inject.Inject + +internal class UrlsExtractor @Inject constructor() { + private val urlRegex = Patterns.WEB_URL.toRegex() + + fun extract(event: Event): List { + return event.takeIf { it.getClearType() == EventType.MESSAGE } + ?.getClearContent() + ?.toModel() + ?.body + ?.let { urlRegex.findAll(it) } + ?.map { it.value } + ?.distinct() + ?.toList() + .orEmpty() + } +} From a36d5684b888c72e249e1e5ff85636fe252cfaf7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 18:52:35 +0100 Subject: [PATCH 23/89] Create extension for androidx.collection.LruCache --- .../session/media/DefaultMediaService.kt | 8 ++----- .../android/sdk/internal/util/LruCache.kt | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt 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 index 084a88b8cf..1a400ccfcf 100644 --- 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 @@ -22,6 +22,7 @@ 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( @@ -34,12 +35,7 @@ internal class DefaultMediaService @Inject constructor( private val extractedUrlsCache = LruCache>(1_000) override fun extractUrls(event: Event): List { - val cacheKey = event.cacheKey() - return extractedUrlsCache.get(cacheKey) - ?: let { - urlsExtractor.extract(event) - .also { extractedUrlsCache.put(cacheKey, it) } - } + return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) } } private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}" 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) } +} From fcd9fe7d5a0c230409ac42b9c2644bdf9c5389c4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 3 Dec 2020 19:32:49 +0100 Subject: [PATCH 24/89] PreviewUrl: layout for a single PreviewUrl --- .../res/layout/item_timeline_event_base.xml | 10 +++ vector/src/main/res/layout/url_preview.xml | 72 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 vector/src/main/res/layout/url_preview.xml 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..4be92a9b1c 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -143,6 +143,16 @@ android:addStatesFromChildren="true" android:orientation="vertical"> + + + + + + + + + + + + + + + \ No newline at end of file From 48354c779374be1fea059fcf9084bd231563c184 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 07:46:09 +0100 Subject: [PATCH 25/89] PreviewUrl: Application part - WIP --- .../internal/session/media/UrlsExtractor.kt | 2 + .../app/core/ui/views/PreviewUrlView.kt | 147 ++++++++++++++++++ .../home/room/detail/RoomDetailViewModel.kt | 8 + .../timeline/factory/MessageItemFactory.kt | 6 + .../detail/timeline/item/MessageTextItem.kt | 31 ++++ .../timeline/url/PreviewUrlRetriever.kt | 108 +++++++++++++ .../detail/timeline/url/PreviewUrlUiState.kt | 39 +++++ .../features/media/ImageContentRenderer.kt | 17 +- .../res/layout/item_timeline_event_base.xml | 11 -- .../item_timeline_event_text_message_stub.xml | 26 +++- vector/src/main/res/layout/url_preview.xml | 8 +- 11 files changed, 382 insertions(+), 21 deletions(-) create mode 100755 vector/src/main/java/im/vector/app/core/ui/views/PreviewUrlView.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt 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 index 789c57236b..b6cea04600 100644 --- 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 @@ -21,6 +21,7 @@ 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() { @@ -30,6 +31,7 @@ internal class UrlsExtractor @Inject constructor() { 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 } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/core/ui/views/PreviewUrlView.kt new file mode 100755 index 0000000000..a63273f00a --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/views/PreviewUrlView.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.ui.views + +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.url.PreviewUrlUiState +import im.vector.app.features.media.ImageContentRenderer +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import timber.log.Timber + +/** + * 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 + + var delegate: Delegate? = 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) { + Timber.v("State unchanged") + return + } + Timber.v("Rendering $newState") + + 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?.onUrlClicked(finalState.previewUrlData.url) + else -> Unit + } + } + + // PRIVATE METHODS **************************************************************************************************************************************** + + private fun setupView() { + inflate(context, R.layout.url_preview, this) + ButterKnife.bind(this) + + setOnClickListener(this) + } + + private fun renderHidden() { + isVisible = false + } + + private fun renderLoading() { + // TODO + isVisible = false + } + + private fun renderData(previewUrlData: PreviewUrlData, imageContentRenderer: ImageContentRenderer) { + isVisible = true + titleView.setTextOrHide(previewUrlData.title) + val mxcUrl = previewUrlData.mxcUrl + imageView.isVisible = mxcUrl != null + if (mxcUrl != null) { + imageContentRenderer.render(mxcUrl, imageView) + } + descriptionView.setTextOrHide(previewUrlData.description) + siteView.setTextOrHide(previewUrlData.siteName) + } + + /** + * 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 + } + + /** + * An interface to delegate some actions to another object + */ + interface Delegate { + // TODO + fun onUrlClicked(url: String) + + // TODO + // fun close() + } +} 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..77ed959300 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 @@ -112,6 +113,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val rainbowGenerator: RainbowGenerator, private val session: Session, private val rawService: RawService, + private val previewUrlRetriever: PreviewUrlRetriever, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val stickerPickerActionHandler: StickerPickerActionHandler, private val roomSummaryHolder: RoomSummaryHolder, @@ -1350,6 +1352,12 @@ class RoomDetailViewModel @AssistedInject constructor( override fun onTimelineUpdated(snapshot: List) { timelineEvents.accept(snapshot) + + // PreviewUrl + // TODO Check if URL preview is enable, check if encrypted room, etc. + snapshot.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/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 2b067ccf3f..f3be4337ef 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 @@ -58,6 +58,7 @@ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequest import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.linkify +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.html.CodeVisitor import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor @@ -107,6 +108,7 @@ class MessageItemFactory @Inject constructor( private val defaultItemFactory: DefaultItemFactory, private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, + private val previewUrlRetriever: PreviewUrlRetriever, private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val session: Session) { @@ -424,6 +426,8 @@ class MessageItemFactory @Inject constructor( } .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .searchForPills(isFormatted) + .previewUrlRetriever(previewUrlRetriever) + .imageContentRenderer(imageContentRenderer) .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) @@ -529,6 +533,8 @@ class MessageItemFactory @Inject constructor( } } .leftGuideline(avatarSizeProvider.leftGuideline) + .previewUrlRetriever(previewUrlRetriever) + .imageContentRenderer(imageContentRenderer) .attributes(attributes) .highlighted(highlight) .movementMethod(createLinkMovementMethod(callback)) 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..5ba5496f3d 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,11 @@ import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R +import im.vector.app.core.ui.views.PreviewUrlView 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.media.ImageContentRenderer @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageTextItem : AbsMessageItem() { @@ -37,10 +41,22 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var useBigFont: Boolean = false + @EpoxyAttribute + var previewUrlRetriever: PreviewUrlRetriever? = null + + @EpoxyAttribute + var imageContentRenderer: ImageContentRenderer? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var movementMethod: MovementMethod? = null + private val previewUrlViewUpdater = PreviewUrlViewUpdater() + override fun bind(holder: Holder) { + previewUrlViewUpdater.previewUrlView = holder.previewUrlView + previewUrlViewUpdater.imageContentRenderer = imageContentRenderer + previewUrlRetriever?.addListener(attributes.informationData.eventId, previewUrlViewUpdater) + if (useBigFont) { holder.messageView.textSize = 44F } else { @@ -65,12 +81,27 @@ abstract class MessageTextItem : AbsMessageItem() { holder.messageView.setTextFuture(textFuture) } + override fun unbind(holder: Holder) { + super.unbind(holder) + 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..94df822218 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt @@ -0,0 +1,108 @@ +/* + * 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.core.di.ScreenScope +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 +import javax.inject.Inject + +@ScreenScope +class PreviewUrlRetriever @Inject constructor( + private val session: Session +) { + private val data = mutableMapOf() + private val listeners = mutableMapOf>() + + fun getPreviewUrl(event: Event, coroutineScope: CoroutineScope) { + val eventId = event.eventId ?: return + + val urlToRetrieve = synchronized(data) { + if (data[eventId] == null) { + // Keep only the first URL for the moment + val url = session.mediaService().extractUrls(event).firstOrNull() + if (url == null) { + updateState(eventId, PreviewUrlUiState.NoUrl) + } else { + updateState(eventId, PreviewUrlUiState.Loading) + } + url + } else { + // Already handled + null + } + } + + urlToRetrieve?.let { urlToRetrieve -> + coroutineScope.launch { + runCatching { + session.mediaService().getPreviewUrl( + url = urlToRetrieve, + timestamp = null, + cacheStrategy = CacheStrategy.TtlCache(CACHE_VALIDITY, false) + ) + }.fold( + { + synchronized(data) { + updateState(eventId, PreviewUrlUiState.Data(it)) + } + }, + { + synchronized(data) { + updateState(eventId, PreviewUrlUiState.Error(it)) + } + } + ) + } + } + } + + 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.getOrPut(key) { mutableSetOf() }.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..e411ba3031 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt @@ -0,0 +1,39 @@ +/* + * 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 previewUrlData: PreviewUrlData) : PreviewUrlUiState() +} 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..31492b299a 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 @@ -83,6 +83,19 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: STICKER } + /** + * For url preview + */ + fun render(mxcUrl: String, imageView: ImageView) { + val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() + val imageUrl = contentUrlResolver.resolveFullSize(mxcUrl) ?: return + + GlideApp.with(imageView) + .load(imageUrl) + .placeholder(R.drawable.ic_image) + .into(imageView) + } + /** * For gallery */ @@ -227,7 +240,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: 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 +308,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/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 4be92a9b1c..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" /> @@ -143,16 +142,6 @@ android:addStatesFromChildren="true" android:orientation="vertical"> - - - + android:orientation="vertical"> + + + + + + diff --git a/vector/src/main/res/layout/url_preview.xml b/vector/src/main/res/layout/url_preview.xml index c2a8cea108..fc896b359a 100644 --- a/vector/src/main/res/layout/url_preview.xml +++ b/vector/src/main/res/layout/url_preview.xml @@ -1,10 +1,11 @@ - + android:layout_height="wrap_content" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> - \ No newline at end of file + \ No newline at end of file From fa7b0a24a73ee53dba502af774b6c45ad75d3f8f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 08:01:00 +0100 Subject: [PATCH 26/89] PreviewUrl: Application part - bugfix --- .../home/room/detail/timeline/item/MessageTextItem.kt | 2 +- .../home/room/detail/timeline/url/PreviewUrlRetriever.kt | 9 ++++----- .../home/room/detail/timeline/url}/PreviewUrlView.kt | 8 ++------ .../res/layout/item_timeline_event_text_message_stub.xml | 2 +- vector/src/main/res/layout/url_preview.xml | 1 - 5 files changed, 8 insertions(+), 14 deletions(-) rename vector/src/main/java/im/vector/app/{core/ui/views => features/home/room/detail/timeline/url}/PreviewUrlView.kt (93%) 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 5ba5496f3d..8bb5a84695 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,10 +23,10 @@ import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -import im.vector.app.core.ui.views.PreviewUrlView 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) 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 index 94df822218..a36b2367f6 100644 --- 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 @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail.timeline.url +import im.vector.app.BuildConfig import im.vector.app.core.di.ScreenScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -34,7 +35,7 @@ class PreviewUrlRetriever @Inject constructor( fun getPreviewUrl(event: Event, coroutineScope: CoroutineScope) { val eventId = event.eventId ?: return - val urlToRetrieve = synchronized(data) { + synchronized(data) { if (data[eventId] == null) { // Keep only the first URL for the moment val url = session.mediaService().extractUrls(event).firstOrNull() @@ -48,15 +49,13 @@ class PreviewUrlRetriever @Inject constructor( // Already handled null } - } - - urlToRetrieve?.let { urlToRetrieve -> + }?.let { urlToRetrieve -> coroutineScope.launch { runCatching { session.mediaService().getPreviewUrl( url = urlToRetrieve, timestamp = null, - cacheStrategy = CacheStrategy.TtlCache(CACHE_VALIDITY, false) + cacheStrategy = if (BuildConfig.DEBUG) CacheStrategy.NoCache else CacheStrategy.TtlCache(CACHE_VALIDITY, false) ) }.fold( { diff --git a/vector/src/main/java/im/vector/app/core/ui/views/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt similarity index 93% rename from vector/src/main/java/im/vector/app/core/ui/views/PreviewUrlView.kt rename to vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt index a63273f00a..c29d57539d 100755 --- a/vector/src/main/java/im/vector/app/core/ui/views/PreviewUrlView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.core.ui.views +package im.vector.app.features.home.room.detail.timeline.url import android.content.Context import android.util.AttributeSet @@ -27,10 +27,8 @@ 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.url.PreviewUrlUiState import im.vector.app.features.media.ImageContentRenderer import org.matrix.android.sdk.api.session.media.PreviewUrlData -import timber.log.Timber /** * A View to display a PreviewUrl and some other state @@ -70,10 +68,8 @@ class PreviewUrlView @JvmOverloads constructor( imageContentRenderer: ImageContentRenderer, force: Boolean = false) { if (newState == state && !force) { - Timber.v("State unchanged") return } - Timber.v("Rendering $newState") state = newState @@ -121,7 +117,7 @@ class PreviewUrlView @JvmOverloads constructor( imageContentRenderer.render(mxcUrl, imageView) } descriptionView.setTextOrHide(previewUrlData.description) - siteView.setTextOrHide(previewUrlData.siteName) + siteView.setTextOrHide(previewUrlData.siteName.takeIf { it != previewUrlData.title }) } /** 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 914f4a9d9b..54d677c310 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 @@ -13,7 +13,7 @@ android:textSize="14sp" tools:text="@sample/matrix.json/data/message" /> - Date: Fri, 4 Dec 2020 08:31:14 +0100 Subject: [PATCH 27/89] PreviewUrl: protocol is mandatory (exclude rstp://) --- .../matrix/android/sdk/internal/session/media/UrlsExtractor.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index b6cea04600..b7f9a88845 100644 --- 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 @@ -25,6 +25,7 @@ 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 { @@ -35,6 +36,7 @@ internal class UrlsExtractor @Inject constructor() { ?.body ?.let { urlRegex.findAll(it) } ?.map { it.value } + ?.filter { it.startsWith("https://") || it.startsWith("http://") } ?.distinct() ?.toList() .orEmpty() From 770041eceb5102702b83c2a68af6cd0b3fa06edf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 09:48:48 +0100 Subject: [PATCH 28/89] PreviewUrl: setting and e2e room --- .../features/home/room/detail/RoomDetailViewModel.kt | 11 ++++++++--- .../vector/app/features/settings/VectorPreferences.kt | 9 +++++++++ .../settings/VectorSettingsPreferencesFragment.kt | 6 +++--- .../src/main/res/xml/vector_settings_preferences.xml | 3 +-- 4 files changed, 21 insertions(+), 8 deletions(-) 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 77ed959300..40dbf9627c 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 @@ -1354,9 +1354,14 @@ class RoomDetailViewModel @AssistedInject constructor( timelineEvents.accept(snapshot) // PreviewUrl - // TODO Check if URL preview is enable, check if encrypted room, etc. - snapshot.forEach { - previewUrlRetriever.getPreviewUrl(it.root, viewModelScope) + if (vectorPreferences.showUrlPreviews()) { + withState { state -> + snapshot + .takeIf { state.asyncRoomSummary.invoke()?.isEncrypted == false } + ?.forEach { + previewUrlRetriever.getPreviewUrl(it.root, viewModelScope) + } + } } } 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..58528dc405 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 @@ -64,9 +64,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 +100,8 @@ class VectorSettingsPreferencesFragment @Inject constructor( false } - */ } + */ // update keep medias period findPreference(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY)!!.let { 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" /> Date: Fri, 4 Dec 2020 11:08:06 +0100 Subject: [PATCH 29/89] PreviewUrl: handle click --- .../home/room/detail/RoomDetailFragment.kt | 8 ++++++++ .../detail/timeline/TimelineEventController.kt | 13 ++++++++++++- .../timeline/factory/MessageItemFactory.kt | 2 ++ .../room/detail/timeline/item/MessageTextItem.kt | 6 ++++++ .../detail/timeline/url/PreviewUrlRetriever.kt | 2 +- .../detail/timeline/url/PreviewUrlUiState.kt | 2 +- .../room/detail/timeline/url/PreviewUrlView.kt | 16 +++------------- 7 files changed, 33 insertions(+), 16 deletions(-) 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 3f5e476a5e..5e8d41c545 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 @@ -1652,6 +1652,14 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } + override fun onPreviewUrlClicked(url: String) { + onUrlClicked(url, url) + } + + override fun onPreviewUrlCloseClicked(url: String) { + TODO("Not yet implemented") + } + private fun onShareActionClicked(action: EventSharedAction.Share) { if (action.messageContent is MessageTextContent) { shareText(requireContext(), action.messageContent.body) 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..31c4ceaff5 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 @@ -76,7 +76,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) @@ -118,6 +124,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onUrlLongClicked(url: String): Boolean } + interface PreviewUrlCallback { + fun onPreviewUrlClicked(url: String) + fun onPreviewUrlCloseClicked(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/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index f3be4337ef..96fa055d1f 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 @@ -428,6 +428,7 @@ class MessageItemFactory @Inject constructor( .searchForPills(isFormatted) .previewUrlRetriever(previewUrlRetriever) .imageContentRenderer(imageContentRenderer) + .previewUrlCallback(callback) .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) @@ -535,6 +536,7 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) .previewUrlRetriever(previewUrlRetriever) .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/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index 8bb5a84695..5bb7aff194 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,6 +23,7 @@ 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 @@ -44,6 +45,9 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var previewUrlRetriever: PreviewUrlRetriever? = null + @EpoxyAttribute + var previewUrlCallback: TimelineEventController.PreviewUrlCallback? = null + @EpoxyAttribute var imageContentRenderer: ImageContentRenderer? = null @@ -53,9 +57,11 @@ abstract class MessageTextItem : AbsMessageItem() { 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 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 index a36b2367f6..0153a38719 100644 --- 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 @@ -60,7 +60,7 @@ class PreviewUrlRetriever @Inject constructor( }.fold( { synchronized(data) { - updateState(eventId, PreviewUrlUiState.Data(it)) + updateState(eventId, PreviewUrlUiState.Data(urlToRetrieve, it)) } }, { 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 index e411ba3031..d86633b1e4 100644 --- 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 @@ -35,5 +35,5 @@ sealed class PreviewUrlUiState { data class Error(val throwable: Throwable) : PreviewUrlUiState() // PreviewUrl data - data class Data(val previewUrlData: PreviewUrlData) : PreviewUrlUiState() + data class Data(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 index c29d57539d..58c36a95c9 100755 --- 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 @@ -27,6 +27,7 @@ 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.session.media.PreviewUrlData @@ -51,7 +52,7 @@ class PreviewUrlView @JvmOverloads constructor( @BindView(R.id.url_preview_site) lateinit var siteView: TextView - var delegate: Delegate? = null + var delegate: TimelineEventController.PreviewUrlCallback? = null init { setupView() @@ -85,7 +86,7 @@ class PreviewUrlView @JvmOverloads constructor( override fun onClick(v: View?) { when (val finalState = state) { - is PreviewUrlUiState.Data -> delegate?.onUrlClicked(finalState.previewUrlData.url) + is PreviewUrlUiState.Data -> delegate?.onPreviewUrlClicked(finalState.url) else -> Unit } } @@ -129,15 +130,4 @@ class PreviewUrlView @JvmOverloads constructor( descriptionView.isVisible = false siteView.isVisible = false } - - /** - * An interface to delegate some actions to another object - */ - interface Delegate { - // TODO - fun onUrlClicked(url: String) - - // TODO - // fun close() - } } From 679d9bae1c838c4213e263a199f4093f400206e4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 11:45:05 +0100 Subject: [PATCH 30/89] Fix bad image rendering --- .../home/room/detail/timeline/url/PreviewUrlView.kt | 7 ++----- .../im/vector/app/features/media/ImageContentRenderer.kt | 6 +++--- vector/src/main/res/layout/url_preview.xml | 7 ++++--- vector/src/main/res/values/colors_riotx.xml | 7 +++++++ vector/src/main/res/values/theme_black.xml | 1 + vector/src/main/res/values/theme_dark.xml | 1 + vector/src/main/res/values/theme_light.xml | 1 + 7 files changed, 19 insertions(+), 11 deletions(-) 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 index 58c36a95c9..29701cacbe 100755 --- 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 @@ -29,6 +29,7 @@ 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 /** @@ -112,11 +113,7 @@ class PreviewUrlView @JvmOverloads constructor( private fun renderData(previewUrlData: PreviewUrlData, imageContentRenderer: ImageContentRenderer) { isVisible = true titleView.setTextOrHide(previewUrlData.title) - val mxcUrl = previewUrlData.mxcUrl - imageView.isVisible = mxcUrl != null - if (mxcUrl != null) { - imageContentRenderer.render(mxcUrl, imageView) - } + imageView.isVisible = previewUrlData.mxcUrl?.let { imageContentRenderer.render(it, imageView) }.orFalse() descriptionView.setTextOrHide(previewUrlData.description) siteView.setTextOrHide(previewUrlData.siteName.takeIf { it != previewUrlData.title }) } 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 31492b299a..87315c91cd 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 @@ -86,14 +86,14 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: /** * For url preview */ - fun render(mxcUrl: String, imageView: ImageView) { + fun render(mxcUrl: String, imageView: ImageView): Boolean { val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() - val imageUrl = contentUrlResolver.resolveFullSize(mxcUrl) ?: return + val imageUrl = contentUrlResolver.resolveFullSize(mxcUrl) ?: return false GlideApp.with(imageView) .load(imageUrl) - .placeholder(R.drawable.ic_image) .into(imageView) + return true } /** diff --git a/vector/src/main/res/layout/url_preview.xml b/vector/src/main/res/layout/url_preview.xml index b9f355162e..ad6259a57e 100644 --- a/vector/src/main/res/layout/url_preview.xml +++ b/vector/src/main/res/layout/url_preview.xml @@ -11,7 +11,7 @@ android:id="@+id/url_preview_left_border" android:layout_width="2dp" android:layout_height="0dp" - android:background="#8D99A5" + android:background="?riotx_text_tertiary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -32,10 +32,11 @@ #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 From 9089c549902e513b000c9a119cfe2829f47dcf77 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 11:53:37 +0100 Subject: [PATCH 31/89] Ripple effect --- .../main/res/layout/item_timeline_event_text_message_stub.xml | 1 + 1 file changed, 1 insertion(+) 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 54d677c310..dcff25ecf9 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 @@ -19,6 +19,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginBottom="4dp" + android:foreground="?attr/selectableItemBackground" android:visibility="gone" tools:visibility="visible" /> From c08c6520808718f0244c91c31f2a69efcf10ef4c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 12:41:26 +0100 Subject: [PATCH 32/89] PreviewUrl: handle close (in memory) --- .../home/room/detail/RoomDetailFragment.kt | 6 +++-- .../timeline/TimelineEventController.kt | 2 +- .../timeline/url/PreviewUrlRetriever.kt | 27 +++++++++++++++++-- .../detail/timeline/url/PreviewUrlUiState.kt | 4 ++- .../detail/timeline/url/PreviewUrlView.kt | 17 +++++++++--- .../src/main/res/drawable/ic_close_24dp.xml | 10 +++++++ vector/src/main/res/layout/url_preview.xml | 12 +++++++++ 7 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_close_24dp.xml 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 5e8d41c545..f211e89e7d 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 @@ -215,6 +216,7 @@ class RoomDetailFragment @Inject constructor( private val session: Session, private val avatarRenderer: AvatarRenderer, private val timelineEventController: TimelineEventController, + private val previewUrlRetriever: PreviewUrlRetriever, autoCompleterFactory: AutoCompleter.Factory, private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, @@ -1656,8 +1658,8 @@ class RoomDetailFragment @Inject constructor( onUrlClicked(url, url) } - override fun onPreviewUrlCloseClicked(url: String) { - TODO("Not yet implemented") + override fun onPreviewUrlCloseClicked(eventId: String, url: String) { + previewUrlRetriever.doNotShowPreviewUrlFor(eventId, url) } private fun onShareActionClicked(action: EventSharedAction.Share) { 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 31c4ceaff5..693383c751 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 @@ -126,7 +126,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface PreviewUrlCallback { fun onPreviewUrlClicked(url: String) - fun onPreviewUrlCloseClicked(url: String) + fun onPreviewUrlCloseClicked(eventId: String, url: String) } // Map eventId to adapter position 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 index 0153a38719..b4fbeaf325 100644 --- 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 @@ -32,13 +32,18 @@ class PreviewUrlRetriever @Inject constructor( 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 = session.mediaService().extractUrls(event).firstOrNull() + val url = session.mediaService().extractUrls(event) + .firstOrNull() + ?.takeIf { it !in blockedUrl } if (url == null) { updateState(eventId, PreviewUrlUiState.NoUrl) } else { @@ -60,7 +65,12 @@ class PreviewUrlRetriever @Inject constructor( }.fold( { synchronized(data) { - updateState(eventId, PreviewUrlUiState.Data(urlToRetrieve, it)) + // Blocked after the request has been sent? + if (urlToRetrieve in blockedUrl) { + updateState(eventId, PreviewUrlUiState.NoUrl) + } else { + updateState(eventId, PreviewUrlUiState.Data(eventId, urlToRetrieve, it)) + } } }, { @@ -73,6 +83,19 @@ class PreviewUrlRetriever @Inject constructor( } } + 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 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 index d86633b1e4..a8f8f7b0cb 100644 --- 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 @@ -35,5 +35,7 @@ sealed class PreviewUrlUiState { data class Error(val throwable: Throwable) : PreviewUrlUiState() // PreviewUrl data - data class Data(val url: String, val previewUrlData: PreviewUrlData) : PreviewUrlUiState() + 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 index 29701cacbe..b314344356 100755 --- 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 @@ -53,6 +53,9 @@ class PreviewUrlView @JvmOverloads constructor( @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 { @@ -78,10 +81,10 @@ class PreviewUrlView @JvmOverloads constructor( hideAll() when (newState) { PreviewUrlUiState.Unknown, - PreviewUrlUiState.NoUrl -> renderHidden() - PreviewUrlUiState.Loading -> renderLoading() + PreviewUrlUiState.NoUrl -> renderHidden() + PreviewUrlUiState.Loading -> renderLoading() is PreviewUrlUiState.Error -> renderHidden() - is PreviewUrlUiState.Data -> renderData(newState.previewUrlData, imageContentRenderer) + is PreviewUrlUiState.Data -> renderData(newState.previewUrlData, imageContentRenderer) } } @@ -92,6 +95,13 @@ class PreviewUrlView @JvmOverloads constructor( } } + private fun onCloseClick() { + when (val finalState = state) { + is PreviewUrlUiState.Data -> delegate?.onPreviewUrlCloseClicked(finalState.eventId, finalState.url) + else -> Unit + } + } + // PRIVATE METHODS **************************************************************************************************************************************** private fun setupView() { @@ -99,6 +109,7 @@ class PreviewUrlView @JvmOverloads constructor( ButterKnife.bind(this) setOnClickListener(this) + closeView.setOnClickListener { onCloseClick() } } private fun renderHidden() { 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/url_preview.xml b/vector/src/main/res/layout/url_preview.xml index ad6259a57e..a08c60c663 100644 --- a/vector/src/main/res/layout/url_preview.xml +++ b/vector/src/main/res/layout/url_preview.xml @@ -26,6 +26,7 @@ android:textColor="?riotx_text_primary" android:textSize="14sp" android:textStyle="bold" + app:layout_constraintEnd_toStartOf="@+id/url_preview_close" app:layout_constraintStart_toStartOf="@+id/url_preview_left_border" app:layout_constraintTop_toTopOf="parent" tools:text="Jo Malone denounces her former brand's John Boyega decision" /> @@ -71,4 +72,15 @@ app:layout_constraintTop_toBottomOf="@+id/url_preview_description" tools:text="BBC News" /> + + \ No newline at end of file From 2a19726e497c4dfd4d1a3416a4a54fe02a5f2c6f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 14:19:24 +0100 Subject: [PATCH 33/89] Cleanup and changelog --- CHANGES.md | 1 + .../matrix/android/sdk/internal/session/media/UrlsExtractor.kt | 2 +- .../app/features/settings/VectorSettingsPreferencesFragment.kt | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 06af610ecb..d75b514c7c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ Changes in Element 1.0.12 (2020-XX-XX) Features ✨: - Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428) - Room setting: update join rules and guest access (#2442) + - Url preview (#481) Improvements 🙌: - Add Setting Item to Change PIN (#2462) 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 index b7f9a88845..9d374c3428 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 New Vector Ltd + * 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. 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 58528dc405..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 From 5d3682cd440bccacca52c44923119f61c7b06960 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 14:19:36 +0100 Subject: [PATCH 34/89] More cleanup --- .../app/core/ui/views/JumpToReadMarkerView.kt | 28 +++++++++---------- .../im/vector/app/core/utils/Debouncer.kt | 28 +++++++++---------- .../java/im/vector/app/core/utils/Handler.kt | 28 +++++++++---------- .../helper/MessageInformationDataFactory.kt | 28 +++++++++---------- .../helper/MessageItemAttributesFactory.kt | 28 +++++++++---------- 5 files changed, 65 insertions(+), 75 deletions(-) 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/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/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 From c2c9e37a368a3e2ba98b9031002bf84c6d9006de Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 14:36:11 +0100 Subject: [PATCH 35/89] PreviewUrl: fix layout issue and add more tests --- .../internal/session/media/UrlsExtractorTest.kt | 17 +++++++++++++++++ .../room/detail/timeline/url/PreviewUrlView.kt | 2 +- vector/src/main/res/layout/url_preview.xml | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt index a3fc876de3..9ee84fdfc6 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt @@ -50,6 +50,13 @@ internal class UrlsExtractorTest : InstrumentedTest { } } + @Test + fun withoutProtocolTest() { + createEvent(body = "www.matrix.org") + .let { urlsExtractor.extract(it) } + .size shouldBeEqualTo 0 + } + @Test fun oneUrlWithParamTest() { createEvent(body = "https://matrix.org?foo=bar") @@ -60,6 +67,16 @@ internal class UrlsExtractorTest : InstrumentedTest { } } + @Test + fun oneUrlWithParamsTest() { + createEvent(body = "https://matrix.org?foo=bar&bar=foo") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org?foo=bar&bar=foo" + } + } + @Test fun oneUrlInlinedTest() { createEvent(body = "Hello https://matrix.org, how are you?") 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 index b314344356..fafed08db1 100755 --- 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 @@ -117,7 +117,7 @@ class PreviewUrlView @JvmOverloads constructor( } private fun renderLoading() { - // TODO + // Just hide for the moment isVisible = false } diff --git a/vector/src/main/res/layout/url_preview.xml b/vector/src/main/res/layout/url_preview.xml index a08c60c663..a8c287b471 100644 --- a/vector/src/main/res/layout/url_preview.xml +++ b/vector/src/main/res/layout/url_preview.xml @@ -53,6 +53,7 @@ android:maxLines="4" android:textColor="?riotx_text_secondary" android:textSize="14sp" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/url_preview_left_border" app:layout_constraintTop_toBottomOf="@+id/url_preview_image" tools:text="The British perfumer says removing actor John Boyega from his own advert was “utterly despicable”." /> @@ -68,6 +69,7 @@ android:singleLine="true" android:textColor="?riotx_text_tertiary" android:textSize="14sp" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/url_preview_left_border" app:layout_constraintTop_toBottomOf="@+id/url_preview_description" tools:text="BBC News" /> From 78fe7e5c16cf9c29b115b4ab4a111cf09ddbd4ec Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 14:56:26 +0100 Subject: [PATCH 36/89] No need to create a Set to remove an item from it --- .../home/room/detail/timeline/url/PreviewUrlRetriever.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index b4fbeaf325..174841b599 100644 --- 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 @@ -116,7 +116,7 @@ class PreviewUrlRetriever @Inject constructor( // Called by the Epoxy item during unbinding fun removeListener(key: String, listener: PreviewUrlRetrieverListener) { - listeners.getOrPut(key) { mutableSetOf() }.remove(listener) + listeners[key]?.remove(listener) } interface PreviewUrlRetrieverListener { From 431ac5aa2d98e5c7cc180a18645202b96f44cb40 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Dec 2020 15:23:59 +0100 Subject: [PATCH 37/89] Fix layout issue --- .../main/res/layout/item_timeline_event_text_message_stub.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dcff25ecf9..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 @@ -15,7 +15,7 @@ Date: Mon, 7 Dec 2020 09:55:11 +0100 Subject: [PATCH 38/89] Format files --- .../timeline/factory/MessageItemFactory.kt | 24 +++++++++---------- .../detail/timeline/url/PreviewUrlView.kt | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) 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 96fa055d1f..0a88f81a02 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 @@ -146,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) } } @@ -166,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) @@ -373,7 +373,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) @@ -389,7 +389,7 @@ class MessageItemFactory @Inject constructor( buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) } } - CodeVisitor.Kind.NONE -> { + CodeVisitor.Kind.NONE -> { buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) } } 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 index fafed08db1..9d8f438683 100755 --- 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 @@ -81,10 +81,10 @@ class PreviewUrlView @JvmOverloads constructor( hideAll() when (newState) { PreviewUrlUiState.Unknown, - PreviewUrlUiState.NoUrl -> renderHidden() - PreviewUrlUiState.Loading -> renderLoading() + PreviewUrlUiState.NoUrl -> renderHidden() + PreviewUrlUiState.Loading -> renderLoading() is PreviewUrlUiState.Error -> renderHidden() - is PreviewUrlUiState.Data -> renderData(newState.previewUrlData, imageContentRenderer) + is PreviewUrlUiState.Data -> renderData(newState.previewUrlData, imageContentRenderer) } } From 7869d731d47e2b306a00b728c68abfc58cecbeae Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 7 Dec 2020 10:38:34 +0100 Subject: [PATCH 39/89] Fix the rotate screen issue --- .../features/home/room/detail/RoomDetailAction.kt | 3 +++ .../features/home/room/detail/RoomDetailFragment.kt | 7 +++++-- .../home/room/detail/RoomDetailViewModel.kt | 9 ++++++++- .../room/detail/timeline/TimelineEventController.kt | 3 +++ .../detail/timeline/factory/MessageItemFactory.kt | 6 ++---- .../room/detail/timeline/item/MessageTextItem.kt | 2 ++ .../room/detail/timeline/url/PreviewUrlRetriever.kt | 13 +++++-------- 7 files changed, 28 insertions(+), 15 deletions(-) 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 f211e89e7d..399bf564bf 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 @@ -216,7 +216,6 @@ class RoomDetailFragment @Inject constructor( private val session: Session, private val avatarRenderer: AvatarRenderer, private val timelineEventController: TimelineEventController, - private val previewUrlRetriever: PreviewUrlRetriever, autoCompleterFactory: AutoCompleter.Factory, private val permalinkHandler: PermalinkHandler, private val notificationDrawerManager: NotificationDrawerManager, @@ -1632,6 +1631,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 { @@ -1659,7 +1662,7 @@ class RoomDetailFragment @Inject constructor( } override fun onPreviewUrlCloseClicked(eventId: String, url: String) { - previewUrlRetriever.doNotShowPreviewUrlFor(eventId, url) + roomDetailViewModel.handle(RoomDetailAction.DoNotShowPreviewUrlFor(eventId, url)) } private fun onShareActionClicked(action: EventSharedAction.Share) { 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 40dbf9627c..ed25d41e9f 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 @@ -113,7 +113,6 @@ class RoomDetailViewModel @AssistedInject constructor( private val rainbowGenerator: RainbowGenerator, private val session: Session, private val rawService: RawService, - private val previewUrlRetriever: PreviewUrlRetriever, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val stickerPickerActionHandler: StickerPickerActionHandler, private val roomSummaryHolder: RoomSummaryHolder, @@ -130,6 +129,9 @@ 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 @@ -288,9 +290,14 @@ 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 { 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 693383c751..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 @@ -97,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 { 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 0a88f81a02..cd4f798769 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 @@ -58,7 +58,6 @@ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequest import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.linkify -import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.html.CodeVisitor import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor @@ -108,7 +107,6 @@ class MessageItemFactory @Inject constructor( private val defaultItemFactory: DefaultItemFactory, private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, - private val previewUrlRetriever: PreviewUrlRetriever, private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val session: Session) { @@ -426,7 +424,7 @@ class MessageItemFactory @Inject constructor( } .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .searchForPills(isFormatted) - .previewUrlRetriever(previewUrlRetriever) + .previewUrlRetriever(callback?.getPreviewUrlRetriever()) .imageContentRenderer(imageContentRenderer) .previewUrlCallback(callback) .leftGuideline(avatarSizeProvider.leftGuideline) @@ -534,7 +532,7 @@ class MessageItemFactory @Inject constructor( } } .leftGuideline(avatarSizeProvider.leftGuideline) - .previewUrlRetriever(previewUrlRetriever) + .previewUrlRetriever(callback?.getPreviewUrlRetriever()) .imageContentRenderer(imageContentRenderer) .previewUrlCallback(callback) .attributes(attributes) 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 5bb7aff194..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 @@ -89,6 +89,8 @@ abstract class MessageTextItem : AbsMessageItem() { override fun unbind(holder: Holder) { super.unbind(holder) + previewUrlViewUpdater.previewUrlView = null + previewUrlViewUpdater.imageContentRenderer = null previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater) } 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 index 174841b599..695661feeb 100644 --- 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 @@ -17,18 +17,15 @@ package im.vector.app.features.home.room.detail.timeline.url import im.vector.app.BuildConfig -import im.vector.app.core.di.ScreenScope 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 -import javax.inject.Inject -@ScreenScope -class PreviewUrlRetriever @Inject constructor( - private val session: Session -) { +class PreviewUrlRetriever(session: Session) { + private val mediaService = session.mediaService() + private val data = mutableMapOf() private val listeners = mutableMapOf>() @@ -41,7 +38,7 @@ class PreviewUrlRetriever @Inject constructor( synchronized(data) { if (data[eventId] == null) { // Keep only the first URL for the moment - val url = session.mediaService().extractUrls(event) + val url = mediaService.extractUrls(event) .firstOrNull() ?.takeIf { it !in blockedUrl } if (url == null) { @@ -57,7 +54,7 @@ class PreviewUrlRetriever @Inject constructor( }?.let { urlToRetrieve -> coroutineScope.launch { runCatching { - session.mediaService().getPreviewUrl( + mediaService.getPreviewUrl( url = urlToRetrieve, timestamp = null, cacheStrategy = if (BuildConfig.DEBUG) CacheStrategy.NoCache else CacheStrategy.TtlCache(CACHE_VALIDITY, false) From c7efd1feb99179585e865affa866d680a96fefe2 Mon Sep 17 00:00:00 2001 From: aqulu Date: Tue, 8 Dec 2020 19:04:30 +0900 Subject: [PATCH 40/89] Convert StateService to suspend functions Signed-off-by: aqulu --- CHANGES.md | 2 +- matrix-sdk-android-rx/build.gradle | 1 + .../java/org/matrix/android/sdk/rx/RxRoom.kt | 35 +++--- .../api/session/room/state/StateService.kt | 16 +-- .../session/room/state/DefaultStateService.kt | 110 +++++++----------- .../home/room/detail/RoomDetailViewModel.kt | 25 ++-- .../RoomMemberProfileViewModel.kt | 4 +- .../roomprofile/alias/RoomAliasViewModel.kt | 11 +- .../members/RoomMemberListViewModel.kt | 3 +- .../features/widgets/WidgetPostAPIHandler.kt | 39 +++++-- 10 files changed, 124 insertions(+), 122 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b16a6690bc..b8ed81efb7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ Translations 🗣: - SDK API changes ⚠️: - - + - StateService now exposes suspendable function instead of using MatrixCallback. Build 🧱: - Upgrade some dependencies and Kotlin version diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 37f41d0a2a..a99b5856ba 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version" // Paging implementation "androidx.paging:paging-runtime-ktx:2.1.2" diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index bf4bcacc31..b938f60e39 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -17,14 +17,20 @@ package org.matrix.android.sdk.rx import android.net.Uri +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import kotlinx.coroutines.rx2.rxCompletable 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.identity.ThreePid import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.ReadReceipt 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.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState @@ -32,11 +38,6 @@ import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional -import io.reactivex.Completable -import io.reactivex.Observable -import io.reactivex.Single -import org.matrix.android.sdk.api.session.room.model.GuestAccess -import org.matrix.android.sdk.api.session.room.model.RoomJoinRules class RxRoom(private val room: Room) { @@ -121,28 +122,28 @@ class RxRoom(private val room: Room) { room.invite3pid(threePid, it) } - fun updateTopic(topic: String): Completable = completableBuilder { - room.updateTopic(topic, it) + fun updateTopic(topic: String): Completable = rxCompletable { + room.updateTopic(topic) } - fun updateName(name: String): Completable = completableBuilder { - room.updateName(name, it) + fun updateName(name: String): Completable = rxCompletable { + room.updateName(name) } - fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder { - room.updateHistoryReadability(readability, it) + fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = rxCompletable { + room.updateHistoryReadability(readability) } - fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = completableBuilder { - room.updateJoinRule(joinRules, guestAccess, it) + fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = rxCompletable { + room.updateJoinRule(joinRules, guestAccess) } - fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder { - room.updateAvatar(avatarUri, fileName, it) + fun updateAvatar(avatarUri: Uri, fileName: String): Completable = rxCompletable { + room.updateAvatar(avatarUri, fileName) } - fun deleteAvatar(): Completable = completableBuilder { - room.deleteAvatar(it) + fun deleteAvatar(): Completable = rxCompletable { + room.deleteAvatar() } } 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..98dde5839f 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 @@ -33,41 +33,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/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..607784b48f 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,7 @@ 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 kotlinx.coroutines.withContext 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,20 +32,14 @@ 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.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, @@ -73,45 +67,41 @@ 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 { - val params = SendStateTask.Params( - roomId = roomId, - stateKey = stateKey, - eventType = eventType, - body = body - ) - return sendStateTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + body: JsonDict + ) { + withContext(coroutineDispatchers.main) { + val params = SendStateTask.Params( + roomId = roomId, + stateKey = stateKey, + eventType = eventType, + body = body + ) + + 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 +113,52 @@ 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) { + override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) { + withContext(coroutineDispatchers.main) { if (joinRules != null) { - awaitCallback { - sendStateEvent( - eventType = EventType.STATE_ROOM_JOIN_RULES, - body = RoomJoinRulesContent(joinRules).toContent(), - callback = it, - stateKey = null - ) - } + sendStateEvent( + eventType = EventType.STATE_ROOM_JOIN_RULES, + body = RoomJoinRulesContent(joinRules).toContent(), + stateKey = null + ) } if (guestAccess != null) { - awaitCallback { - sendStateEvent( - eventType = EventType.STATE_ROOM_GUEST_ACCESS, - body = RoomGuestAccessContent(guestAccess).toContent(), - callback = it, - 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, + eventType = EventType.STATE_ROOM_GUEST_ACCESS, + body = RoomGuestAccessContent(guestAccess).toContent(), stateKey = null ) } } } - override fun deleteAvatar(callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateAvatar(avatarUri: Uri, fileName: String) { + withContext(coroutineDispatchers.main) { + val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg") + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = mapOf("url" to response.contentUri), + stateKey = null + ) + } + } + + override suspend fun deleteAvatar() { + sendStateEvent( eventType = EventType.STATE_ROOM_AVATAR, body = emptyMap(), - callback = callback, stateKey = null ) } 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..5e414422f7 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 @@ -292,9 +292,7 @@ class RoomDetailViewModel @AssistedInject constructor( 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) } 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..af0972913a 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 @@ -301,21 +301,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..fe8ed63cce 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 @@ -197,8 +197,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/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index a4d759250d..fbd08b0c9f 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,8 @@ 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.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 +312,19 @@ 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) - ) + + GlobalScope.launch { + try { + room.sendStateEvent( + eventType = EventType.PLUMBING, + stateKey = null, + body = params + ) + widgetPostAPIMediator.sendSuccess(eventData) + } catch (failure: Exception) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData) + } + } } /** @@ -333,12 +342,18 @@ 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) - ) + GlobalScope.launch { + try { + room.sendStateEvent( + eventType = EventType.BOT_OPTIONS, + stateKey = stateKey, + body = content + ) + widgetPostAPIMediator.sendSuccess(eventData) + } catch (failure: Exception) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData) + } + } } /** From f3bc39a0c57c7f28d71313f6fa26a8e24164de49 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 11:14:55 +0100 Subject: [PATCH 41/89] Cleanup --- .../android/sdk/internal/session/sync/job/SyncThread.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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) } From 28bfea6af0f611ab40981b0b2b8181da21108556 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 11:31:16 +0100 Subject: [PATCH 42/89] This code is for debug build (see the path), so no need to check again --- .../interceptors/FormattedJsonHttpLogger.kt | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt index 630f6f1e29..849a464867 100644 --- a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -38,31 +38,28 @@ class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger { */ @Synchronized override fun log(@NonNull message: String) { - // In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG - if (BuildConfig.DEBUG) { - Timber.v(message) + Timber.v(message) - if (message.startsWith("{")) { - // JSON Detected - try { - val o = JSONObject(message) - logJson(o.toString(INDENT_SPACE)) - } catch (e: JSONException) { - // Finally this is not a JSON string... - Timber.e(e) - } - } else if (message.startsWith("[")) { - // JSON Array detected - try { - val o = JSONArray(message) - logJson(o.toString(INDENT_SPACE)) - } catch (e: JSONException) { - // Finally not JSON... - Timber.e(e) - } + if (message.startsWith("{")) { + // JSON Detected + try { + val o = JSONObject(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally this is not a JSON string... + Timber.e(e) + } + } else if (message.startsWith("[")) { + // JSON Array detected + try { + val o = JSONArray(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally not JSON... + Timber.e(e) } - // Else not a json string to log } + // Else not a json string to log } private fun logJson(formattedJson: String) { From a0c8a8e97ce5e7739f9881b9e7bfadcd7663dc5b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 11:36:12 +0100 Subject: [PATCH 43/89] Log HTTP requests and responses in production (level BASIC, i.e. without any private data) --- CHANGES.md | 1 + gradle.properties | 2 +- matrix-sdk-android/build.gradle | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1005040328..12cfd8ef83 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,7 @@ Test: Other changes: - Remove "Status.im" theme #2424 + - Log HTTP requests and responses in production (level BASIC, i.e. without any private data) Changes in Element 1.0.11 (2020-11-27) =================================================== diff --git a/gradle.properties b/gradle.properties index b3f11e08a3..200866be25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m org.gradle.vfs.watch=true vector.debugPrivateData=false -vector.httpLogLevel=NONE +vector.httpLogLevel=BASIC # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above #vector.debugPrivateData=true diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index d961560c17..7f0d5c1bbf 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -63,7 +63,7 @@ android { release { buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" - buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC" } } From b43f3b3b6aec9413b0f0c165dad79d0ee330023a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 11:57:18 +0100 Subject: [PATCH 44/89] Log some details about the request which has failed --- .../org/matrix/android/sdk/internal/network/Request.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 From dda2685bd8693d5317866c69d9c21e7c76c85b35 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 13:33:01 +0100 Subject: [PATCH 45/89] Upgrade Realm dependency to 10.1.2 --- CHANGES.md | 1 + matrix-sdk-android/build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 12cfd8ef83..b0be0a1145 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ SDK API changes ⚠️: Build 🧱: - Upgrade some dependencies and Kotlin version - Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable) + - Upgrade Realm dependency to 10.1.2 Test: - diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 7f0d5c1bbf..519b8439c9 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath "io.realm:realm-gradle-plugin:10.0.0" + classpath "io.realm:realm-gradle-plugin:10.1.2" } } From 416f57b1d791e71722d0847ba4549c328e0bab5e Mon Sep 17 00:00:00 2001 From: aqulu Date: Tue, 8 Dec 2020 22:02:00 +0900 Subject: [PATCH 46/89] Fix failing test compilation Signed-off-by: aqulu --- .../crypto/encryption/EncryptionTest.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt index e42059c639..da5e90abdd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt @@ -17,13 +17,13 @@ package org.matrix.android.sdk.internal.crypto.encryption import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.Room @@ -57,13 +57,14 @@ class EncryptionTest : InstrumentedTest { @Test fun test_EncryptionStateEvent() { performTest(roomShouldBeEncrypted = true) { room -> - // Send an encryption Event as a State Event - room.sendStateEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - stateKey = null, - body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent(), - callback = NoOpMatrixCallback() - ) + runBlocking { + // Send an encryption Event as a State Event + room.sendStateEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + stateKey = null, + body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) + } } } From c889deaab1dccd2207612a00b2c9b0112a4bb6f5 Mon Sep 17 00:00:00 2001 From: aqulu Date: Tue, 8 Dec 2020 22:02:49 +0900 Subject: [PATCH 47/89] Remove unused imports Signed-off-by: aqulu --- .../matrix/android/sdk/api/session/room/state/StateService.kt | 2 -- .../vector/app/features/roomprofile/alias/RoomAliasViewModel.kt | 1 - .../app/features/roomprofile/members/RoomMemberListViewModel.kt | 1 - 3 files changed, 4 deletions(-) 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 98dde5839f..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 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 af0972913a..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 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 fe8ed63cce..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 From 19d421df84485effcdc6d5f3ec9fc018f5185f35 Mon Sep 17 00:00:00 2001 From: aqulu Date: Tue, 8 Dec 2020 22:06:21 +0900 Subject: [PATCH 48/89] Remove coroutine context change for sendStateEvent Signed-off-by: aqulu --- .../session/room/state/DefaultStateService.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) 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 607784b48f..f71b8868ed 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 @@ -72,16 +72,13 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private stateKey: String?, body: JsonDict ) { - withContext(coroutineDispatchers.main) { - val params = SendStateTask.Params( - roomId = roomId, - stateKey = stateKey, - eventType = eventType, - body = body - ) - - sendStateTask.execute(params) - } + val params = SendStateTask.Params( + roomId = roomId, + stateKey = stateKey, + eventType = eventType, + body = body + ) + sendStateTask.execute(params) } override suspend fun updateTopic(topic: String) { From 40b9f031325cac0a7474a3706341318e62d48dc4 Mon Sep 17 00:00:00 2001 From: aqulu Date: Tue, 8 Dec 2020 22:11:35 +0900 Subject: [PATCH 49/89] Remove explicit coroutine context changes Signed-off-by: aqulu --- .../session/room/state/DefaultStateService.kt | 45 ++++++++----------- 1 file changed, 19 insertions(+), 26 deletions(-) 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 f71b8868ed..78663e8ce2 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 kotlinx.coroutines.withContext 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 @@ -36,12 +35,10 @@ import org.matrix.android.sdk.api.util.JsonDict 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.util.MatrixCoroutineDispatchers internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, private val stateEventDataSource: StateEventDataSource, private val sendStateTask: SendStateTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, private val fileUploader: FileUploader, private val addRoomAliasTask: AddRoomAliasTask ) : StateService { @@ -123,33 +120,29 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private } override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) { - withContext(coroutineDispatchers.main) { - 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 - ) - } + 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 suspend fun updateAvatar(avatarUri: Uri, fileName: String) { - withContext(coroutineDispatchers.main) { - val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg") - sendStateEvent( - eventType = EventType.STATE_ROOM_AVATAR, - body = mapOf("url" to response.contentUri), - stateKey = null - ) - } + val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg") + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = mapOf("url" to response.contentUri), + stateKey = null + ) } override suspend fun deleteAvatar() { From 7152dead1da6272836601e18986b941fb985cf02 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 16:47:29 +0100 Subject: [PATCH 50/89] Rename method --- .../org/matrix/android/sdk/internal/session/SessionModule.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..f3a9fc59e3 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 @@ -169,8 +169,8 @@ internal abstract class SessionModule { @JvmStatic @Provides @SessionDownloadsDirectory - fun providesCacheDir(@SessionId sessionId: String, - context: Context): File { + fun providesDownloadsCacheDir(@SessionId sessionId: String, + context: Context): File { return File(context.cacheDir, "downloads/$sessionId") } From 24a9ddaa5e3aad9f42550948c87b1e78d85eee04 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 17:20:40 +0100 Subject: [PATCH 51/89] FileService: remove useless FileService.DownloadMode --- CHANGES.md | 2 +- .../sdk/api/session/file/FileService.kt | 18 --------------- .../sdk/internal/di/MatrixComponent.kt | 3 --- .../android/sdk/internal/di/MatrixModule.kt | 7 ------ .../internal/session/DefaultFileService.kt | 23 ++----------------- .../sdk/internal/session/SessionModule.kt | 5 ++-- .../app/core/glide/VectorGlideModelLoader.kt | 2 -- .../home/room/detail/RoomDetailFragment.kt | 3 --- .../home/room/detail/RoomDetailViewModel.kt | 2 -- .../features/media/BaseAttachmentProvider.kt | 1 - .../media/DataAttachmentRoomProvider.kt | 1 - .../media/RoomEventsAttachmentProvider.kt | 1 - .../features/media/VideoContentRenderer.kt | 3 --- .../uploads/RoomUploadsViewModel.kt | 3 --- 14 files changed, 6 insertions(+), 68 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b0be0a1145..7ba7b7cb76 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,7 +18,7 @@ Translations 🗣: - SDK API changes ⚠️: - - + - FileService: remove useless FileService.DownloadMode Build 🧱: - Upgrade some dependencies and Kotlin version diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index 31f016be14..dd592d84a3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -27,23 +27,6 @@ import java.io.File */ interface FileService { - enum class DownloadMode { - /** - * Download file in external storage - */ - TO_EXPORT, - - /** - * Download file in cache - */ - FOR_INTERNAL_USE, - - /** - * Download file in file provider path - */ - FOR_EXTERNAL_SHARE - } - enum class FileState { IN_CACHE, DOWNLOADING, @@ -55,7 +38,6 @@ interface FileService { * Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision. */ fun downloadFile( - downloadMode: DownloadMode, id: String, fileName: String, mimeType: String?, 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/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 861ae7c7ee..d71c3262c2 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 @@ -29,8 +29,6 @@ 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 @@ -54,10 +52,6 @@ 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, @@ -81,8 +75,7 @@ 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, + override fun downloadFile(id: String, fileName: String, mimeType: String?, url: String?, @@ -162,7 +155,7 @@ internal class DefaultFileService @Inject constructor( Timber.v("## FileService: cache hit for $url") } - Try.just(copyFile(destFile, downloadMode)) + Try.just(destFile) } }.fold({ callback.onFailure(it) @@ -232,18 +225,6 @@ internal class DefaultFileService @Inject constructor( 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 { 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 f3a9fc59e3..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 @@ -170,8 +171,8 @@ internal abstract class SessionModule { @Provides @SessionDownloadsDirectory fun providesDownloadsCacheDir(@SessionId sessionId: String, - context: Context): File { - return File(context.cacheDir, "downloads/$sessionId") + @CacheDirectory cacheFile: File): File { + return File(cacheFile, "downloads/$sessionId") } @JvmStatic 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..cf40926ba4 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,7 +109,6 @@ 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, 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 3f5e476a5e..bbce180e80 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 @@ -174,7 +174,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 @@ -1657,7 +1656,6 @@ class RoomDetailFragment @Inject constructor( 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, @@ -1692,7 +1690,6 @@ 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, 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..a13ee3be62 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 @@ -69,7 +69,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 @@ -1033,7 +1032,6 @@ 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, 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..5f61ca36e4 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,7 +153,6 @@ 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, 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..6f58c1a4f3 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 @@ -77,7 +77,6 @@ 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, 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..9b895dbc4d 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 @@ -125,7 +125,6 @@ class RoomEventsAttachmentProvider( as? MessageWithAttachmentContent ?: return@let fileService.downloadFile( - downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, id = timelineEvent.eventId, fileName = messageContent.body, mimeType = messageContent.mimeType, 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..35375bc8ce 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,7 +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, @@ -116,7 +114,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, 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..95d7ce8e93 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,7 +30,6 @@ 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 @@ -134,7 +133,6 @@ 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(), @@ -155,7 +153,6 @@ 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, From 8e11ba21edfe1283b6055ac36bbbdac352fa27f5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 17:37:24 +0100 Subject: [PATCH 52/89] Glide: No Disk cache for encrypted images --- .../im/vector/app/features/media/ImageContentRenderer.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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..cf214b391a 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 @@ -129,6 +130,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 +185,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: GlideApp .with(imageView) .load(data) + .diskCacheStrategy(DiskCacheStrategy.NONE) } else { // Clear image val resolvedUrl = resolveUrl(data) @@ -214,14 +217,16 @@ 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() From 42ab7f1b4f8098cb18f24e41cbb9331ce70e6896 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 17:42:42 +0100 Subject: [PATCH 53/89] Add space between image and text And remove useless `apply` block --- .../timeline/factory/EncryptedItemFactory.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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" From 237cb63fc2b21674e6e09ff064e451d4007f8ab3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 18:04:42 +0100 Subject: [PATCH 54/89] Small formatting --- .../internal/session/DefaultFileService.kt | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) 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 d71c3262c2..37b27cdbae 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,13 @@ 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 okio.buffer +import okio.sink +import okio.source import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver @@ -36,13 +43,6 @@ import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers 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 @@ -157,30 +157,33 @@ internal class DefaultFileService @Inject constructor( Try.just(destFile) } - }.fold({ - callback.onFailure(it) - // 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[unwrappedUrl]?.also { + ongoing.remove(unwrappedUrl) + } + } + toNotify?.forEach { otherCallbacks -> + tryOrNull { otherCallbacks.onFailure(throwable) } + } + }, + { file -> + callback.onSuccess(file) + // notify concurrent requests + val toNotify = synchronized(ongoing) { + ongoing[unwrappedUrl]?.also { + ongoing.remove(unwrappedUrl) + } + } + Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") + toNotify?.forEach { otherCallbacks -> + tryOrNull { otherCallbacks.onSuccess(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) - } - } - Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") - toNotify?.forEach { otherCallbacks -> - tryOrNull { otherCallbacks.onSuccess(file) } - } - }) + ) }.toCancelable() } From 62791e4b36df0020af82fb2098f5bd12d5c3f5f1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 18:35:17 +0100 Subject: [PATCH 55/89] Encrypted files: store decrypted file in a dedicated folder --- .../sdk/api/session/file/FileService.kt | 7 +- .../internal/session/DefaultFileService.kt | 65 +++++++++++++------ 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index dd592d84a3..e13aed628c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -60,10 +60,15 @@ interface FileService { fun fileState(mxcUrl: String, mimeType: String?): FileState /** - * Clears all the files downloaded by the service + * Clears all the files downloaded by the service, including decrypted files */ fun clearCache() + /** + * Clears all the decrypted files by the service + */ + fun clearDecryptedCache() + /** * Get size of cached files */ 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 37b27cdbae..062d09e101 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 @@ -63,7 +63,15 @@ internal class DefaultFileService @Inject constructor( private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName()) - private val downloadFolder = File(sessionCacheDirectory, "MF") + // Folder to store downloaded file (not decrypted) + private val legacyFolder = File(sessionCacheDirectory, "MF") + private val downloadFolder = File(sessionCacheDirectory, "F") + private val decryptedFolder = File(downloadFolder, "D") + + init { + // Clear the legacy downloaded files + legacyFolder.deleteRecursively() + } /** * Retain ongoing downloads to avoid re-downloading and already downloading file @@ -103,8 +111,8 @@ 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 @@ -134,29 +142,42 @@ 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(), destFile) + response.close() } else { Timber.v("## FileService: cache hit for $url") } Try.just(destFile) } + }.flatMap { downloadedFile -> + // Decrypt if necessary + if (elementToDecrypt != null) { + val decryptedFile = File(decryptedFolder, fileForUrl(unwrappedUrl, mimeType)) + + if (!decryptedFile.exists()) { + Timber.v("## FileService: decrypt file") + val decryptSuccess = decryptedFile.outputStream().buffered().use { outputStream -> + downloadedFile.inputStream().use { inputStream -> + 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(decryptedFile) + } else { + // Clear file + Try.just(downloadedFile) + } }.fold( { throwable -> callback.onFailure(throwable) @@ -240,4 +261,8 @@ internal class DefaultFileService @Inject constructor( override fun clearCache() { downloadFolder.deleteRecursively() } + + override fun clearDecryptedCache() { + decryptedFolder.deleteRecursively() + } } From 7057b2970b4a84acbcce1fc31f6b92bcba6f4f50 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Dec 2020 19:31:29 +0100 Subject: [PATCH 56/89] Improve FileService API: add facility methods to deal with MessageWithAttachment object --- .../sdk/api/session/file/FileService.kt | 50 +++++++++++++++++-- .../internal/session/DefaultFileService.kt | 24 ++++++--- .../home/room/detail/RoomDetailFragment.kt | 10 +--- .../home/room/detail/RoomDetailViewModel.kt | 13 ++--- .../timeline/factory/MessageItemFactory.kt | 4 +- .../uploads/RoomUploadsViewModel.kt | 10 +--- 6 files changed, 76 insertions(+), 35 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index e13aed628c..d3327ba920 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -18,8 +18,12 @@ package org.matrix.android.sdk.api.session.file import android.net.Uri import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent +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.util.Cancelable import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import java.io.File /** @@ -45,19 +49,59 @@ interface FileService { elementToDecrypt: ElementToDecrypt?, callback: MatrixCallback): Cancelable - fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean + fun downloadFile( + id: String, + messageContent: MessageWithAttachmentContent, + callback: MatrixCallback): Cancelable = + downloadFile( + id = id, + fileName = messageContent.getFileName(), + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + callback = callback + ) + + fun isFileInCache(mxcUrl: String?, + mimeType: String?, + elementToDecrypt: ElementToDecrypt? + ): Boolean + + fun isFileInCache(messageContent: MessageWithAttachmentContent) = + isFileInCache( + mxcUrl = messageContent.getFileUrl(), + mimeType = messageContent.mimeType, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) /** * 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) */ - fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? + fun getTemporarySharableURI(mxcUrl: String?, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): Uri? + + fun getTemporarySharableURI(messageContent: MessageWithAttachmentContent): Uri? = + getTemporarySharableURI( + mxcUrl = messageContent.getFileUrl(), + mimeType = messageContent.mimeType, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt() + ) /** * Get information on the given file. * Mimetype should be the same one as passed to downloadFile (limitation for now) */ - fun fileState(mxcUrl: String, mimeType: String?): FileState + fun fileState(mxcUrl: String?, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): FileState + + fun fileState(messageContent: MessageWithAttachmentContent): FileState = + fileState( + mxcUrl = messageContent.getFileUrl(), + mimeType = messageContent.mimeType, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt() + ) /** * Clears all the files downloaded by the service, including decrypted files 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 062d09e101..1e5dff107e 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 @@ -225,12 +225,23 @@ internal class DefaultFileService @Inject constructor( return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName() } - override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean { - return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists() + override fun isFileInCache(mxcUrl: String?, mimeType: String?, elementToDecrypt: ElementToDecrypt?): Boolean { + return fileState(mxcUrl, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE } - override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState { - if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE + private fun getClearFile(mxcUrl: String, mimeType: String?, elementToDecrypt: ElementToDecrypt?): File { + return if (elementToDecrypt == null) { + // Clear file + File(downloadFolder, fileForUrl(mxcUrl, mimeType)) + } else { + // Encrypted file + File(decryptedFolder, fileForUrl(mxcUrl, mimeType)) + } + } + + override fun fileState(mxcUrl: String?, mimeType: String?, elementToDecrypt: ElementToDecrypt?): FileService.FileState { + mxcUrl ?: return FileService.FileState.UNKNOWN + if (getClearFile(mxcUrl, mimeType, elementToDecrypt).exists()) return FileService.FileState.IN_CACHE val isDownloading = synchronized(ongoing) { ongoing[mxcUrl] != null } @@ -241,10 +252,11 @@ 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?, 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 = getClearFile(mxcUrl, mimeType, elementToDecrypt) if (!targetFile.exists()) return null return FileProvider.getUriForFile(context, authority, targetFile) } 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 bbce180e80..a229f72755 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 @@ -1657,10 +1657,7 @@ class RoomDetailFragment @Inject constructor( } else if (action.messageContent is MessageWithAttachmentContent) { session.fileService().downloadFile( 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) { @@ -1691,10 +1688,7 @@ class RoomDetailFragment @Inject constructor( } session.fileService().downloadFile( 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) { 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 a13ee3be62..86f22a55ad 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 @@ -1009,10 +1009,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( @@ -1023,7 +1023,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,10 +1033,7 @@ class RoomDetailViewModel @AssistedInject constructor( } else { session.fileService().downloadFile( 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( 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..213c50b6ac 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 @@ -204,7 +204,7 @@ class MessageItemFactory @Inject constructor( return MessageFileItem_() .attributes(attributes) .izLocalFile(fileUrl.isLocalFile()) - .izDownloaded(session.fileService().isFileInCache(fileUrl, messageContent.mimeType)) + .izDownloaded(session.fileService().isFileInCache(fileUrl, messageContent.mimeType, messageContent.encryptedFileInfo?.toElementToDecrypt())) .mxcUrl(fileUrl) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) @@ -264,7 +264,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) 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 95d7ce8e93..bd37cecd56 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 @@ -134,10 +134,7 @@ class RoomUploadsViewModel @AssistedInject constructor( val file = awaitCallback { session.fileService().downloadFile( 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 ) } @@ -154,10 +151,7 @@ class RoomUploadsViewModel @AssistedInject constructor( val file = awaitCallback { session.fileService().downloadFile( 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)) From ed822becc68c16e03330b0139a7287861ccf72de Mon Sep 17 00:00:00 2001 From: aqulu Date: Wed, 9 Dec 2020 08:39:00 +0900 Subject: [PATCH 57/89] Fix try-catch behavior of sendStateEvent actions Signed-off-by: aqulu --- .../features/widgets/WidgetPostAPIHandler.kt | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) 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 fbd08b0c9f..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 @@ -22,6 +22,7 @@ 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 @@ -312,18 +313,12 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo val params = HashMap() params["status"] = status - - GlobalScope.launch { - try { - room.sendStateEvent( - eventType = EventType.PLUMBING, - stateKey = null, - body = params - ) - widgetPostAPIMediator.sendSuccess(eventData) - } catch (failure: Exception) { - widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData) - } + launchWidgetAPIAction(widgetPostAPIMediator, eventData) { + room.sendStateEvent( + eventType = EventType.PLUMBING, + stateKey = null, + body = params + ) } } @@ -342,17 +337,13 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo Timber.d(description) val content = eventData["content"] as JsonDict val stateKey = "_$userId" - GlobalScope.launch { - try { - room.sendStateEvent( - eventType = EventType.BOT_OPTIONS, - stateKey = stateKey, - body = content - ) - widgetPostAPIMediator.sendSuccess(eventData) - } catch (failure: Exception) { - widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData) - } + + launchWidgetAPIAction(widgetPostAPIMediator, eventData) { + room.sendStateEvent( + eventType = EventType.BOT_OPTIONS, + stateKey = stateKey, + body = content + ) } } @@ -471,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) + } + ) + } + } } From ca7796114cb56aac7f645c4c20e70082403b531a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Dec 2020 10:50:21 +0100 Subject: [PATCH 58/89] DefaultFileService: better management of the files and the filenames --- .../sdk/api/session/file/FileService.kt | 6 + .../internal/session/DefaultFileService.kt | 141 ++++++++++++------ .../session/content/UploadContentWorker.kt | 3 +- .../android/sdk/internal/util/FileSaver.kt | 3 + .../timeline/factory/MessageItemFactory.kt | 8 +- 5 files changed, 110 insertions(+), 51 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index d3327ba920..d0f53f25de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -63,6 +63,7 @@ interface FileService { ) fun isFileInCache(mxcUrl: String?, + fileName: String, mimeType: String?, elementToDecrypt: ElementToDecrypt? ): Boolean @@ -70,6 +71,7 @@ interface FileService { fun isFileInCache(messageContent: MessageWithAttachmentContent) = isFileInCache( mxcUrl = messageContent.getFileUrl(), + fileName = messageContent.getFileName(), mimeType = messageContent.mimeType, elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) @@ -78,12 +80,14 @@ interface FileService { * (if not other app won't be able to access it) */ fun getTemporarySharableURI(mxcUrl: String?, + fileName: String, mimeType: String?, elementToDecrypt: ElementToDecrypt?): Uri? fun getTemporarySharableURI(messageContent: MessageWithAttachmentContent): Uri? = getTemporarySharableURI( mxcUrl = messageContent.getFileUrl(), + fileName = messageContent.getFileName(), mimeType = messageContent.mimeType, elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt() ) @@ -93,12 +97,14 @@ interface FileService { * Mimetype should be the same one as passed to downloadFile (limitation for now) */ fun fileState(mxcUrl: String?, + fileName: String, mimeType: String?, elementToDecrypt: ElementToDecrypt?): FileState fun fileState(messageContent: MessageWithAttachmentContent): FileState = fileState( mxcUrl = messageContent.getFileUrl(), + fileName = messageContent.getFileName(), mimeType = messageContent.mimeType, elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt() ) 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 1e5dff107e..006ced8530 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 @@ -25,9 +25,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request -import okio.buffer -import okio.sink -import okio.source import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver @@ -41,13 +38,12 @@ import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProg 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 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( @@ -61,8 +57,6 @@ internal class DefaultFileService @Inject constructor( private val taskExecutor: TaskExecutor ) : FileService { - private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName()) - // Folder to store downloaded file (not decrypted) private val legacyFolder = File(sessionCacheDirectory, "MF") private val downloadFolder = File(sessionCacheDirectory, "F") @@ -89,21 +83,21 @@ internal class DefaultFileService @Inject constructor( 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 } } @@ -117,9 +111,9 @@ internal class DefaultFileService @Inject constructor( // 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) + }.flatMap { cachedFiles -> + if (!cachedFiles.file.exists()) { val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) val request = Request.Builder() @@ -143,23 +137,23 @@ internal class DefaultFileService @Inject constructor( Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") // Write the file to cache (encrypted version if the file is encrypted) - writeToFile(source.inputStream(), destFile) + writeToFile(source.inputStream(), cachedFiles.file) response.close() } else { Timber.v("## FileService: cache hit for $url") } - Try.just(destFile) + Try.just(cachedFiles) } - }.flatMap { downloadedFile -> + }.flatMap { cachedFiles -> // Decrypt if necessary - if (elementToDecrypt != null) { - val decryptedFile = File(decryptedFolder, fileForUrl(unwrappedUrl, mimeType)) - - if (!decryptedFile.exists()) { + if (cachedFiles.decryptedFile != null) { + if (!cachedFiles.decryptedFile.exists()) { Timber.v("## FileService: decrypt file") - val decryptSuccess = decryptedFile.outputStream().buffered().use { outputStream -> - downloadedFile.inputStream().use { inputStream -> + // 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, @@ -173,18 +167,18 @@ internal class DefaultFileService @Inject constructor( } else { Timber.v("## FileService: cache hit for decrypted file") } - Try.just(decryptedFile) + Try.just(cachedFiles.decryptedFile) } else { // Clear file - Try.just(downloadedFile) + Try.just(cachedFiles.file) } }.fold( { throwable -> callback.onFailure(throwable) // notify concurrent requests val toNotify = synchronized(ongoing) { - ongoing[unwrappedUrl]?.also { - ongoing.remove(unwrappedUrl) + ongoing[url]?.also { + ongoing.remove(url) } } toNotify?.forEach { otherCallbacks -> @@ -195,8 +189,8 @@ internal class DefaultFileService @Inject constructor( callback.onSuccess(file) // notify concurrent requests val toNotify = synchronized(ongoing) { - ongoing[unwrappedUrl]?.also { - ongoing.remove(unwrappedUrl) + ongoing[url]?.also { + ongoing.remove(url) } } Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") @@ -208,6 +202,7 @@ internal class DefaultFileService @Inject constructor( }.toCancelable() } + /* fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) { val file = File(downloadFolder, fileForUrl(url, mimeType)) val source = inputStream.source().buffer() @@ -219,29 +214,70 @@ internal class DefaultFileService @Inject constructor( } } } + */ - 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?, mimeType: String?, elementToDecrypt: ElementToDecrypt?): Boolean { - return fileState(mxcUrl, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE - } - - private fun getClearFile(mxcUrl: String, mimeType: String?, elementToDecrypt: ElementToDecrypt?): File { - return if (elementToDecrypt == null) { - // Clear file - File(downloadFolder, fileForUrl(mxcUrl, mimeType)) - } else { - // Encrypted file - File(decryptedFolder, fileForUrl(mxcUrl, mimeType)) + 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(), "_") + 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) + } + } } } - override fun fileState(mxcUrl: String?, mimeType: String?, elementToDecrypt: ElementToDecrypt?): FileService.FileState { + override fun isFileInCache(mxcUrl: String?, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): Boolean { + return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE + } + + 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 + } + + private fun getFiles(mxcUrl: String, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): CachedFiles { + val hashFolder = mxcUrl.md5() + val safeFileName = safeFileName(fileName, mimeType) + return if (elementToDecrypt == null) { + // Clear file + CachedFiles( + File(downloadFolder, "$hashFolder/$safeFileName"), + null + ) + } else { + // Encrypted file + CachedFiles( + File(downloadFolder, "$hashFolder/$ENCRYPTED_FILENAME"), + File(decryptedFolder, "$hashFolder/$safeFileName"), + ) + } + } + + override fun fileState(mxcUrl: String?, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): FileService.FileState { mxcUrl ?: return FileService.FileState.UNKNOWN - if (getClearFile(mxcUrl, mimeType, elementToDecrypt).exists()) return FileService.FileState.IN_CACHE + if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt).file.exists()) return FileService.FileState.IN_CACHE val isDownloading = synchronized(ongoing) { ongoing[mxcUrl] != null } @@ -252,11 +288,14 @@ 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?, elementToDecrypt: ElementToDecrypt?): 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 = getClearFile(mxcUrl, mimeType, elementToDecrypt) + val targetFile = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt).getClearFile() if (!targetFile.exists()) return null return FileProvider.getUriForFile(context, authority, targetFile) } @@ -277,4 +316,8 @@ internal class DefaultFileService @Inject constructor( override fun clearDecryptedCache() { decryptedFolder.deleteRecursively() } + + companion object { + private const val ENCRYPTED_FILENAME = "encrypted.bin" + } } 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..8df5082c33 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 @@ -199,9 +199,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") try { + /* TODO context.contentResolver.openInputStream(attachment.queryUri)?.let { fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) - } + } */ Timber.v("## FileService: cache storage updated") } catch (failure: Throwable) { Timber.e(failure, "## FileService: Failed to update file cache") 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/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 213c50b6ac..34086043da 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,6 +84,7 @@ 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 @@ -204,7 +205,12 @@ class MessageItemFactory @Inject constructor( return MessageFileItem_() .attributes(attributes) .izLocalFile(fileUrl.isLocalFile()) - .izDownloaded(session.fileService().isFileInCache(fileUrl, messageContent.mimeType, messageContent.encryptedFileInfo?.toElementToDecrypt())) + .izDownloaded(session.fileService().isFileInCache( + fileUrl, + messageContent.getFileName(), + messageContent.mimeType, + messageContent.encryptedFileInfo?.toElementToDecrypt()) + ) .mxcUrl(fileUrl) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) From 1c43f92e49aae4d21cc28b2b193ccb3f2d1d973c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Dec 2020 12:20:48 +0100 Subject: [PATCH 59/89] DefaultFileService: store just sent file --- .../internal/session/DefaultFileService.kt | 57 +++++++++++-------- .../session/content/UploadContentWorker.kt | 19 ++++--- 2 files changed, 44 insertions(+), 32 deletions(-) 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 006ced8530..54ff90631b 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 @@ -111,7 +111,7 @@ internal class DefaultFileService @Inject constructor( // 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) - getFiles(url, fileName, mimeType, elementToDecrypt) + 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")) @@ -202,24 +202,29 @@ internal class DefaultFileService @Inject constructor( }.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 { + 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(), "_") + 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) } @@ -252,23 +257,23 @@ internal class DefaultFileService @Inject constructor( } private fun getFiles(mxcUrl: String, - fileName: String, + fileName: String?, mimeType: String?, - elementToDecrypt: ElementToDecrypt?): CachedFiles { + isEncrypted: Boolean): CachedFiles { val hashFolder = mxcUrl.md5() val safeFileName = safeFileName(fileName, mimeType) - return if (elementToDecrypt == null) { - // Clear file - CachedFiles( - File(downloadFolder, "$hashFolder/$safeFileName"), - null - ) - } else { + return if (isEncrypted) { // Encrypted file CachedFiles( File(downloadFolder, "$hashFolder/$ENCRYPTED_FILENAME"), File(decryptedFolder, "$hashFolder/$safeFileName"), ) + } else { + // Clear file + CachedFiles( + File(downloadFolder, "$hashFolder/$safeFileName"), + null + ) } } @@ -277,7 +282,7 @@ internal class DefaultFileService @Inject constructor( mimeType: String?, elementToDecrypt: ElementToDecrypt?): FileService.FileState { mxcUrl ?: return FileService.FileState.UNKNOWN - if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt).file.exists()) return FileService.FileState.IN_CACHE + if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE val isDownloading = synchronized(ongoing) { ongoing[mxcUrl] != null } @@ -295,7 +300,7 @@ internal class DefaultFileService @Inject constructor( mxcUrl ?: return null // this string could be extracted no? val authority = "${context.packageName}.mx-sdk.fileprovider" - val targetFile = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt).getClearFile() + val targetFile = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).getClearFile() if (!targetFile.exists()) return null return FileProvider.getUriForFile(context, authority, targetFile) } @@ -319,5 +324,7 @@ internal class DefaultFileService @Inject constructor( 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/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 8df5082c33..77f39a7768 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 @@ -174,14 +174,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,19 +191,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, "application/octet-stream", 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 { - /* TODO - 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") From 283e10dfefd3a3ead990b1a649bc525d1df3127e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Dec 2020 12:26:49 +0100 Subject: [PATCH 60/89] Use filename if available --- .../sdk/internal/session/room/send/DefaultSendService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ) From e4968c4119014a1bc6ff796b6b8ccf5a720aa7a9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Dec 2020 12:27:03 +0100 Subject: [PATCH 61/89] Doc and internal --- .../sdk/internal/session/content/ContentUploadResponse.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 ) From 0956baecf958bc6db35a26b3af74da8eb4ed8214 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Dec 2020 12:27:37 +0100 Subject: [PATCH 62/89] Delete unencrypted files each time the app is started --- .../im/vector/app/features/home/HomeActivityViewModel.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 From 4bd538e448a62d2775ef34a6236e988a3c5983ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Dec 2020 12:49:25 +0100 Subject: [PATCH 63/89] Changelog and update comment --- CHANGES.md | 1 + .../matrix/android/sdk/internal/session/DefaultFileService.kt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 7ba7b7cb76..7904834e62 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ Changes in Element 1.0.12 (2020-XX-XX) Features ✨: - Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428) - Room setting: update join rules and guest access (#2442) + - Store encrypted file in cache and cleanup decrypted file at each app start Improvements 🙌: - Add Setting Item to Change PIN (#2462) 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 54ff90631b..730c0dd82f 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 @@ -57,9 +57,11 @@ internal class DefaultFileService @Inject constructor( private val taskExecutor: TaskExecutor ) : FileService { - // Folder to store downloaded file (not decrypted) + // 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") init { From 75071cf1d9ab15ee715df1697e1faedc1cc41e81 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Dec 2020 13:50:14 +0100 Subject: [PATCH 64/89] Cleanup --- .../internal/network/interceptors/FormattedJsonHttpLogger.kt | 1 - .../matrix/android/sdk/internal/session/DefaultFileService.kt | 2 +- .../vector/app/features/home/room/detail/RoomDetailFragment.kt | 2 -- .../vector/app/features/home/room/detail/RoomDetailViewModel.kt | 2 -- .../app/features/roomprofile/uploads/RoomUploadsViewModel.kt | 2 -- 5 files changed, 1 insertion(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt index 849a464867..34ed28d467 100644 --- a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.network.interceptors import androidx.annotation.NonNull -import org.matrix.android.sdk.BuildConfig import okhttp3.logging.HttpLoggingInterceptor import org.json.JSONArray import org.json.JSONException 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 730c0dd82f..ee4f5da41e 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 @@ -268,7 +268,7 @@ internal class DefaultFileService @Inject constructor( // Encrypted file CachedFiles( File(downloadFolder, "$hashFolder/$ENCRYPTED_FILENAME"), - File(decryptedFolder, "$hashFolder/$safeFileName"), + File(decryptedFolder, "$hashFolder/$safeFileName") ) } else { // Clear file 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 a229f72755..f8168140a3 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 @@ -184,7 +184,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 @@ -193,7 +192,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 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 86f22a55ad..182ee6016d 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 @@ -79,7 +79,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 @@ -91,7 +90,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 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 bd37cecd56..bf2b56fc9b 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 @@ -31,8 +31,6 @@ 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.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 From d54571d0a6a768997327c053d7231a8ba1adcc54 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 9 Dec 2020 18:45:33 +0300 Subject: [PATCH 65/89] Emoji library added with Google style. --- vector/build.gradle | 4 ++++ vector/src/main/java/im/vector/app/VectorApplication.kt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/vector/build.gradle b/vector/build.gradle index 62ff3951af..28d8fe5c1b 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -440,6 +440,10 @@ dependencies { implementation 'com.google.zxing:core:3.3.3' implementation 'me.dm7.barcodescanner:zxing:1.9.13' + // Emoji Keyboard + implementation 'com.vanniktech:emoji-material:0.7.0' + implementation 'com.vanniktech:emoji-google:0.7.0' + // TESTS testImplementation 'junit:junit:4.13' testImplementation "org.amshove.kluent:kluent-android:$kluent_version" diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 5be313d719..921e8c0780 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -36,6 +36,8 @@ import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyController import com.facebook.stetho.Stetho import com.gabrielittner.threetenbp.LazyThreeTen +import com.vanniktech.emoji.EmojiManager +import com.vanniktech.emoji.google.GoogleEmojiProvider import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DaggerVectorComponent import im.vector.app.core.di.HasVectorInjector @@ -184,6 +186,8 @@ class VectorApplication : addAction(Intent.ACTION_SCREEN_OFF) addAction(Intent.ACTION_SCREEN_ON) }) + + EmojiManager.install(GoogleEmojiProvider()) } private fun enableStrictModeIfNeeded() { From 3d975b7fba2716fa385429a82eefc7200f06df62 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 9 Dec 2020 18:46:33 +0300 Subject: [PATCH 66/89] Update composer icons. --- vector/src/main/res/drawable/bg_send.xml | 6 +++++ .../src/main/res/drawable/ic_attachment.xml | 26 ++++++++++++++----- .../src/main/res/drawable/ic_insert_emoji.xml | 21 +++++++++++++++ vector/src/main/res/drawable/ic_keyboard.xml | 4 +++ 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 vector/src/main/res/drawable/bg_send.xml create mode 100644 vector/src/main/res/drawable/ic_insert_emoji.xml create mode 100644 vector/src/main/res/drawable/ic_keyboard.xml diff --git a/vector/src/main/res/drawable/bg_send.xml b/vector/src/main/res/drawable/bg_send.xml new file mode 100644 index 0000000000..6b12664032 --- /dev/null +++ b/vector/src/main/res/drawable/bg_send.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_attachment.xml b/vector/src/main/res/drawable/ic_attachment.xml index 944998c1cd..ea01e94372 100644 --- a/vector/src/main/res/drawable/ic_attachment.xml +++ b/vector/src/main/res/drawable/ic_attachment.xml @@ -1,7 +1,21 @@ - - + + + + diff --git a/vector/src/main/res/drawable/ic_insert_emoji.xml b/vector/src/main/res/drawable/ic_insert_emoji.xml new file mode 100644 index 0000000000..c35000342b --- /dev/null +++ b/vector/src/main/res/drawable/ic_insert_emoji.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_keyboard.xml b/vector/src/main/res/drawable/ic_keyboard.xml new file mode 100644 index 0000000000..5e5d431abb --- /dev/null +++ b/vector/src/main/res/drawable/ic_keyboard.xml @@ -0,0 +1,4 @@ + + + From a96cc19eb600aa8857ec46d82fa87f06059834fd Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 9 Dec 2020 18:47:34 +0300 Subject: [PATCH 67/89] Update composer layout by adding emoji icon. --- ...constraint_set_composer_layout_compact.xml | 69 ++++++++--------- ...onstraint_set_composer_layout_expanded.xml | 76 +++++++++---------- .../main/res/layout/merge_composer_layout.xml | 18 ++--- 3 files changed, 78 insertions(+), 85 deletions(-) diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml b/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml index ac67db6a64..6b9fbd4885 100644 --- a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml +++ b/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml @@ -77,9 +77,9 @@ android:alpha="0" app:layout_constraintEnd_toStartOf="parent" app:layout_constraintTop_toBottomOf="parent" + app:tint="?riotx_text_primary" tools:ignore="MissingConstraints,MissingPrefix" - tools:src="@drawable/ic_edit" - app:tint="?riotx_text_primary" /> + tools:src="@drawable/ic_edit" /> + app:tint="@color/riotx_notice" + tools:ignore="MissingPrefix" + tools:visibility="visible" /> - - + tools:ignore="MissingPrefix" /> - + tools:visibility="visible" /> + app:layout_constraintTop_toTopOf="@id/sendButton" + app:layout_goneMarginEnd="8dp" /> diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml index dba996309e..f52b072ece 100644 --- a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml +++ b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml @@ -83,9 +83,9 @@ app:layout_constraintEnd_toEndOf="@id/composer_related_message_avatar_view" app:layout_constraintStart_toStartOf="@id/composer_related_message_avatar_view" app:layout_constraintTop_toBottomOf="@id/composer_related_message_avatar_view" - tools:src="@drawable/ic_edit" app:tint="?riotx_text_primary" - tools:ignore="MissingPrefix" /> + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_edit" /> - - - - - - - - + + + + diff --git a/vector/src/main/res/layout/merge_composer_layout.xml b/vector/src/main/res/layout/merge_composer_layout.xml index 908b3f009b..ea2bc1bf30 100644 --- a/vector/src/main/res/layout/merge_composer_layout.xml +++ b/vector/src/main/res/layout/merge_composer_layout.xml @@ -70,8 +70,8 @@ android:id="@+id/composer_related_message_action_image" android:layout_width="0dp" android:layout_height="0dp" - tools:ignore="MissingConstraints,MissingPrefix" - app:tint="?riotx_text_primary" /> + app:tint="?riotx_text_primary" + tools:ignore="MissingConstraints,MissingPrefix" /> - - + android:background="?android:attr/selectableItemBackground" + android:src="@drawable/ic_insert_emoji" + tools:ignore="MissingConstraints" /> Date: Wed, 9 Dec 2020 18:48:14 +0300 Subject: [PATCH 68/89] Add emoji keyboard, remove profile picture from composer. --- .../home/room/detail/RoomDetailFragment.kt | 24 ++++++++++++++++--- .../room/detail/composer/ComposerEditText.kt | 4 ++-- .../room/detail/composer/TextComposerView.kt | 4 ++-- 3 files changed, 25 insertions(+), 7 deletions(-) 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 3f5e476a5e..f880b632d4 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 @@ -69,6 +69,7 @@ import com.airbnb.mvrx.withState import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText import com.jakewharton.rxbinding3.widget.textChanges +import com.vanniktech.emoji.EmojiPopup import im.vector.app.R import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper @@ -296,6 +297,8 @@ class RoomDetailFragment @Inject constructor( private var lockSendButton = false private val activeCallViewHolder = ActiveCallViewHolder() + private lateinit var emojiPopup: EmojiPopup + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) @@ -311,6 +314,7 @@ class RoomDetailFragment @Inject constructor( setupActiveCallView() setupJumpToBottomView() setupConfBannerView() + setupEmojiPopup() roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) @@ -478,6 +482,23 @@ class RoomDetailFragment @Inject constructor( } } + private fun setupEmojiPopup() { + EmojiPopup + .Builder + .fromRootView(rootConstraintLayout) + .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) + .setOnEmojiPopupShownListener { composerLayout.composerEmojiButton.setImageResource(R.drawable.ic_keyboard) } + .setOnEmojiPopupDismissListener { composerLayout.composerEmojiButton.setImageResource(R.drawable.ic_insert_emoji) } + .build(composerLayout.composerEditText) + .also { + emojiPopup = it + } + + composerLayout.composerEmojiButton.debouncedClicks { + emojiPopup.toggle() + } + } + private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) } @@ -1211,9 +1232,6 @@ class RoomDetailFragment @Inject constructor( scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline timelineEventController.update(state) inviteView.visibility = View.GONE - val uid = session.myUserId - val meMember = state.myRoomMember() - avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView) if (state.tombstoneEvent == null) { if (state.canSendMessage) { composerLayout.visibility = View.VISIBLE diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt index 353ab783db..2257e5ee81 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt @@ -24,16 +24,16 @@ import android.text.Editable import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection -import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat +import com.vanniktech.emoji.EmojiEditText import im.vector.app.core.extensions.ooi import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.features.html.PillImageSpan import timber.log.Timber class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) - : AppCompatEditText(context, attrs, defStyleAttr) { + : EmojiEditText(context, attrs, defStyleAttr) { interface Callback { fun onRichContentSelected(contentUri: Uri): Boolean 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 f4b14571c0..af0e1a91f0 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 @@ -72,8 +72,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib @BindView(R.id.composerEditText) lateinit var composerEditText: ComposerEditText - @BindView(R.id.composer_avatar_view) - lateinit var composerAvatarImageView: ImageView + @BindView(R.id.composer_emoji) + lateinit var composerEmojiButton: ImageButton @BindView(R.id.composer_shield) lateinit var composerShieldImageView: ImageView From 08964d8548bbf1544885e950b5bcdf73b724a6d8 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 10 Dec 2020 12:51:15 +0300 Subject: [PATCH 69/89] Fix emoji keyboard orientation bug. --- .../home/room/detail/RoomDetailFragment.kt | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) 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 f880b632d4..bc0f5ce6dc 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 @@ -53,7 +53,6 @@ import androidx.lifecycle.Observer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.addGlidePreloader @@ -290,15 +289,11 @@ class RoomDetailFragment @Inject constructor( private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var keyboardStateUtils: KeyboardStateUtils - @BindView(R.id.composerLayout) - lateinit var composerLayout: TextComposerView private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView private var lockSendButton = false private val activeCallViewHolder = ActiveCallViewHolder() - private lateinit var emojiPopup: EmojiPopup - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) @@ -483,16 +478,13 @@ class RoomDetailFragment @Inject constructor( } private fun setupEmojiPopup() { - EmojiPopup + val emojiPopup = EmojiPopup .Builder .fromRootView(rootConstraintLayout) .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) - .setOnEmojiPopupShownListener { composerLayout.composerEmojiButton.setImageResource(R.drawable.ic_keyboard) } - .setOnEmojiPopupDismissListener { composerLayout.composerEmojiButton.setImageResource(R.drawable.ic_insert_emoji) } + .setOnEmojiPopupShownListener { composerLayout?.composerEmojiButton?.setImageResource(R.drawable.ic_keyboard) } + .setOnEmojiPopupDismissListener { composerLayout?.composerEmojiButton?.setImageResource(R.drawable.ic_insert_emoji) } .build(composerLayout.composerEditText) - .also { - emojiPopup = it - } composerLayout.composerEmojiButton.debouncedClicks { emojiPopup.toggle() From a9f5ed3869e412136f89a8d66d5cf54781c774d9 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 10 Dec 2020 12:56:09 +0300 Subject: [PATCH 70/89] Add emoji license. --- vector/src/main/assets/open_source_licenses.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index ac80b0d691..acf0bec14f 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -347,11 +347,6 @@ SOFTWARE.
Copyright 2017 Gabriel Ittner. -
  • - Android-multipicker-library -
    - Copyright 2018 Kumar Bibek -
  • htmlcompressor
    @@ -390,6 +385,11 @@ SOFTWARE.
    Copyright 2018, Aleksandr Nikiforov
  • +
  • + Emoji +
    + Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors +
  •  Apache License
    
    From 4007982db6df660bd9d3e9bc401249504f773f77 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Thu, 10 Dec 2020 12:59:09 +0300
    Subject: [PATCH 71/89] Changelog added.
    
    ---
     CHANGES.md | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 1005040328..1381a1e7ca 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -4,6 +4,7 @@ Changes in Element 1.0.12 (2020-XX-XX)
     Features ✨:
      - Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428)
      - Room setting: update join rules and guest access (#2442)
    + - Emoji Keyboard (#2520)
     
     Improvements 🙌:
      - Add Setting Item to Change PIN (#2462)
    
    From c1cb23d728296cf71c3b97f0bd67f0d548603b54 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Thu, 10 Dec 2020 13:12:19 +0300
    Subject: [PATCH 72/89] Fix ripple effect of the send button.
    
    ---
     vector/src/main/res/drawable/bg_send.xml | 13 ++++++++-----
     1 file changed, 8 insertions(+), 5 deletions(-)
    
    diff --git a/vector/src/main/res/drawable/bg_send.xml b/vector/src/main/res/drawable/bg_send.xml
    index 6b12664032..4b357d7ab1 100644
    --- a/vector/src/main/res/drawable/bg_send.xml
    +++ b/vector/src/main/res/drawable/bg_send.xml
    @@ -1,6 +1,9 @@
     
    -
    -
    -    
    -
    \ No newline at end of file
    +
    +    
    +        
    +            
    +        
    +    
    +    
    +
    \ No newline at end of file
    
    From 5e2f091ec105e0018e97f086b6d124eaba8ec3b3 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 10 Dec 2020 13:36:00 +0100
    Subject: [PATCH 73/89] Remove useless parameter `id`
    
    ---
     .../sdk/api/session/file/FileService.kt       | 19 +++++++------------
     .../internal/session/DefaultFileService.kt    |  3 +--
     .../app/core/glide/VectorGlideModelLoader.kt  |  5 ++---
     .../home/room/detail/RoomDetailFragment.kt    |  2 --
     .../home/room/detail/RoomDetailViewModel.kt   |  1 -
     .../features/media/BaseAttachmentProvider.kt  |  5 ++---
     .../media/DataAttachmentRoomProvider.kt       |  3 +--
     .../media/RoomEventsAttachmentProvider.kt     |  1 -
     .../features/media/VideoContentRenderer.kt    |  2 --
     .../uploads/RoomUploadsViewModel.kt           |  2 --
     10 files changed, 13 insertions(+), 30 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt
    index d0f53f25de..bcdb5ea257 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt
    @@ -41,20 +41,15 @@ interface FileService {
          * Download a file.
          * Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
          */
    -    fun downloadFile(
    -            id: String,
    -            fileName: String,
    -            mimeType: String?,
    -            url: String?,
    -            elementToDecrypt: ElementToDecrypt?,
    -            callback: MatrixCallback): Cancelable
    +    fun downloadFile(fileName: String,
    +                     mimeType: String?,
    +                     url: String?,
    +                     elementToDecrypt: ElementToDecrypt?,
    +                     callback: MatrixCallback): Cancelable
     
    -    fun downloadFile(
    -            id: String,
    -            messageContent: MessageWithAttachmentContent,
    -            callback: MatrixCallback): Cancelable =
    +    fun downloadFile(messageContent: MessageWithAttachmentContent,
    +                     callback: MatrixCallback): Cancelable =
                 downloadFile(
    -                    id = id,
                         fileName = messageContent.getFileName(),
                         mimeType = messageContent.mimeType,
                         url = messageContent.getFileUrl(),
    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 ee4f5da41e..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
    @@ -79,8 +79,7 @@ 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(id: String,
    -                              fileName: String,
    +    override fun downloadFile(fileName: String,
                                   mimeType: String?,
                                   url: String?,
                                   elementToDecrypt: ElementToDecrypt?,
    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 cf40926ba4..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
    @@ -109,10 +109,9 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
             }
             // Use the file vector service, will avoid flickering and redownload after upload
             fileService.downloadFile(
    -                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/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
    index f8168140a3..f1ae79a0aa 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
    @@ -1654,7 +1654,6 @@ class RoomDetailFragment @Inject constructor(
                 shareText(requireContext(), action.messageContent.body)
             } else if (action.messageContent is MessageWithAttachmentContent) {
                 session.fileService().downloadFile(
    -                    id = action.eventId,
                         messageContent = action.messageContent,
                         callback = object : MatrixCallback {
                             override fun onSuccess(data: File) {
    @@ -1685,7 +1684,6 @@ class RoomDetailFragment @Inject constructor(
                 return
             }
             session.fileService().downloadFile(
    -                id = action.eventId,
                     messageContent = action.messageContent,
                     callback = object : MatrixCallback {
                         override fun onSuccess(data: File) {
    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 182ee6016d..7bba9728ca 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
    @@ -1030,7 +1030,6 @@ class RoomDetailViewModel @AssistedInject constructor(
                 }
             } else {
                 session.fileService().downloadFile(
    -                    id = action.eventId,
                         messageContent = action.messageFileContent,
                         callback = object : MatrixCallback {
                             override fun onSuccess(data: File) {
    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 5f61ca36e4..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,11 +153,10 @@ abstract class BaseAttachmentProvider(
             } else {
                 target.onVideoFileLoading(info.uid)
                 fileService.downloadFile(
    -                    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 6f58c1a4f3..584b13f32b 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
    @@ -77,10 +77,9 @@ class DataAttachmentRoomProvider(
         override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
             val item = getItem(position)
             fileService.downloadFile(
    -                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/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt
    index 9b895dbc4d..569d006fba 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
    @@ -125,7 +125,6 @@ class RoomEventsAttachmentProvider(
                         as? MessageWithAttachmentContent
                         ?: return@let
                 fileService.downloadFile(
    -                    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 35375bc8ce..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
    @@ -75,7 +75,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
     
                     activeSessionHolder.getActiveSession().fileService()
                             .downloadFile(
    -                                id = data.eventId,
                                     fileName = data.filename,
                                     mimeType = data.mimeType,
                                     url = data.url,
    @@ -114,7 +113,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
     
                     activeSessionHolder.getActiveSession().fileService()
                             .downloadFile(
    -                                id = data.eventId,
                                     fileName = data.filename,
                                     mimeType = data.mimeType,
                                     url = data.url,
    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 bf2b56fc9b..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
    @@ -131,7 +131,6 @@ class RoomUploadsViewModel @AssistedInject constructor(
                 try {
                     val file = awaitCallback {
                         session.fileService().downloadFile(
    -                            id = action.uploadEvent.eventId,
                                 messageContent = action.uploadEvent.contentWithAttachmentContent,
                                 callback = it
                         )
    @@ -148,7 +147,6 @@ class RoomUploadsViewModel @AssistedInject constructor(
                 try {
                     val file = awaitCallback {
                         session.fileService().downloadFile(
    -                            id = action.uploadEvent.eventId,
                                 messageContent = action.uploadEvent.contentWithAttachmentContent,
                                 callback = it)
                     }
    
    From 38843f74ab251981ef4303e125cce3f4bfcb2e60 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 1 Dec 2020 17:07:24 +0100
    Subject: [PATCH 74/89] No need for WRITE_EXTERNAL permission to send
     attachment to the app (anymore?)
    
    ---
     .../home/room/detail/RoomDetailFragment.kt    | 22 +------------------
     .../home/room/detail/RoomDetailViewModel.kt   |  3 ---
     2 files changed, 1 insertion(+), 24 deletions(-)
    
    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 93b2b69ba5..29036a91fb 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
    @@ -1100,18 +1100,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)
    @@ -1157,14 +1145,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)
                 }
             }
         }
    @@ -1561,7 +1542,6 @@ class RoomDetailFragment @Inject constructor(
         private fun cleanUpAfterPermissionNotGranted() {
             // Reset all pending data
             roomDetailViewModel.pendingAction = null
    -        roomDetailViewModel.pendingUri = null
             attachmentsHelper.pendingType = null
         }
     
    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 7bba9728ca..6db2a9205a 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
    @@ -128,9 +128,6 @@ class RoomDetailViewModel @AssistedInject constructor(
         // 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
     
    
    From eb30b9fae9df12082b051d6cfa3bebe99c172ad8 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 1 Dec 2020 17:11:05 +0100
    Subject: [PATCH 75/89] Show preview when sending attachment from the keyboard
     (#2440) It's actually a revert of a3b205b310fa10c8a82b22c2bd3cbdd348ce92f3
    
    ---
     CHANGES.md                                    |  1 +
     .../home/room/detail/RoomDetailFragment.kt    | 24 +++++++------------
     .../home/room/detail/RoomDetailViewModel.kt   |  3 ---
     3 files changed, 9 insertions(+), 19 deletions(-)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index a4aa0b7a0f..206e48170f 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -15,6 +15,7 @@ Bugfix 🐛:
      - Fix cancellation of sending event (#2438)
      - Double bottomsheet effect after verify with passphrase
      - EditText cursor jumps to the start while typing fast (#2469)
    + - Show preview when sending attachment from the keyboard (#2440)
     
     Translations 🗣:
      -
    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 29036a91fb..c471b90e8e 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
    @@ -1176,11 +1176,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
    @@ -1936,24 +1934,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 6db2a9205a..13362c8a2e 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
    @@ -128,9 +128,6 @@ class RoomDetailViewModel @AssistedInject constructor(
         // Slot to keep a pending action during permission request
         var pendingAction: RoomDetailAction? = 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
     
    
    From 439029467ad0c65d28ed8df1fb1f4a4e5f14e4cc Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 1 Dec 2020 17:37:06 +0100
    Subject: [PATCH 76/89] Attachment preview also for Gif files
    
    ---
     .idea/dictionaries/bmarty.xml                                 | 2 ++
     .../vector/app/features/attachments/ContentAttachmentData.kt  | 4 +++-
     2 files changed, 5 insertions(+), 1 deletion(-)
    
    diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
    index 5ad39614b7..16cc35cebe 100644
    --- a/.idea/dictionaries/bmarty.xml
    +++ b/.idea/dictionaries/bmarty.xml
    @@ -24,6 +24,8 @@
           pbkdf
           pids
           pkcs
    +      previewable
    +      previewables
           riotx
           signin
           signout
    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..3ca4f1b13e 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
    @@ -18,10 +18,12 @@ package im.vector.app.features.attachments
     
     import org.matrix.android.sdk.api.session.content.ContentAttachmentData
     
    +private val listOfPreviewableMimeTypes = listOf("image/jpeg", "image/png", "image/jpg", "image/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(
    
    From 21271b6510a0a01144f6491dfeda6137a39df8f2 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 1 Dec 2020 17:55:37 +0100
    Subject: [PATCH 77/89] Do not compress GIFs (#1616, #1254)
    
    ---
     CHANGES.md                                                   | 1 +
     .../sdk/internal/session/content/UploadContentWorker.kt      | 5 ++++-
     2 files changed, 5 insertions(+), 1 deletion(-)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 206e48170f..422d084db6 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -16,6 +16,7 @@ Bugfix 🐛:
      - Double bottomsheet effect after verify with passphrase
      - EditText cursor jumps to the start while typing fast (#2469)
      - Show preview when sending attachment from the keyboard (#2440)
    + - Do not compress GIFs (#1616, #1254)
     
     Translations 🗣:
      -
    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 77f39a7768..a72141e0ab 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
    @@ -151,7 +151,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 != "image/gif"
    +                        && params.compressBeforeSending) {
                         fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
                                 .also { compressedFile ->
                                     // Get new Bitmap size
    
    From ca75eae0aa6ed7d9aae85679274825acf2bbf7b0 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Wed, 2 Dec 2020 10:00:06 +0100
    Subject: [PATCH 78/89] Create MimeTypes object
    
    ---
     .../session/content/ContentAttachmentData.kt  |  3 +-
     .../room/model/message/MessageImageContent.kt |  3 +-
     .../matrix/android/sdk/api/util/MimeTypes.kt  | 38 +++++++++++++++++++
     .../session/content/ThumbnailExtractor.kt     |  3 +-
     .../session/content/UploadContentWorker.kt    |  7 ++--
     .../session/profile/DefaultProfileService.kt  |  3 +-
     .../room/create/CreateRoomBodyBuilder.kt      |  3 +-
     .../session/room/state/DefaultStateService.kt |  3 +-
     .../app/core/resources/ResourceUtils.kt       | 12 ++----
     .../core/utils/ExternalApplicationsUtil.kt    | 28 ++++++++------
     .../features/attachments/AttachmentsMapper.kt | 11 ++++--
     .../attachments/ContentAttachmentData.kt      |  8 +++-
     .../attachments/preview/Extensions.kt         |  6 ++-
     .../timeline/factory/MessageItemFactory.kt    |  3 +-
     .../media/DataAttachmentRoomProvider.kt       |  3 +-
     .../media/RoomEventsAttachmentProvider.kt     |  3 +-
     .../app/features/rageshake/BugReporter.kt     |  5 ++-
     17 files changed, 100 insertions(+), 42 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt
    
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt
    index 4677c2be32..4164b84ecd 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt
    @@ -21,6 +21,7 @@ import android.os.Parcelable
     import androidx.exifinterface.media.ExifInterface
     import com.squareup.moshi.JsonClass
     import kotlinx.android.parcel.Parcelize
    +import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
     
     @Parcelize
     @JsonClass(generateAdapter = true)
    @@ -45,5 +46,5 @@ data class ContentAttachmentData(
             VIDEO
         }
     
    -    fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType
    +    fun getSafeMimeType() = mimeType?.normalizeMimeType()
     }
    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/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/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 a72141e0ab..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
    @@ -153,7 +154,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
     
                     if (attachment.type == ContentAttachmentData.Type.IMAGE
                             // Do not compress gif
    -                        && attachment.mimeType != "image/gif"
    +                        && attachment.mimeType != MimeTypes.Gif
                             && params.compressBeforeSending) {
                         fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
                                 .also { compressedFile ->
    @@ -194,7 +195,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                         Timber.v("## FileService: Uploading file")
     
                         fileUploader
    -                            .uploadFile(encryptedFile, attachment.name, "application/octet-stream", progressListener)
    +                            .uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener)
                     } else {
                         Timber.v("## FileService: Clear file")
                         encryptedFile = null
    @@ -261,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/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/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
    index 6015d945c4..a93ec8e797 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
    @@ -34,6 +34,7 @@ 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
    @@ -164,7 +165,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
     
         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")
    +            val response = fileUploader.uploadFromUri(avatarUri, fileName, MimeTypes.Jpeg)
                 awaitCallback {
                     sendStateEvent(
                             eventType = EventType.STATE_ROOM_AVATAR,
    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/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/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 3ca4f1b13e..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,8 +17,14 @@
     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("image/jpeg", "image/png", "image/jpg", "image/gif")
    +private val listOfPreviewableMimeTypes = listOf(
    +        MimeTypes.Jpeg,
    +        MimeTypes.BadJpg,
    +        MimeTypes.Png,
    +        MimeTypes.Gif
    +)
     
     fun ContentAttachmentData.isPreviewable(): Boolean {
         // For now the preview only supports still image
    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/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    index 34086043da..2a98fd2dd7 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
    @@ -88,6 +88,7 @@ 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
    @@ -311,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 {
    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 584b13f32b..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 ?: "",
    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 569d006fba..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 ?: "",
    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")
                                 }
    
    From 49cad8feec7096612a339fb7b97a7891d4dee0a7 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 11 Dec 2020 11:49:46 +0100
    Subject: [PATCH 79/89] Rename files.
    
    ---
     .../features/home/room/detail/RoomDetailFragment.kt  |  2 +-
     .../home/room/detail/composer/TextComposerView.kt    | 12 ++++++------
     ...merge_composer_layout.xml => composer_layout.xml} |  2 +-
     ...ml => composer_layout_constraint_set_compact.xml} |  0
     ...l => composer_layout_constraint_set_expanded.xml} |  0
     5 files changed, 8 insertions(+), 8 deletions(-)
     rename vector/src/main/res/layout/{merge_composer_layout.xml => composer_layout.xml} (98%)
     rename vector/src/main/res/layout/{constraint_set_composer_layout_compact.xml => composer_layout_constraint_set_compact.xml} (100%)
     rename vector/src/main/res/layout/{constraint_set_composer_layout_expanded.xml => composer_layout_constraint_set_expanded.xml} (100%)
    
    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 93b2b69ba5..a8cd8590c4 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
    @@ -165,7 +165,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
    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/res/layout/merge_composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml
    similarity index 98%
    rename from vector/src/main/res/layout/merge_composer_layout.xml
    rename to vector/src/main/res/layout/composer_layout.xml
    index ea2bc1bf30..cb5dcbc42c 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">
     
         
         
     
         
     
         
    +        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" />
     
         
    Date: Fri, 11 Dec 2020 12:17:23 +0100
    Subject: [PATCH 81/89] Fix Layout issue (visible only on RTL) (#2523)
    
    ---
     .../main/res/layout/composer_layout_constraint_set_compact.xml  | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    index 4c91ba6b38..e1eb0f01c3 100644
    --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    @@ -113,7 +113,7 @@
             android:layout_width="16dp"
             android:layout_height="16dp"
             app:layout_constraintBottom_toBottomOf="@id/attachmentButton"
    -        app:layout_constraintEnd_toStartOf="@+id/attachmentButton"
    +        app:layout_constraintEnd_toStartOf="@+id/composerEditText"
             app:layout_constraintStart_toEndOf="@+id/attachmentButton"
             app:layout_constraintTop_toTopOf="@id/attachmentButton"
             tools:src="@drawable/ic_shield_black"
    
    From 32fd3be73235cd6392ac911237f6b10bb6d5dc79 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 11 Dec 2020 12:41:35 +0100
    Subject: [PATCH 82/89] Better alignment of button and animation, bigger touch
     area for send button
    
    ---
     vector/src/main/res/drawable/bg_send.xml       |  6 +++++-
     .../composer_layout_constraint_set_compact.xml | 16 ++++++++--------
     ...composer_layout_constraint_set_expanded.xml | 18 +++++++++---------
     3 files changed, 22 insertions(+), 18 deletions(-)
    
    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/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    index e1eb0f01c3..231e58c68a 100644
    --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    @@ -102,20 +102,20 @@
             android:layout_margin="12dp"
             android:background="?android:attr/selectableItemBackground"
             android:src="@drawable/ic_attachment"
    -        app:layout_constraintBottom_toBottomOf="parent"
    +        app:layout_constraintBottom_toBottomOf="@id/sendButton"
             app:layout_constraintEnd_toStartOf="@+id/composer_shield"
             app:layout_constraintStart_toStartOf="parent"
    -        app:layout_constraintTop_toTopOf="parent"
    +        app:layout_constraintTop_toTopOf="@id/sendButton"
             tools:ignore="MissingPrefix" />
     
         
     
    @@ -145,11 +145,11 @@
     
         
     
         
     
         
    Date: Fri, 11 Dec 2020 12:46:44 +0100
    Subject: [PATCH 83/89] Bigger touch area for the other buttons
    
    ---
     .../composer_layout_constraint_set_compact.xml      | 13 ++++++-------
     .../composer_layout_constraint_set_expanded.xml     | 12 ++++++------
     2 files changed, 12 insertions(+), 13 deletions(-)
    
    diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    index 231e58c68a..2de3efc8ee 100644
    --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    @@ -97,9 +97,9 @@
     
         
    Date: Fri, 11 Dec 2020 13:18:22 +0100
    Subject: [PATCH 84/89] Reorder Views (no other change)
    
    ---
     .../src/main/res/layout/composer_layout.xml   | 56 +++++++--------
     ...composer_layout_constraint_set_compact.xml | 72 +++++++++----------
     ...omposer_layout_constraint_set_expanded.xml | 46 ++++++------
     3 files changed, 87 insertions(+), 87 deletions(-)
    
    diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml
    index a1d6675278..cb0b37d844 100644
    --- a/vector/src/main/res/layout/composer_layout.xml
    +++ b/vector/src/main/res/layout/composer_layout.xml
    @@ -39,13 +39,6 @@
             tools:ignore="MissingConstraints"
             tools:src="@tools:sample/avatars" />
     
    -    
    -
         
     
    -    
    -
    -    
    -
         
     
         
     
    +    
    +
         
     
    +    
    +
    +    
    +
     
    diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    index 2de3efc8ee..a4dfcf019c 100644
    --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
    @@ -95,6 +95,16 @@
             tools:ignore="MissingPrefix"
             tools:visibility="visible" />
     
    +    
    +
         
     
    -    
    -
    -    
    -
    -    
    -
         
     
    +    
    +
    +    
    +
     
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml
    index 75dbdfd2a8..8a76c0547e 100644
    --- a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml
    +++ b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml
    @@ -100,6 +100,16 @@
             app:tint="@color/riotx_notice"
             tools:ignore="MissingPrefix" />
     
    +    
    +
         
     
    +    
    +
         
     
    -    
    -
         
     
    -    
    -
     
    \ No newline at end of file
    
    From bd9da8eaa6dddf74e23f678e01539c01955d462b Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Fri, 11 Dec 2020 15:06:22 +0100
    Subject: [PATCH 85/89] element:// support + basic peeking + fix join via
     server
    
    ---
     .../org/matrix/android/sdk/rx/RxSession.kt    |   3 +-
     .../session/permalinks/PermalinkService.kt    |   1 +
     .../sdk/api/session/room/RoomService.kt       |  14 +-
     .../session/room/DefaultRoomService.kt        |  25 ++-
     .../sdk/internal/session/room/RoomAPI.kt      |   3 +
     .../sdk/internal/session/room/RoomModule.kt   |  10 +
     .../room/alias/GetRoomIdByAliasTask.kt        |  14 +-
     .../room/alias/RoomAliasDescription.kt        |   2 +-
     .../session/room/peeking/PeekRoomTask.kt      | 173 ++++++++++++++++++
     .../room/peeking/ResolveRoomStateTask.kt      |  42 +++++
     vector/src/main/AndroidManifest.xml           |   5 +
     .../vector/app/features/home/HomeActivity.kt  |  34 +++-
     .../features/permalink/PermalinkHandler.kt    |  41 +++--
     .../app/features/popup/PopupAlertManager.kt   |   2 +-
     .../roompreview/RoomPreviewActivity.kt        |   1 +
     .../RoomPreviewNoPreviewFragment.kt           |  76 ++++++--
     .../roompreview/RoomPreviewViewModel.kt       |  56 +++++-
     .../roompreview/RoomPreviewViewState.kt       |  28 ++-
     .../fragment_room_preview_no_preview.xml      |  10 +-
     vector/src/main/res/values/strings.xml        |   3 +-
     20 files changed, 488 insertions(+), 55 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
     create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt
    
    diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt
    index 0e5b88adb2..a7b269fcc6 100644
    --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt
    +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt
    @@ -47,6 +47,7 @@ import org.matrix.android.sdk.api.util.toOptional
     import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
     import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
     import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
    +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
     
     class RxSession(private val session: Session) {
     
    @@ -139,7 +140,7 @@ class RxSession(private val session: Session) {
         }
     
         fun getRoomIdByAlias(roomAlias: String,
    -                         searchOnServer: Boolean): Single> = singleBuilder {
    +                         searchOnServer: Boolean): Single> = singleBuilder {
             session.getRoomIdByAlias(roomAlias, searchOnServer, it)
         }
     
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
    index ac1d726d03..aefc086b43 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
    @@ -25,6 +25,7 @@ interface PermalinkService {
     
         companion object {
             const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
    +        const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
         }
     
         /**
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
    index 477bef66cf..441a64e2c0 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
    @@ -18,12 +18,15 @@ package org.matrix.android.sdk.api.session.room
     
     import androidx.lifecycle.LiveData
     import org.matrix.android.sdk.api.MatrixCallback
    +import org.matrix.android.sdk.api.session.events.model.Event
     import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
     import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
     import org.matrix.android.sdk.api.session.room.model.RoomSummary
     import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
     import org.matrix.android.sdk.api.util.Cancelable
     import org.matrix.android.sdk.api.util.Optional
    +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
    +import org.matrix.android.sdk.internal.session.room.peeking.PeekResult
     
     /**
      * This interface defines methods to get rooms. It's implemented at the session level.
    @@ -120,7 +123,7 @@ interface RoomService {
          */
         fun getRoomIdByAlias(roomAlias: String,
                              searchOnServer: Boolean,
    -                         callback: MatrixCallback>): Cancelable
    +                         callback: MatrixCallback>): Cancelable
     
         /**
          * Delete a room alias
    @@ -163,4 +166,13 @@ interface RoomService {
          * @return a LiveData of the optional found room member
          */
         fun getRoomMemberLive(userId: String, roomId: String): LiveData>
    +
    +    fun getRoomState(roomId: String, callback: MatrixCallback>)
    +
    +    /**
    +     * Use this if you want to get information from a room that you are not yet in (or invited)
    +     * It might be possible to get some information on this room if it is public or if guest access is allowed
    +     * This call will try to gather some information on this room, but it could fail and get nothing more
    +     */
    +    fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback)
     }
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
    index 9ec985e0b6..e540105800 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
    @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
     import androidx.lifecycle.Transformations
     import com.zhuinden.monarchy.Monarchy
     import org.matrix.android.sdk.api.MatrixCallback
    +import org.matrix.android.sdk.api.session.events.model.Event
     import org.matrix.android.sdk.api.session.room.Room
     import org.matrix.android.sdk.api.session.room.RoomService
     import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
    @@ -35,10 +36,14 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
     import org.matrix.android.sdk.internal.di.SessionDatabase
     import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
     import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
    +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
     import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
     import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
     import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
     import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
    +import org.matrix.android.sdk.internal.session.room.peeking.PeekResult
    +import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
    +import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
     import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
     import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
     import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
    @@ -55,6 +60,8 @@ internal class DefaultRoomService @Inject constructor(
             private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
             private val roomIdByAliasTask: GetRoomIdByAliasTask,
             private val deleteRoomAliasTask: DeleteRoomAliasTask,
    +        private val resolveRoomStateTask: ResolveRoomStateTask,
    +        private val peekRoomTask: PeekRoomTask,
             private val roomGetter: RoomGetter,
             private val roomSummaryDataSource: RoomSummaryDataSource,
             private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
    @@ -119,7 +126,7 @@ internal class DefaultRoomService @Inject constructor(
                     .executeBy(taskExecutor)
         }
     
    -    override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable {
    +    override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable {
             return roomIdByAliasTask
                     .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
                         this.callback = callback
    @@ -154,4 +161,20 @@ internal class DefaultRoomService @Inject constructor(
                 results.firstOrNull().toOptional()
             }
         }
    +
    +    override fun getRoomState(roomId: String, callback: MatrixCallback>) {
    +        resolveRoomStateTask
    +                .configureWith(ResolveRoomStateTask.Params(roomId)) {
    +                    this.callback = callback
    +                }
    +                .executeBy(taskExecutor)
    +    }
    +
    +    override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback) {
    +        peekRoomTask
    +                .configureWith(PeekRoomTask.Params(roomIdOrAlias)) {
    +                    this.callback = callback
    +                }
    +                .executeBy(taskExecutor)
    +    }
     }
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
    index 955a251b52..f335987085 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
    @@ -354,4 +354,7 @@ internal interface RoomAPI {
         fun deleteTag(@Path("userId") userId: String,
                       @Path("roomId") roomId: String,
                       @Path("tag") tag: String): Call
    +
    +    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state")
    +    fun getRoomState(@Path("roomId") roomId: String) : Call>
     }
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
    index 3a94396a61..92f4ea2aea 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
    @@ -57,6 +57,10 @@ import org.matrix.android.sdk.internal.session.room.membership.leaving.DefaultLe
     import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
     import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask
     import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask
    +import org.matrix.android.sdk.internal.session.room.peeking.DefaultPeekRoomTask
    +import org.matrix.android.sdk.internal.session.room.peeking.DefaultResolveRoomStateTask
    +import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
    +import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
     import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask
     import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask
     import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
    @@ -223,4 +227,10 @@ internal abstract class RoomModule {
     
         @Binds
         abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask
    +
    +    @Binds
    +    abstract fun bindResolveRoomStateTask(task: DefaultResolveRoomStateTask): ResolveRoomStateTask
    +
    +    @Binds
    +    abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask
     }
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt
    index 3c47ee6ef0..2fe290ead2 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt
    @@ -29,7 +29,7 @@ import org.matrix.android.sdk.internal.session.directory.DirectoryAPI
     import org.matrix.android.sdk.internal.task.Task
     import javax.inject.Inject
     
    -internal interface GetRoomIdByAliasTask : Task> {
    +internal interface GetRoomIdByAliasTask : Task> {
         data class Params(
                 val roomAlias: String,
                 val searchOnServer: Boolean
    @@ -42,21 +42,21 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor(
             private val eventBus: EventBus
     ) : GetRoomIdByAliasTask {
     
    -    override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional {
    +    override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional {
             var roomId = Realm.getInstance(monarchy.realmConfiguration).use {
                 RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId
             }
             return if (roomId != null) {
    -            Optional.from(roomId)
    +            Optional.from(RoomAliasDescription(roomId))
             } else if (!params.searchOnServer) {
    -            Optional.from(null)
    +            Optional.from(null)
             } else {
    -            roomId = tryOrNull("## Failed to get roomId from alias") {
    +            val description  = tryOrNull("## Failed to get roomId from alias") {
                     executeRequest(eventBus) {
                         apiCall = directoryAPI.getRoomIdByAlias(params.roomAlias)
                     }
    -            }?.roomId
    -            Optional.from(roomId)
    +            }
    +            Optional.from(description)
             }
         }
     }
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt
    index ada3839fa0..d1f93c50be 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt
    @@ -20,7 +20,7 @@ import com.squareup.moshi.Json
     import com.squareup.moshi.JsonClass
     
     @JsonClass(generateAdapter = true)
    -internal data class RoomAliasDescription(
    +data class RoomAliasDescription(
             /**
              * The room ID for this alias.
              */
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
    new file mode 100644
    index 0000000000..ae557adb5f
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
    @@ -0,0 +1,173 @@
    +/*
    + * 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 org.matrix.android.sdk.internal.session.room.peeking
    +
    +import org.matrix.android.sdk.api.MatrixPatterns
    +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.RoomAvatarContent
    +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
    +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
    +import org.matrix.android.sdk.api.session.room.model.RoomNameContent
    +import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
    +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter
    +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
    +import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
    +import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
    +import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
    +import org.matrix.android.sdk.internal.task.Task
    +import javax.inject.Inject
    +
    +sealed class PeekResult {
    +    data class Success(
    +            val roomId: String,
    +            val alias: String?,
    +            val name: String?,
    +            val topic: String?,
    +            val avatarUrl: String?,
    +            val numJoinedMembers: Int?,
    +            val viaServers: List
    +    ) : PeekResult()
    +
    +    data class PeekingNotAllowed(
    +            val roomId: String,
    +            val alias: String?,
    +            val viaServers: List
    +    ) : PeekResult()
    +
    +    object UnknownAlias : PeekResult()
    +}
    +
    +internal interface PeekRoomTask : Task {
    +    data class Params(
    +            val roomIdOrAlias: String
    +    )
    +}
    +
    +internal class DefaultPeekRoomTask @Inject constructor(
    +        private val getRoomIdByAliasTask: GetRoomIdByAliasTask,
    +        private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask,
    +        private val getPublicRoomTask: GetPublicRoomTask,
    +        private val resolveRoomStateTask: ResolveRoomStateTask
    +) : PeekRoomTask {
    +
    +    override suspend fun execute(params: PeekRoomTask.Params): PeekResult {
    +        val roomId: String?
    +        val serverList: List
    +        val isAlias: Boolean
    +        if (MatrixPatterns.isRoomAlias(params.roomIdOrAlias)) {
    +            isAlias = true
    +            // get alias description
    +            val aliasDescription = getRoomIdByAliasTask
    +                    .execute(GetRoomIdByAliasTask.Params(params.roomIdOrAlias, true))
    +                    .getOrNull()
    +                    ?: return PeekResult.UnknownAlias
    +
    +            roomId = aliasDescription.roomId
    +            serverList = aliasDescription.servers
    +        } else {
    +            isAlias = false
    +            roomId = params.roomIdOrAlias
    +            serverList = emptyList()
    +        }
    +
    +        // Is it a public room?
    +        val publicRepoResult = when (getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId))) {
    +            RoomDirectoryVisibility.PRIVATE -> {
    +                // We cannot resolve this room :/
    +                null
    +            }
    +            RoomDirectoryVisibility.PUBLIC -> {
    +                // Try to find it in directory
    +                val filter = if (isAlias) PublicRoomsFilter(searchTerm = params.roomIdOrAlias.substring(1))
    +                else null
    +
    +                getPublicRoomTask.execute(GetPublicRoomTask.Params(
    +                        server = serverList.firstOrNull(),
    +                        publicRoomsParams = PublicRoomsParams(
    +                                filter = filter,
    +                                limit = 20.takeIf { filter != null } ?: 100
    +                        )
    +                )).chunk?.firstOrNull { it.roomId == roomId }
    +            }
    +        }
    +
    +        if (publicRepoResult != null) {
    +            return PeekResult.Success(
    +                    roomId = roomId,
    +                    alias = publicRepoResult.getPrimaryAlias() ?: params.roomIdOrAlias.takeIf { isAlias },
    +                    avatarUrl = publicRepoResult.avatarUrl,
    +                    name = publicRepoResult.name,
    +                    topic = publicRepoResult.topic,
    +                    numJoinedMembers = publicRepoResult.numJoinedMembers,
    +                    viaServers = serverList
    +            )
    +        }
    +
    +        // mm... try to peek state ? maybe the room is not public but yet allow guest to get events?
    +        // this could be slow
    +        try {
    +            val stateEvents = resolveRoomStateTask
    +                    .execute(ResolveRoomStateTask.Params(roomId))
    +            val name = stateEvents.lastOrNull {
    +                it.type == EventType.STATE_ROOM_NAME
    +                        && it.stateKey == ""
    +            }?.let { it.content?.toModel()?.name }
    +
    +            val topic = stateEvents.lastOrNull {
    +                it.type == EventType.STATE_ROOM_TOPIC
    +                        && it.stateKey == ""
    +            }?.let { it.content?.toModel()?.topic }
    +
    +            val avatarUrl = stateEvents.lastOrNull {
    +                it.type == EventType.STATE_ROOM_AVATAR
    +            }?.let { it.content?.toModel()?.avatarUrl }
    +
    +            val alias = stateEvents.lastOrNull {
    +                it.type == EventType.STATE_ROOM_CANONICAL_ALIAS
    +            }?.let {
    +                it.content?.toModel()?.canonicalAlias
    +                        ?: it.content?.toModel()?.alternativeAliases?.firstOrNull()
    +            }
    +
    +            // not sure if it's the right way to do that :/
    +            val memberCount = stateEvents.filter {
    +                it.type == EventType.STATE_ROOM_MEMBER
    +                        && it.stateKey?.isNotEmpty() == true
    +            }.distinctBy { it.stateKey }
    +                    .count()
    +
    +            return PeekResult.Success(
    +                    roomId = roomId,
    +                    alias = alias,
    +                    avatarUrl = avatarUrl,
    +                    name = name,
    +                    topic = topic,
    +                    numJoinedMembers = memberCount,
    +                    viaServers = serverList
    +            )
    +        } catch (failure: Throwable) {
    +            // Would be M_FORBIDDEN if cannot peek :/
    +            // User XXX not in room !XXX, and room previews are disabled
    +            return PeekResult.PeekingNotAllowed(
    +                    roomId = roomId,
    +                    alias = params.roomIdOrAlias.takeIf { isAlias },
    +                    viaServers = serverList
    +            )
    +        }
    +    }
    +}
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt
    new file mode 100644
    index 0000000000..289fdb498f
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt
    @@ -0,0 +1,42 @@
    +/*
    + * 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 org.matrix.android.sdk.internal.session.room.peeking
    +
    +import org.greenrobot.eventbus.EventBus
    +import org.matrix.android.sdk.api.session.events.model.Event
    +import org.matrix.android.sdk.internal.network.executeRequest
    +import org.matrix.android.sdk.internal.session.room.RoomAPI
    +import org.matrix.android.sdk.internal.task.Task
    +import javax.inject.Inject
    +
    +internal interface ResolveRoomStateTask : Task> {
    +    data class Params(
    +            val roomId: String
    +    )
    +}
    +
    +internal class DefaultResolveRoomStateTask @Inject constructor(
    +        private val roomAPI: RoomAPI,
    +        private val eventBus: EventBus
    +) : ResolveRoomStateTask {
    +
    +    override suspend fun execute(params: ResolveRoomStateTask.Params): List {
    +        return executeRequest(eventBus) {
    +            apiCall = roomAPI.getRoomState(params.roomId)
    +        }
    +    }
    +}
    diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
    index e9bd03cb4b..fc3c7366ec 100644
    --- a/vector/src/main/AndroidManifest.xml
    +++ b/vector/src/main/AndroidManifest.xml
    @@ -199,6 +199,11 @@
                     
                     
                     
    +                
    +                
    +
                 
             
     
    diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
    index 7dde0edf32..4ec786f841 100644
    --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
    @@ -126,9 +126,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
                     .observe()
                     .subscribe { sharedAction ->
                         when (sharedAction) {
    -                        is HomeActivitySharedAction.OpenDrawer  -> drawerLayout.openDrawer(GravityCompat.START)
    +                        is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
                             is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
    -                        is HomeActivitySharedAction.OpenGroup   -> {
    +                        is HomeActivitySharedAction.OpenGroup -> {
                                 drawerLayout.closeDrawer(GravityCompat.START)
                                 replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
                             }
    @@ -145,9 +145,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
             homeActivityViewModel.observeViewEvents {
                 when (it) {
                     is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it)
    -                is HomeActivityViewEvents.OnNewSession                  -> handleOnNewSession(it)
    -                HomeActivityViewEvents.PromptToEnableSessionPush        -> handlePromptToEnablePush()
    -                is HomeActivityViewEvents.OnCrossSignedInvalidated      -> handleCrossSigningInvalidated(it)
    +                is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
    +                HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
    +                is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
                 }.exhaustive
             }
             homeActivityViewModel.subscribe(this) { renderState(it) }
    @@ -162,9 +162,27 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
     
         private fun handleIntent(intent: Intent?) {
             intent?.dataString?.let { deepLink ->
    -            if (!deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) return@let
    +            val resolvedLink = if (deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) {
    +                deepLink
    +            } else if (deepLink.startsWith(PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE)) {
    +                // This is a bit hugly, but for now just convert to matrix.to link for compatibility
    +                val service = activeSessionHolder.getSafeActiveSession()?.permalinkService()
    +                val roomLinkPrefix = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/"
    +                val userPrefix = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/"
    +                when {
    +                    deepLink.startsWith(userPrefix)     -> {
    +                        val userId = deepLink.substring(userPrefix.length)
    +                        service?.createPermalink(userId)
    +                    }
    +                    deepLink.startsWith(roomLinkPrefix) -> {
    +                        val param = deepLink.substring(roomLinkPrefix.length)
    +                        service?.createRoomPermalink(param)
    +                    }
    +                    else                                -> null
    +                }
    +            } else null
     
    -            permalinkHandler.launch(this, deepLink,
    +            permalinkHandler.launch(this, resolvedLink,
                         navigationInterceptor = this,
                         buildTask = true)
                         // .delay(500, TimeUnit.MILLISECONDS)
    @@ -180,7 +198,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
     
         private fun renderState(state: HomeActivityViewState) {
             when (val status = state.initialSyncProgressServiceStatus) {
    -            is InitialSyncProgressService.Status.Idle        -> {
    +            is InitialSyncProgressService.Status.Idle -> {
                     waiting_view.isVisible = false
                 }
                 is InitialSyncProgressService.Status.Progressing -> {
    diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
    index f1149d8990..4e8b73cb2b 100644
    --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
    +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
    @@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkData
     import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
     import org.matrix.android.sdk.api.session.room.model.Membership
     import org.matrix.android.sdk.api.util.Optional
    +import org.matrix.android.sdk.api.util.toOptional
     import org.matrix.android.sdk.rx.rx
     import javax.inject.Inject
     
    @@ -76,7 +77,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
                 buildTask: Boolean
         ): Single {
             return when (permalinkData) {
    -            is PermalinkData.RoomLink     -> {
    +            is PermalinkData.RoomLink -> {
                     permalinkData.getRoomId()
                             .observeOn(AndroidSchedulers.mainThread())
                             .map {
    @@ -92,11 +93,11 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
                                 true
                             }
                 }
    -            is PermalinkData.GroupLink    -> {
    +            is PermalinkData.GroupLink -> {
                     navigator.openGroupDetail(permalinkData.groupId, context, buildTask)
                     Single.just(true)
                 }
    -            is PermalinkData.UserLink     -> {
    +            is PermalinkData.UserLink -> {
                     if (navigationInterceptor?.navToMemberProfile(permalinkData.userId, rawLink) != true) {
                         navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask)
                     }
    @@ -111,7 +112,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
         private fun PermalinkData.RoomLink.getRoomId(): Single> {
             val session = activeSessionHolder.getSafeActiveSession()
             return if (isRoomAlias && session != null) {
    -            session.rx().getRoomIdByAlias(roomIdOrAlias, true).subscribeOn(Schedulers.io())
    +            session.rx().getRoomIdByAlias(roomIdOrAlias, true).map { it.getOrNull()?.roomId.toOptional() }.subscribeOn(Schedulers.io())
             } else {
                 Single.just(Optional.from(roomIdOrAlias))
             }
    @@ -149,16 +150,28 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
                     navigator.openRoom(context, roomId, eventId, buildTask)
                 }
                 else                             -> {
    -                val roomPreviewData = RoomPreviewData(
    -                        roomId = roomId,
    -                        eventId = eventId,
    -                        roomAlias = roomAlias ?: roomSummary?.canonicalAlias,
    -                        roomName = roomSummary?.displayName,
    -                        avatarUrl = roomSummary?.avatarUrl,
    -                        buildTask = buildTask,
    -                        homeServers = permalinkData.viaParameters
    -                )
    -                navigator.openRoomPreview(context, roomPreviewData)
    +                if (roomSummary == null) {
    +                    // we don't know this room, try to peek
    +                    val roomPreviewData = RoomPreviewData(
    +                            roomId = roomId,
    +                            roomAlias = roomAlias,
    +                            peekFromServer = true,
    +                            buildTask = buildTask,
    +                            homeServers = permalinkData.viaParameters
    +                    )
    +                    navigator.openRoomPreview(context, roomPreviewData)
    +                } else {
    +                    val roomPreviewData = RoomPreviewData(
    +                            roomId = roomId,
    +                            eventId = eventId,
    +                            roomAlias = roomAlias ?: roomSummary.canonicalAlias,
    +                            roomName = roomSummary.displayName,
    +                            avatarUrl = roomSummary.avatarUrl,
    +                            buildTask = buildTask,
    +                            homeServers = permalinkData.viaParameters
    +                    )
    +                    navigator.openRoomPreview(context, roomPreviewData)
    +                }
                 }
             }
         }
    diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt
    index b2257b250a..c677a99175 100644
    --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt
    +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt
    @@ -130,7 +130,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy = emptyList(),
    +        val peekFromServer: Boolean = false,
             val buildTask: Boolean = false
     ) : Parcelable {
         val matrixItem: MatrixItem
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    index 108c3bacf1..bc4552fc11 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    @@ -20,6 +20,8 @@ import android.os.Bundle
     import android.view.View
     import androidx.core.view.isVisible
     import androidx.transition.TransitionManager
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.args
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    @@ -30,6 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment
     import im.vector.app.features.home.AvatarRenderer
     import im.vector.app.features.roomdirectory.JoinState
     import kotlinx.android.synthetic.main.fragment_room_preview_no_preview.*
    +import org.matrix.android.sdk.api.util.MatrixItem
     import javax.inject.Inject
     
     /**
    @@ -48,22 +51,6 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
         override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
             super.onViewCreated(view, savedInstanceState)
             setupToolbar(roomPreviewNoPreviewToolbar)
    -        val titleText = roomPreviewData.roomName ?: roomPreviewData.roomAlias ?: roomPreviewData.roomId
    -
    -        // Toolbar
    -        avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewToolbarAvatar)
    -        roomPreviewNoPreviewToolbarTitle.text = titleText
    -
    -        // Screen
    -        avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewAvatar)
    -        roomPreviewNoPreviewName.text = titleText
    -        roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic)
    -
    -        if (roomPreviewData.worldReadable) {
    -            roomPreviewNoPreviewLabel.setText(R.string.room_preview_world_readable_room_not_supported_yet)
    -        } else {
    -            roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview)
    -        }
     
             roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
                 override fun onButtonClicked() {
    @@ -100,7 +87,62 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
                 // Quit this screen
                 requireActivity().finish()
                 // Open room
    -            navigator.openRoom(requireActivity(), roomPreviewData.roomId, roomPreviewData.eventId, roomPreviewData.buildTask)
    +            navigator.openRoom(requireActivity(), state.roomId, roomPreviewData.eventId, roomPreviewData.buildTask)
    +        }
    +
    +        val bestName = state.roomName ?: state.roomAlias ?: state.roomId
    +        when (state.peekingState) {
    +            is Loading -> {
    +                roomPreviewPeekingProgress.isVisible = true
    +                roomPreviewNoPreviewJoin.isVisible = false
    +            }
    +            is Success -> {
    +                roomPreviewPeekingProgress.isVisible = false
    +                when (state.peekingState.invoke()) {
    +                    PeekingState.FOUND     -> {
    +                        // show join buttons
    +                        roomPreviewNoPreviewJoin.isVisible = true
    +                        renderState(bestName, state.matrixItem(), state.roomTopic)
    +                    }
    +                    PeekingState.NO_ACCESS -> {
    +                        roomPreviewNoPreviewJoin.isVisible = true
    +                        roomPreviewNoPreviewLabel.isVisible = true
    +                        roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview_join)
    +                        renderState(bestName, state.matrixItem().takeIf { state.roomAlias != null }, state.roomTopic)
    +                    }
    +                    else                   -> {
    +                        roomPreviewNoPreviewJoin.isVisible = false
    +                        roomPreviewNoPreviewLabel.isVisible = true
    +                        roomPreviewNoPreviewLabel.setText(R.string.room_preview_not_found)
    +                        renderState(bestName, null, state.roomTopic)
    +                    }
    +                }
    +            }
    +            else       -> {
    +                // Render with initial state, no peeking
    +                roomPreviewPeekingProgress.isVisible = false
    +                roomPreviewNoPreviewJoin.isVisible = true
    +                renderState(bestName, state.matrixItem(), state.roomTopic)
    +                roomPreviewNoPreviewLabel.isVisible = false
    +            }
             }
         }
    +
    +    private fun renderState(roomName: String, matrixItem: MatrixItem?, topic: String?) {
    +        // Toolbar
    +        if (matrixItem != null) {
    +            roomPreviewNoPreviewToolbarAvatar.isVisible = true
    +            roomPreviewNoPreviewAvatar.isVisible = true
    +            avatarRenderer.render(matrixItem, roomPreviewNoPreviewToolbarAvatar)
    +            avatarRenderer.render(matrixItem, roomPreviewNoPreviewAvatar)
    +        } else {
    +            roomPreviewNoPreviewToolbarAvatar.isVisible = false
    +            roomPreviewNoPreviewAvatar.isVisible = false
    +        }
    +        roomPreviewNoPreviewToolbarTitle.text = roomName
    +
    +        // Screen
    +        roomPreviewNoPreviewName.text = roomName
    +        roomPreviewNoPreviewTopic.setTextOrHide(topic)
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    index 900ba537b5..a18a41285b 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    @@ -16,8 +16,11 @@
     
     package im.vector.app.features.roomdirectory.roompreview
     
    +import androidx.lifecycle.viewModelScope
     import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
    @@ -25,12 +28,17 @@ import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.EmptyViewEvents
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.features.roomdirectory.JoinState
    +import kotlinx.coroutines.Dispatchers
    +import kotlinx.coroutines.launch
     import org.matrix.android.sdk.api.MatrixCallback
    +import org.matrix.android.sdk.api.extensions.tryOrNull
     import org.matrix.android.sdk.api.query.QueryStringValue
     import org.matrix.android.sdk.api.session.Session
     import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
     import org.matrix.android.sdk.api.session.room.model.Membership
     import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
    +import org.matrix.android.sdk.internal.session.room.peeking.PeekResult
    +import org.matrix.android.sdk.internal.util.awaitCallback
     import org.matrix.android.sdk.rx.rx
     import timber.log.Timber
     
    @@ -56,6 +64,52 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini
             // Observe joined room (from the sync)
             observeRoomSummary()
             observeMembershipChanges()
    +
    +        if (initialState.shouldPeekFromServer) {
    +            setState {
    +                copy(peekingState = Loading())
    +            }
    +            viewModelScope.launch(Dispatchers.IO) {
    +                val peekResult = tryOrNull {
    +                    awaitCallback {
    +                        session.peekRoom(initialState.roomAlias ?: initialState.roomId, it)
    +                    }
    +                }
    +
    +                when (peekResult) {
    +                    is PeekResult.Success -> {
    +                        setState {
    +                            copy(
    +                                    roomId = peekResult.roomId,
    +                                    avatarUrl = peekResult.avatarUrl,
    +                                    roomAlias = peekResult.alias ?: initialState.roomAlias,
    +                                    roomTopic = peekResult.topic,
    +                                    homeServers = peekResult.viaServers,
    +                                    peekingState = Success(PeekingState.FOUND)
    +                            )
    +                        }
    +                    }
    +                    is PeekResult.PeekingNotAllowed -> {
    +                        setState {
    +                            copy(
    +                                    roomId = peekResult.roomId,
    +                                    roomAlias = peekResult.alias ?: initialState.roomAlias,
    +                                    homeServers = peekResult.viaServers,
    +                                    peekingState = Success(PeekingState.NO_ACCESS)
    +                            )
    +                        }
    +                    }
    +                    PeekResult.UnknownAlias,
    +                    null -> {
    +                        setState {
    +                            copy(
    +                                    peekingState = Success(PeekingState.NOT_FOUND)
    +                            )
    +                        }
    +                    }
    +                }
    +            }
    +        }
         }
     
         private fun observeRoomSummary() {
    @@ -82,7 +136,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini
                     .subscribe {
                         val changeMembership = it[initialState.roomId] ?: ChangeMembershipState.Unknown
                         val joinState = when (changeMembership) {
    -                        is ChangeMembershipState.Joining       -> JoinState.JOINING
    +                        is ChangeMembershipState.Joining -> JoinState.JOINING
                             is ChangeMembershipState.FailedJoining -> JoinState.JOINING_ERROR
                             // Other cases are handled by room summary
                             else                                   -> null
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt
    index 6816e54481..c0f7591f87 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt
    @@ -16,13 +16,31 @@
     
     package im.vector.app.features.roomdirectory.roompreview
     
    +import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.Uninitialized
     import im.vector.app.features.roomdirectory.JoinState
    +import org.matrix.android.sdk.api.util.MatrixItem
    +
    +enum class PeekingState {
    +    UNKNOWN,
    +    FOUND,
    +    NOT_FOUND,
    +    NO_ACCESS
    +}
     
     data class RoomPreviewViewState(
    +
    +        val peekingState: Async = Uninitialized,
             // The room id
             val roomId: String = "",
             val roomAlias: String? = null,
    +
    +        val roomName: String? = null,
    +        val roomTopic: String? = null,
    +        val avatarUrl: String? = null,
    +
    +        val shouldPeekFromServer: Boolean = false,
             /**
              * Can be empty when the server is the current user's home server.
              */
    @@ -36,6 +54,14 @@ data class RoomPreviewViewState(
         constructor(args: RoomPreviewData) : this(
                 roomId = args.roomId,
                 roomAlias = args.roomAlias,
    -            homeServers = args.homeServers
    +            homeServers = args.homeServers,
    +            roomName = args.roomName,
    +            roomTopic = args.topic,
    +            avatarUrl = args.avatarUrl,
    +            shouldPeekFromServer = args.peekFromServer
         )
    +
    +    fun matrixItem() : MatrixItem {
    +        return MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl)
    +    }
     }
    diff --git a/vector/src/main/res/layout/fragment_room_preview_no_preview.xml b/vector/src/main/res/layout/fragment_room_preview_no_preview.xml
    index 2f4db6f116..906c7a21ab 100644
    --- a/vector/src/main/res/layout/fragment_room_preview_no_preview.xml
    +++ b/vector/src/main/res/layout/fragment_room_preview_no_preview.xml
    @@ -54,6 +54,14 @@
     
             
     
    +        
     
             
     
                     "This room can't be previewed"
         "The preview of world-readable room is not supported yet in Element"
    -
    +    This room is not accessible at this time.\nTry again later, or ask a room admin to check if you have access.
    +    "This room can't be previewed. Do you want to join it?"
         "Rooms"
         "Direct Messages"
     
    
    From 544345bbf34654f822a7f354bd49381817b7f368 Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Fri, 11 Dec 2020 15:16:10 +0100
    Subject: [PATCH 86/89] Update change log
    
    ---
     CHANGES.md | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 1381a1e7ca..18ebc1dcbd 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -9,11 +9,13 @@ Features ✨:
     Improvements 🙌:
      - Add Setting Item to Change PIN (#2462)
      - Improve room history visibility setting UX (#1579)
    + - Matrix.to deeplink custom scheme support
     
     Bugfix 🐛:
      - Fix cancellation of sending event (#2438)
      - Double bottomsheet effect after verify with passphrase
      - EditText cursor jumps to the start while typing fast (#2469)
    + - No known servers error is given when joining rooms on new Gitter bridge (#2516)
     
     Translations 🗣:
      -
    
    From 5461fd40609d504f4a330483cf4340b8484d56c2 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 11 Dec 2020 16:35:04 +0100
    Subject: [PATCH 87/89] Some cleanup up
    
    ---
     .../sdk/api/session/room/RoomService.kt       |  5 +-
     .../api/session/room/peeking/PeekResult.kt    | 37 +++++++++
     .../session/room/DefaultRoomService.kt        |  2 +-
     .../sdk/internal/session/room/RoomAPI.kt      | 14 ++--
     .../room/alias/GetRoomIdByAliasTask.kt        |  4 +-
     .../session/room/peeking/PeekRoomTask.kt      | 75 ++++++-----------
     .../room/peeking/ResolveRoomStateTask.kt      |  2 +-
     vector/src/main/AndroidManifest.xml           | 20 +++--
     .../vector/app/features/home/HomeActivity.kt  | 55 ++++++-------
     .../features/permalink/PermalinkHandler.kt    |  6 +-
     .../roomdirectory/roompreview/PeekingState.kt | 23 ++++++
     .../roompreview/RoomPreviewViewModel.kt       | 80 ++++++++++---------
     .../roompreview/RoomPreviewViewState.kt       |  8 --
     13 files changed, 188 insertions(+), 143 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt
     create mode 100644 vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/PeekingState.kt
    
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
    index 441a64e2c0..5f02b77a1e 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
    @@ -23,10 +23,10 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
     import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
     import org.matrix.android.sdk.api.session.room.model.RoomSummary
     import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
    +import org.matrix.android.sdk.api.session.room.peeking.PeekResult
     import org.matrix.android.sdk.api.util.Cancelable
     import org.matrix.android.sdk.api.util.Optional
     import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
    -import org.matrix.android.sdk.internal.session.room.peeking.PeekResult
     
     /**
      * This interface defines methods to get rooms. It's implemented at the session level.
    @@ -167,6 +167,9 @@ interface RoomService {
          */
         fun getRoomMemberLive(userId: String, roomId: String): LiveData>
     
    +    /**
    +     * Get some state events about a room
    +     */
         fun getRoomState(roomId: String, callback: MatrixCallback>)
     
         /**
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt
    new file mode 100644
    index 0000000000..db70dadef3
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt
    @@ -0,0 +1,37 @@
    +/*
    + * 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.room.peeking
    +
    +sealed class PeekResult {
    +    data class Success(
    +            val roomId: String,
    +            val alias: String?,
    +            val name: String?,
    +            val topic: String?,
    +            val avatarUrl: String?,
    +            val numJoinedMembers: Int?,
    +            val viaServers: List
    +    ) : PeekResult()
    +
    +    data class PeekingNotAllowed(
    +            val roomId: String,
    +            val alias: String?,
    +            val viaServers: List
    +    ) : PeekResult()
    +
    +    object UnknownAlias : PeekResult()
    +}
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
    index e540105800..383dd876d3 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
    @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
     import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
     import org.matrix.android.sdk.api.session.room.model.RoomSummary
     import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
    +import org.matrix.android.sdk.api.session.room.peeking.PeekResult
     import org.matrix.android.sdk.api.util.Cancelable
     import org.matrix.android.sdk.api.util.Optional
     import org.matrix.android.sdk.api.util.toOptional
    @@ -41,7 +42,6 @@ import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
     import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
     import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
     import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
    -import org.matrix.android.sdk.internal.session.room.peeking.PeekResult
     import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
     import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
     import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
    index f335987085..aa92c1cb3b 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
    @@ -183,7 +183,7 @@ internal interface RoomAPI {
                        @Body body: ThreePidInviteBody): Call
     
         /**
    -     * Send a generic state events
    +     * Send a generic state event
          *
          * @param roomId         the room id.
          * @param stateEventType the state event type
    @@ -195,7 +195,7 @@ internal interface RoomAPI {
                            @Body params: JsonDict): Call
     
         /**
    -     * Send a generic state events
    +     * Send a generic state event
          *
          * @param roomId         the room id.
          * @param stateEventType the state event type
    @@ -208,6 +208,13 @@ internal interface RoomAPI {
                            @Path("state_key") stateKey: String,
                            @Body params: JsonDict): Call
     
    +    /**
    +     * Get state events of a room
    +     * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-state
    +     */
    +    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state")
    +    fun getRoomState(@Path("roomId") roomId: String) : Call>
    +
         /**
          * Send a relation event to a room.
          *
    @@ -354,7 +361,4 @@ internal interface RoomAPI {
         fun deleteTag(@Path("userId") userId: String,
                       @Path("roomId") roomId: String,
                       @Path("tag") tag: String): Call
    -
    -    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state")
    -    fun getRoomState(@Path("roomId") roomId: String) : Call>
     }
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt
    index 2fe290ead2..543d605707 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt
    @@ -43,13 +43,13 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor(
     ) : GetRoomIdByAliasTask {
     
         override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional {
    -        var roomId = Realm.getInstance(monarchy.realmConfiguration).use {
    +        val roomId = Realm.getInstance(monarchy.realmConfiguration).use {
                 RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId
             }
             return if (roomId != null) {
                 Optional.from(RoomAliasDescription(roomId))
             } else if (!params.searchOnServer) {
    -            Optional.from(null)
    +            Optional.from(null)
             } else {
                 val description  = tryOrNull("## Failed to get roomId from alias") {
                     executeRequest(eventBus) {
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
    index ae557adb5f..9fba01efad 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2020 New Vector Ltd
    + * 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.
    @@ -26,32 +26,13 @@ import org.matrix.android.sdk.api.session.room.model.RoomNameContent
     import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
     import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter
     import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
    +import org.matrix.android.sdk.api.session.room.peeking.PeekResult
     import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
     import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
     import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
     import org.matrix.android.sdk.internal.task.Task
     import javax.inject.Inject
     
    -sealed class PeekResult {
    -    data class Success(
    -            val roomId: String,
    -            val alias: String?,
    -            val name: String?,
    -            val topic: String?,
    -            val avatarUrl: String?,
    -            val numJoinedMembers: Int?,
    -            val viaServers: List
    -    ) : PeekResult()
    -
    -    data class PeekingNotAllowed(
    -            val roomId: String,
    -            val alias: String?,
    -            val viaServers: List
    -    ) : PeekResult()
    -
    -    object UnknownAlias : PeekResult()
    -}
    -
     internal interface PeekRoomTask : Task {
         data class Params(
                 val roomIdOrAlias: String
    @@ -66,11 +47,10 @@ internal class DefaultPeekRoomTask @Inject constructor(
     ) : PeekRoomTask {
     
         override suspend fun execute(params: PeekRoomTask.Params): PeekResult {
    -        val roomId: String?
    +        val roomId: String
             val serverList: List
    -        val isAlias: Boolean
    -        if (MatrixPatterns.isRoomAlias(params.roomIdOrAlias)) {
    -            isAlias = true
    +        val isAlias = MatrixPatterns.isRoomAlias(params.roomIdOrAlias)
    +        if (isAlias) {
                 // get alias description
                 val aliasDescription = getRoomIdByAliasTask
                         .execute(GetRoomIdByAliasTask.Params(params.roomIdOrAlias, true))
    @@ -80,7 +60,6 @@ internal class DefaultPeekRoomTask @Inject constructor(
                 roomId = aliasDescription.roomId
                 serverList = aliasDescription.servers
             } else {
    -            isAlias = false
                 roomId = params.roomIdOrAlias
                 serverList = emptyList()
             }
    @@ -91,7 +70,7 @@ internal class DefaultPeekRoomTask @Inject constructor(
                     // We cannot resolve this room :/
                     null
                 }
    -            RoomDirectoryVisibility.PUBLIC -> {
    +            RoomDirectoryVisibility.PUBLIC  -> {
                     // Try to find it in directory
                     val filter = if (isAlias) PublicRoomsFilter(searchTerm = params.roomIdOrAlias.substring(1))
                     else null
    @@ -121,34 +100,30 @@ internal class DefaultPeekRoomTask @Inject constructor(
             // mm... try to peek state ? maybe the room is not public but yet allow guest to get events?
             // this could be slow
             try {
    -            val stateEvents = resolveRoomStateTask
    -                    .execute(ResolveRoomStateTask.Params(roomId))
    -            val name = stateEvents.lastOrNull {
    -                it.type == EventType.STATE_ROOM_NAME
    -                        && it.stateKey == ""
    -            }?.let { it.content?.toModel()?.name }
    +            val stateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomId))
    +            val name = stateEvents
    +                    .lastOrNull { it.type == EventType.STATE_ROOM_NAME && it.stateKey == "" }
    +                    ?.let { it.content?.toModel()?.name }
     
    -            val topic = stateEvents.lastOrNull {
    -                it.type == EventType.STATE_ROOM_TOPIC
    -                        && it.stateKey == ""
    -            }?.let { it.content?.toModel()?.topic }
    +            val topic = stateEvents
    +                    .lastOrNull { it.type == EventType.STATE_ROOM_TOPIC && it.stateKey == "" }
    +                    ?.let { it.content?.toModel()?.topic }
     
    -            val avatarUrl = stateEvents.lastOrNull {
    -                it.type == EventType.STATE_ROOM_AVATAR
    -            }?.let { it.content?.toModel()?.avatarUrl }
    +            val avatarUrl = stateEvents
    +                    .lastOrNull { it.type == EventType.STATE_ROOM_AVATAR }
    +                    ?.let { it.content?.toModel()?.avatarUrl }
     
    -            val alias = stateEvents.lastOrNull {
    -                it.type == EventType.STATE_ROOM_CANONICAL_ALIAS
    -            }?.let {
    -                it.content?.toModel()?.canonicalAlias
    -                        ?: it.content?.toModel()?.alternativeAliases?.firstOrNull()
    -            }
    +            val alias = stateEvents
    +                    .lastOrNull { it.type == EventType.STATE_ROOM_CANONICAL_ALIAS }
    +                    ?.let {
    +                        it.content?.toModel()?.canonicalAlias
    +                                ?: it.content?.toModel()?.alternativeAliases?.firstOrNull()
    +                    }
     
                 // not sure if it's the right way to do that :/
    -            val memberCount = stateEvents.filter {
    -                it.type == EventType.STATE_ROOM_MEMBER
    -                        && it.stateKey?.isNotEmpty() == true
    -            }.distinctBy { it.stateKey }
    +            val memberCount = stateEvents
    +                    .filter { it.type == EventType.STATE_ROOM_MEMBER && it.stateKey?.isNotEmpty() == true }
    +                    .distinctBy { it.stateKey }
                         .count()
     
                 return PeekResult.Success(
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt
    index 289fdb498f..03ea2408f0 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2020 New Vector Ltd
    + * 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.
    diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
    index fc3c7366ec..bf839b807c 100644
    --- a/vector/src/main/AndroidManifest.xml
    +++ b/vector/src/main/AndroidManifest.xml
    @@ -81,8 +81,9 @@
                     android:resource="@xml/shortcuts" />
             
     
    -        
    +        
             
    -        
    +        
                 
                     
    +
                     
                     
     
                     
                     
                     
    -                
    -                
    +                
    +                
     
                 
             
    diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
    index 4ec786f841..e1837ccb1b 100644
    --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
    @@ -126,9 +126,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
                     .observe()
                     .subscribe { sharedAction ->
                         when (sharedAction) {
    -                        is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
    +                        is HomeActivitySharedAction.OpenDrawer  -> drawerLayout.openDrawer(GravityCompat.START)
                             is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
    -                        is HomeActivitySharedAction.OpenGroup -> {
    +                        is HomeActivitySharedAction.OpenGroup   -> {
                                 drawerLayout.closeDrawer(GravityCompat.START)
                                 replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
                             }
    @@ -145,9 +145,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
             homeActivityViewModel.observeViewEvents {
                 when (it) {
                     is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it)
    -                is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
    -                HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
    -                is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
    +                is HomeActivityViewEvents.OnNewSession                  -> handleOnNewSession(it)
    +                HomeActivityViewEvents.PromptToEnableSessionPush        -> handlePromptToEnablePush()
    +                is HomeActivityViewEvents.OnCrossSignedInvalidated      -> handleCrossSigningInvalidated(it)
                 }.exhaustive
             }
             homeActivityViewModel.subscribe(this) { renderState(it) }
    @@ -162,29 +162,27 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
     
         private fun handleIntent(intent: Intent?) {
             intent?.dataString?.let { deepLink ->
    -            val resolvedLink = if (deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) {
    -                deepLink
    -            } else if (deepLink.startsWith(PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE)) {
    -                // This is a bit hugly, but for now just convert to matrix.to link for compatibility
    -                val service = activeSessionHolder.getSafeActiveSession()?.permalinkService()
    -                val roomLinkPrefix = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/"
    -                val userPrefix = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/"
    -                when {
    -                    deepLink.startsWith(userPrefix)     -> {
    -                        val userId = deepLink.substring(userPrefix.length)
    -                        service?.createPermalink(userId)
    +            val resolvedLink = when {
    +                deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)               -> deepLink
    +                deepLink.startsWith(PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> {
    +                    // This is a bit ugly, but for now just convert to matrix.to link for compatibility
    +                    when {
    +                        deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length)
    +                        deepLink.startsWith(ROOM_LINK_PREFIX) -> deepLink.substring(ROOM_LINK_PREFIX.length)
    +                        else                                  -> null
    +                    }?.let {
    +                        activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it)
                         }
    -                    deepLink.startsWith(roomLinkPrefix) -> {
    -                        val param = deepLink.substring(roomLinkPrefix.length)
    -                        service?.createRoomPermalink(param)
    -                    }
    -                    else                                -> null
                     }
    -            } else null
    +                else                                                                   -> null
    +            }
     
    -            permalinkHandler.launch(this, resolvedLink,
    +            permalinkHandler.launch(
    +                    context = this,
    +                    deepLink = resolvedLink,
                         navigationInterceptor = this,
    -                    buildTask = true)
    +                    buildTask = true
    +            )
                         // .delay(500, TimeUnit.MILLISECONDS)
                         .observeOn(AndroidSchedulers.mainThread())
                         .subscribe { isHandled ->
    @@ -198,7 +196,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
     
         private fun renderState(state: HomeActivityViewState) {
             when (val status = state.initialSyncProgressServiceStatus) {
    -            is InitialSyncProgressService.Status.Idle -> {
    +            is InitialSyncProgressService.Status.Idle        -> {
                     waiting_view.isVisible = false
                 }
                 is InitialSyncProgressService.Status.Progressing -> {
    @@ -363,11 +361,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
                     bugReporter.openBugReportScreen(this, false)
                     return true
                 }
    -            R.id.menu_home_filter -> {
    +            R.id.menu_home_filter     -> {
                     navigator.openRoomsFiltering(this)
                     return true
                 }
    -            R.id.menu_home_setting -> {
    +            R.id.menu_home_setting    -> {
                     navigator.openSettings(this)
                     return true
                 }
    @@ -408,5 +406,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
                             putExtra(MvRx.KEY_ARG, args)
                         }
             }
    +
    +        private const val ROOM_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/"
    +        private const val USER_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/"
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
    index 4e8b73cb2b..a7d69c783c 100644
    --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
    +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt
    @@ -77,7 +77,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
                 buildTask: Boolean
         ): Single {
             return when (permalinkData) {
    -            is PermalinkData.RoomLink -> {
    +            is PermalinkData.RoomLink     -> {
                     permalinkData.getRoomId()
                             .observeOn(AndroidSchedulers.mainThread())
                             .map {
    @@ -93,11 +93,11 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
                                 true
                             }
                 }
    -            is PermalinkData.GroupLink -> {
    +            is PermalinkData.GroupLink    -> {
                     navigator.openGroupDetail(permalinkData.groupId, context, buildTask)
                     Single.just(true)
                 }
    -            is PermalinkData.UserLink -> {
    +            is PermalinkData.UserLink     -> {
                     if (navigationInterceptor?.navToMemberProfile(permalinkData.userId, rawLink) != true) {
                         navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask)
                     }
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/PeekingState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/PeekingState.kt
    new file mode 100644
    index 0000000000..918264e594
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/PeekingState.kt
    @@ -0,0 +1,23 @@
    +/*
    + * 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.roomdirectory.roompreview
    +
    +enum class PeekingState {
    +    FOUND,
    +    NOT_FOUND,
    +    NO_ACCESS
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    index a18a41285b..72c4c58a42 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    @@ -36,8 +36,8 @@ import org.matrix.android.sdk.api.query.QueryStringValue
     import org.matrix.android.sdk.api.session.Session
     import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
     import org.matrix.android.sdk.api.session.room.model.Membership
    +import org.matrix.android.sdk.api.session.room.peeking.PeekResult
     import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
    -import org.matrix.android.sdk.internal.session.room.peeking.PeekResult
     import org.matrix.android.sdk.internal.util.awaitCallback
     import org.matrix.android.sdk.rx.rx
     import timber.log.Timber
    @@ -66,46 +66,50 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini
             observeMembershipChanges()
     
             if (initialState.shouldPeekFromServer) {
    -            setState {
    -                copy(peekingState = Loading())
    +            peekRoomFromServer()
    +        }
    +    }
    +
    +    private fun peekRoomFromServer() {
    +        setState {
    +            copy(peekingState = Loading())
    +        }
    +        viewModelScope.launch(Dispatchers.IO) {
    +            val peekResult = tryOrNull {
    +                awaitCallback {
    +                    session.peekRoom(initialState.roomAlias ?: initialState.roomId, it)
    +                }
                 }
    -            viewModelScope.launch(Dispatchers.IO) {
    -                val peekResult = tryOrNull {
    -                    awaitCallback {
    -                        session.peekRoom(initialState.roomAlias ?: initialState.roomId, it)
    +
    +            when (peekResult) {
    +                is PeekResult.Success           -> {
    +                    setState {
    +                        copy(
    +                                roomId = peekResult.roomId,
    +                                avatarUrl = peekResult.avatarUrl,
    +                                roomAlias = peekResult.alias ?: initialState.roomAlias,
    +                                roomTopic = peekResult.topic,
    +                                homeServers = peekResult.viaServers,
    +                                peekingState = Success(PeekingState.FOUND)
    +                        )
                         }
                     }
    -
    -                when (peekResult) {
    -                    is PeekResult.Success -> {
    -                        setState {
    -                            copy(
    -                                    roomId = peekResult.roomId,
    -                                    avatarUrl = peekResult.avatarUrl,
    -                                    roomAlias = peekResult.alias ?: initialState.roomAlias,
    -                                    roomTopic = peekResult.topic,
    -                                    homeServers = peekResult.viaServers,
    -                                    peekingState = Success(PeekingState.FOUND)
    -                            )
    -                        }
    +                is PeekResult.PeekingNotAllowed -> {
    +                    setState {
    +                        copy(
    +                                roomId = peekResult.roomId,
    +                                roomAlias = peekResult.alias ?: initialState.roomAlias,
    +                                homeServers = peekResult.viaServers,
    +                                peekingState = Success(PeekingState.NO_ACCESS)
    +                        )
                         }
    -                    is PeekResult.PeekingNotAllowed -> {
    -                        setState {
    -                            copy(
    -                                    roomId = peekResult.roomId,
    -                                    roomAlias = peekResult.alias ?: initialState.roomAlias,
    -                                    homeServers = peekResult.viaServers,
    -                                    peekingState = Success(PeekingState.NO_ACCESS)
    -                            )
    -                        }
    -                    }
    -                    PeekResult.UnknownAlias,
    -                    null -> {
    -                        setState {
    -                            copy(
    -                                    peekingState = Success(PeekingState.NOT_FOUND)
    -                            )
    -                        }
    +                }
    +                PeekResult.UnknownAlias,
    +                null                            -> {
    +                    setState {
    +                        copy(
    +                                peekingState = Success(PeekingState.NOT_FOUND)
    +                        )
                         }
                     }
                 }
    @@ -136,7 +140,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini
                     .subscribe {
                         val changeMembership = it[initialState.roomId] ?: ChangeMembershipState.Unknown
                         val joinState = when (changeMembership) {
    -                        is ChangeMembershipState.Joining -> JoinState.JOINING
    +                        is ChangeMembershipState.Joining       -> JoinState.JOINING
                             is ChangeMembershipState.FailedJoining -> JoinState.JOINING_ERROR
                             // Other cases are handled by room summary
                             else                                   -> null
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt
    index c0f7591f87..5a73f29ca3 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt
    @@ -22,15 +22,7 @@ import com.airbnb.mvrx.Uninitialized
     import im.vector.app.features.roomdirectory.JoinState
     import org.matrix.android.sdk.api.util.MatrixItem
     
    -enum class PeekingState {
    -    UNKNOWN,
    -    FOUND,
    -    NOT_FOUND,
    -    NO_ACCESS
    -}
    -
     data class RoomPreviewViewState(
    -
             val peekingState: Async = Uninitialized,
             // The room id
             val roomId: String = "",
    
    From 071611b81c7088f142aec6502e7b1dad5c0fc1be Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 11 Dec 2020 16:38:22 +0100
    Subject: [PATCH 88/89] We are not supposed to fallback on alternative alias
    
    ---
     .../sdk/internal/session/room/peeking/PeekRoomTask.kt        | 5 +----
     1 file changed, 1 insertion(+), 4 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
    index 9fba01efad..5a82d74537 100644
    --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
    @@ -115,10 +115,7 @@ internal class DefaultPeekRoomTask @Inject constructor(
     
                 val alias = stateEvents
                         .lastOrNull { it.type == EventType.STATE_ROOM_CANONICAL_ALIAS }
    -                    ?.let {
    -                        it.content?.toModel()?.canonicalAlias
    -                                ?: it.content?.toModel()?.alternativeAliases?.firstOrNull()
    -                    }
    +                    ?.let { it.content?.toModel()?.canonicalAlias }
     
                 // not sure if it's the right way to do that :/
                 val memberCount = stateEvents
    
    From d8a1939c697098c5edd76f0de6599da97668f1fc Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Fri, 11 Dec 2020 17:50:59 +0100
    Subject: [PATCH 89/89] Fix number of enum
    
    ---
     tools/check/forbidden_strings_in_code.txt | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
    index 63a3fad109..fc510e585c 100644
    --- a/tools/check/forbidden_strings_in_code.txt
    +++ b/tools/check/forbidden_strings_in_code.txt
    @@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
     # android\.text\.TextUtils
     
     ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
    -enum class===83
    +enum class===84
     
     ### Do not import temporary legacy classes
     import org.matrix.android.sdk.internal.legacy.riot===3