Merge branch 'develop' into feature/bma/url_preview

This commit is contained in:
Benoit Marty 2020-12-11 09:47:57 +01:00 committed by GitHub
commit 91c86c1a45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 657 additions and 534 deletions

View file

@ -5,12 +5,15 @@ Features ✨:
- Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428) - Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428)
- Room setting: update join rules and guest access (#2442) - Room setting: update join rules and guest access (#2442)
- Url preview (#481) - Url preview (#481)
- Store encrypted file in cache and cleanup decrypted file at each app start (#2512)
- Emoji Keyboard (#2520)
Improvements 🙌: Improvements 🙌:
- Add Setting Item to Change PIN (#2462) - Add Setting Item to Change PIN (#2462)
- Improve room history visibility setting UX (#1579) - Improve room history visibility setting UX (#1579)
Bugfix 🐛: Bugfix 🐛:
- Fix cancellation of sending event (#2438)
- Double bottomsheet effect after verify with passphrase - Double bottomsheet effect after verify with passphrase
- EditText cursor jumps to the start while typing fast (#2469) - EditText cursor jumps to the start while typing fast (#2469)
@ -19,15 +22,19 @@ Translations 🗣:
SDK API changes ⚠️: SDK API changes ⚠️:
- RawCacheStrategy has been moved and renamed to CacheStrategy - RawCacheStrategy has been moved and renamed to CacheStrategy
- FileService: remove useless FileService.DownloadMode
Build 🧱: 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: Test:
- -
Other changes: Other changes:
- Remove "Status.im" theme #2424 - 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) Changes in Element 1.0.11 (2020-11-27)
=================================================== ===================================================

View file

@ -66,7 +66,6 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.fragment:fragment:1.3.0-beta01"
implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.material:material:1.2.1'

View file

@ -2,8 +2,8 @@
buildscript { buildscript {
// Ref: https://kotlinlang.org/releases.html // Ref: https://kotlinlang.org/releases.html
ext.kotlin_version = '1.4.10' ext.kotlin_version = '1.4.20'
ext.kotlin_coroutines_version = "1.3.9" ext.kotlin_coroutines_version = "1.4.1"
repositories { repositories {
google() google()
jcenter() jcenter()
@ -12,7 +12,7 @@ buildscript {
} }
} }
dependencies { 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 'com.google.gms:google-services:4.3.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'

View file

@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m
org.gradle.vfs.watch=true org.gradle.vfs.watch=true
vector.debugPrivateData=false 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 # 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 #vector.debugPrivateData=true

View file

@ -36,9 +36,9 @@ android {
dependencies { dependencies {
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation 'androidx.appcompat:appcompat:1.2.0' 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:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
// Paging // Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.2" implementation "androidx.paging:paging-runtime-ktx:2.1.2"

View file

@ -21,34 +21,36 @@ import org.matrix.android.sdk.api.util.Cancelable
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Single import io.reactivex.Single
fun <T> singleBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Single<T> = Single.create { fun <T> singleBuilder(builder: (MatrixCallback<T>) -> Cancelable): Single<T> = Single.create { emitter ->
val callback: MatrixCallback<T> = object : MatrixCallback<T> { val callback = object : MatrixCallback<T> {
override fun onSuccess(data: T) { override fun onSuccess(data: T) {
it.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) { override fun onFailure(failure: Throwable) {
it.tryOnError(failure) emitter.tryOnError(failure)
} }
} }
val cancelable = builder(callback) val cancelable = builder(callback)
it.setCancellable { emitter.setCancellable {
cancelable.cancel() cancelable.cancel()
} }
} }
fun <T> completableBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Completable = Completable.create { fun <T> completableBuilder(builder: (MatrixCallback<T>) -> Cancelable): Completable = Completable.create { emitter ->
val callback: MatrixCallback<T> = object : MatrixCallback<T> { val callback = object : MatrixCallback<T> {
override fun onSuccess(data: T) { override fun onSuccess(data: T) {
it.onComplete() emitter.onComplete()
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
it.tryOnError(failure) emitter.tryOnError(failure)
} }
} }
val cancelable = builder(callback) val cancelable = builder(callback)
it.setCancellable { emitter.setCancellable {
cancelable.cancel() cancelable.cancel()
} }
} }

View file

@ -9,7 +9,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath "io.realm:realm-gradle-plugin:10.0.0" classpath "io.realm:realm-gradle-plugin:10.1.2"
} }
} }
@ -63,7 +63,7 @@ android {
release { release {
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" 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"
} }
} }
@ -125,7 +125,6 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.appcompat:appcompat:1.2.0" 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.core:core-ktx:1.3.2"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
@ -146,7 +145,7 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:core:$markwon_version"
// Image // Image
implementation 'androidx.exifinterface:exifinterface:1.3.0' implementation 'androidx.exifinterface:exifinterface:1.3.1'
// Database // Database
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'

View file

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.network.interceptors package org.matrix.android.sdk.internal.network.interceptors
import androidx.annotation.NonNull import androidx.annotation.NonNull
import org.matrix.android.sdk.BuildConfig
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
@ -38,31 +37,28 @@ class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
*/ */
@Synchronized @Synchronized
override fun log(@NonNull message: String) { override fun log(@NonNull message: String) {
// In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG Timber.v(message)
if (BuildConfig.DEBUG) {
Timber.v(message)
if (message.startsWith("{")) { if (message.startsWith("{")) {
// JSON Detected // JSON Detected
try { try {
val o = JSONObject(message) val o = JSONObject(message)
logJson(o.toString(INDENT_SPACE)) logJson(o.toString(INDENT_SPACE))
} catch (e: JSONException) { } catch (e: JSONException) {
// Finally this is not a JSON string... // Finally this is not a JSON string...
Timber.e(e) Timber.e(e)
} }
} else if (message.startsWith("[")) { } else if (message.startsWith("[")) {
// JSON Array detected // JSON Array detected
try { try {
val o = JSONArray(message) val o = JSONArray(message)
logJson(o.toString(INDENT_SPACE)) logJson(o.toString(INDENT_SPACE))
} catch (e: JSONException) { } catch (e: JSONException) {
// Finally not JSON... // Finally not JSON...
Timber.e(e) Timber.e(e)
}
} }
// Else not a json string to log
} }
// Else not a json string to log
} }
private fun logJson(formattedJson: String) { private fun logJson(formattedJson: String) {

View file

@ -18,8 +18,12 @@ package org.matrix.android.sdk.api.session.file
import android.net.Uri import android.net.Uri
import org.matrix.android.sdk.api.MatrixCallback 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.api.util.Cancelable
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import java.io.File import java.io.File
/** /**
@ -27,23 +31,6 @@ import java.io.File
*/ */
interface FileService { 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 { enum class FileState {
IN_CACHE, IN_CACHE,
DOWNLOADING, DOWNLOADING,
@ -54,34 +41,79 @@ interface FileService {
* Download a file. * 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. * 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( fun downloadFile(fileName: String,
downloadMode: DownloadMode, mimeType: String?,
id: String, url: String?,
fileName: String, elementToDecrypt: ElementToDecrypt?,
mimeType: String?, callback: MatrixCallback<File>): Cancelable
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>): Cancelable
fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean fun downloadFile(messageContent: MessageWithAttachmentContent,
callback: MatrixCallback<File>): Cancelable =
downloadFile(
fileName = messageContent.getFileName(),
mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
callback = callback
)
fun isFileInCache(mxcUrl: String?,
fileName: String,
mimeType: String?,
elementToDecrypt: ElementToDecrypt?
): Boolean
fun isFileInCache(messageContent: MessageWithAttachmentContent) =
isFileInCache(
mxcUrl = messageContent.getFileUrl(),
fileName = messageContent.getFileName(),
mimeType = messageContent.mimeType,
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt())
/** /**
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION * 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) * (if not other app won't be able to access it)
*/ */
fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? 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()
)
/** /**
* Get information on the given file. * Get information on the given file.
* Mimetype should be the same one as passed to downloadFile (limitation for now) * Mimetype should be the same one as passed to downloadFile (limitation for now)
*/ */
fun fileState(mxcUrl: String, mimeType: String?): FileState 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()
)
/** /**
* Clears all the files downloaded by the service * Clears all the files downloaded by the service, including decrypted files
*/ */
fun clearCache() fun clearCache()
/**
* Clears all the decrypted files by the service
*/
fun clearDecryptedCache()
/** /**
* Get size of cached files * Get size of cached files
*/ */

View file

@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.extensions.toUnsignedInt
import org.matrix.olm.OlmSAS import org.matrix.olm.OlmSAS
import org.matrix.olm.OlmUtility import org.matrix.olm.OlmUtility
import timber.log.Timber import timber.log.Timber
import java.util.Locale
/** /**
* Represents an ongoing short code interactive key verification between two devices. * 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? { protected fun hashUsingAgreedHashMethod(toHash: String): String? {
if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) { if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) {
val olmUtil = OlmUtility() val olmUtil = OlmUtility()
val hashBytes = olmUtil.sha256(toHash) val hashBytes = olmUtil.sha256(toHash)
olmUtil.releaseUtility() olmUtil.releaseUtility()
@ -354,12 +355,11 @@ internal abstract class SASDefaultVerificationTransaction(
} }
private fun macUsingAgreedMethod(message: String, info: String): String? { private fun macUsingAgreedMethod(message: String, info: String): String? {
if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) {
return getSAS().calculateMacLongKdf(message, info) SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info)
} else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { SAS_MAC_SHA256 -> getSAS().calculateMac(message, info)
return getSAS().calculateMac(message, info) else -> null
} }
return null
} }
override fun getDecimalCodeRepresentation(): String { override fun getDecimalCodeRepresentation(): String {

View file

@ -71,9 +71,6 @@ internal interface MatrixComponent {
@CacheDirectory @CacheDirectory
fun cacheDir(): File fun cacheDir(): File
@ExternalFilesDirectory
fun externalFilesDir(): File?
fun olmManager(): OlmManager fun olmManager(): OlmManager
fun taskExecutor(): TaskExecutor fun taskExecutor(): TaskExecutor

View file

@ -57,13 +57,6 @@ internal object MatrixModule {
return context.cacheDir return context.cacheDir
} }
@JvmStatic
@Provides
@ExternalFilesDirectory
fun providesExternalFilesDir(context: Context): File? {
return context.getExternalFilesDir(null)
}
@JvmStatic @JvmStatic
@Provides @Provides
@MatrixScope @MatrixScope

View file

@ -16,14 +16,15 @@
package org.matrix.android.sdk.internal.network 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.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus 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.Call
import retrofit2.awaitResponse import retrofit2.awaitResponse
import timber.log.Timber
import java.io.IOException import java.io.IOException
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?, internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
@ -49,6 +50,9 @@ internal class Request<DATA : Any>(private val eventBus: EventBus?) {
throw response.toFailure(eventBus) throw response.toFailure(eventBus)
} }
} catch (exception: Throwable) { } 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 // Check if this is a certificateException
CertUtil.getCertificateException(exception) CertUtil.getCertificateException(exception)
// TODO Support certificate error once logged // TODO Support certificate error once logged

View file

@ -21,6 +21,10 @@ import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import arrow.core.Try import arrow.core.Try
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
@ -29,35 +33,21 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.di.CacheDirectory
import org.matrix.android.sdk.internal.di.ExternalFilesDirectory
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers 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.toCancelable
import org.matrix.android.sdk.internal.util.writeToFile 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 timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.net.URLEncoder
import javax.inject.Inject import javax.inject.Inject
internal class DefaultFileService @Inject constructor( internal class DefaultFileService @Inject constructor(
private val context: Context, private val context: Context,
@CacheDirectory
private val cacheDirectory: File,
@ExternalFilesDirectory
private val externalFilesDirectory: File?,
@SessionDownloadsDirectory @SessionDownloadsDirectory
private val sessionCacheDirectory: File, private val sessionCacheDirectory: File,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
@ -67,9 +57,17 @@ internal class DefaultFileService @Inject constructor(
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : FileService { ) : FileService {
private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName()) // Legacy folder, will be deleted
private val legacyFolder = File(sessionCacheDirectory, "MF")
// Folder to store downloaded files (not decrypted)
private val downloadFolder = File(sessionCacheDirectory, "F")
// Folder to store decrypted files
private val decryptedFolder = File(downloadFolder, "D")
private val downloadFolder = File(sessionCacheDirectory, "MF") init {
// Clear the legacy downloaded files
legacyFolder.deleteRecursively()
}
/** /**
* Retain ongoing downloads to avoid re-downloading and already downloading file * Retain ongoing downloads to avoid re-downloading and already downloading file
@ -81,28 +79,26 @@ internal class DefaultFileService @Inject constructor(
* Download file in the cache folder, and eventually decrypt it * Download file in the cache folder, and eventually decrypt it
* TODO looks like files are copied 3 times * TODO looks like files are copied 3 times
*/ */
override fun downloadFile(downloadMode: FileService.DownloadMode, override fun downloadFile(fileName: String,
id: String,
fileName: String,
mimeType: String?, mimeType: String?,
url: String?, url: String?,
elementToDecrypt: ElementToDecrypt?, elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>): Cancelable { callback: MatrixCallback<File>): Cancelable {
val unwrappedUrl = url ?: return NoOpCancellable.also { url ?: return NoOpCancellable.also {
callback.onFailure(IllegalArgumentException("url is null")) callback.onFailure(IllegalArgumentException("url is null"))
} }
Timber.v("## FileService downloadFile $unwrappedUrl") Timber.v("## FileService downloadFile $url")
synchronized(ongoing) { synchronized(ongoing) {
val existing = ongoing[unwrappedUrl] val existing = ongoing[url]
if (existing != null) { if (existing != null) {
Timber.v("## FileService downloadFile is already downloading.. ") Timber.v("## FileService downloadFile is already downloading.. ")
existing.add(callback) existing.add(callback)
return NoOpCancellable return NoOpCancellable
} else { } else {
// mark as tracked // mark as tracked
ongoing[unwrappedUrl] = ArrayList() ongoing[url] = ArrayList()
// and proceed to download // and proceed to download
} }
} }
@ -110,15 +106,15 @@ internal class DefaultFileService @Inject constructor(
return taskExecutor.executorScope.launch(coroutineDispatchers.main) { return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
Try { Try {
if (!downloadFolder.exists()) { if (!decryptedFolder.exists()) {
downloadFolder.mkdirs() decryptedFolder.mkdirs()
} }
// ensure we use unique file name by using URL (mapped to suitable file name) // 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 // 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) // shared with will not function well (even if mime type is passed in the intent)
File(downloadFolder, fileForUrl(unwrappedUrl, mimeType)) getFiles(url, fileName, mimeType, elementToDecrypt != null)
}.flatMap { destFile -> }.flatMap { cachedFiles ->
if (!destFile.exists()) { if (!cachedFiles.file.exists()) {
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
val request = Request.Builder() val request = Request.Builder()
@ -141,79 +137,153 @@ internal class DefaultFileService @Inject constructor(
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
if (elementToDecrypt != null) { // Write the file to cache (encrypted version if the file is encrypted)
Timber.v("## FileService: decrypt file") writeToFile(source.inputStream(), cachedFiles.file)
val decryptSuccess = destFile.outputStream().buffered().use { response.close()
MXEncryptedAttachments.decryptAttachment(
source.inputStream(),
elementToDecrypt,
it
)
}
response.close()
if (!decryptSuccess) {
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
}
} else {
writeToFile(source.inputStream(), destFile)
response.close()
}
} else { } else {
Timber.v("## FileService: cache hit for $url") Timber.v("## FileService: cache hit for $url")
} }
Try.just(copyFile(destFile, downloadMode)) Try.just(cachedFiles)
} }
}.fold({ }.flatMap { cachedFiles ->
callback.onFailure(it) // Decrypt if necessary
// notify concurrent requests if (cachedFiles.decryptedFile != null) {
val toNotify = synchronized(ongoing) { if (!cachedFiles.decryptedFile.exists()) {
ongoing[unwrappedUrl]?.also { Timber.v("## FileService: decrypt file")
ongoing.remove(unwrappedUrl) // Ensure the parent folder exists
cachedFiles.decryptedFile.parentFile?.mkdirs()
val decryptSuccess = cachedFiles.file.inputStream().use { inputStream ->
cachedFiles.decryptedFile.outputStream().buffered().use { outputStream ->
MXEncryptedAttachments.decryptAttachment(
inputStream,
elementToDecrypt,
outputStream
)
}
}
if (!decryptSuccess) {
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
}
} else {
Timber.v("## FileService: cache hit for decrypted file")
} }
Try.just(cachedFiles.decryptedFile)
} else {
// Clear file
Try.just(cachedFiles.file)
} }
toNotify?.forEach { otherCallbacks -> }.fold(
tryOrNull { otherCallbacks.onFailure(it) } { throwable ->
} callback.onFailure(throwable)
}, { file -> // notify concurrent requests
callback.onSuccess(file) val toNotify = synchronized(ongoing) {
// notify concurrent requests ongoing[url]?.also {
val toNotify = synchronized(ongoing) { ongoing.remove(url)
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[url]?.also {
ongoing.remove(url)
}
}
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
toNotify?.forEach { otherCallbacks ->
tryOrNull { otherCallbacks.onSuccess(file) }
}
} }
} )
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
toNotify?.forEach { otherCallbacks ->
tryOrNull { otherCallbacks.onSuccess(file) }
}
})
}.toCancelable() }.toCancelable()
} }
fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) { fun storeDataFor(mxcUrl: String,
val file = File(downloadFolder, fileForUrl(url, mimeType)) filename: String?,
val source = inputStream.source().buffer() mimeType: String?,
file.sink().buffer().let { sink -> originalFile: File,
source.use { input -> encryptedFile: File?) {
sink.use { output -> val files = getFiles(mxcUrl, filename, mimeType, encryptedFile != null)
output.writeAll(input) if (encryptedFile != null) {
// We switch the two files here, original file it the decrypted file
files.decryptedFile?.let { originalFile.copyTo(it) }
encryptedFile.copyTo(files.file)
} else {
// Just copy the original file
originalFile.copyTo(files.file)
}
}
private fun safeFileName(fileName: String?, mimeType: String?): String {
return buildString {
// filename has to be safe for the Android System
val result = fileName
?.replace("[^a-z A-Z0-9\\\\.\\-]".toRegex(), "_")
?.takeIf { it.isNotEmpty() }
?: DEFAULT_FILENAME
append(result)
// Check that the extension is correct regarding the mimeType
val extensionFromMime = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
if (extensionFromMime != null) {
// Compare
val fileExtension = result.substringAfterLast(delimiter = ".", missingDelimiterValue = "")
if (fileExtension.isEmpty() || fileExtension != extensionFromMime) {
// Missing extension, or diff in extension, add the one provided by the mimetype
append(".")
append(extensionFromMime)
} }
} }
} }
} }
private fun fileForUrl(url: String, mimeType: String?): String { override fun isFileInCache(mxcUrl: String?,
val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) } fileName: String,
return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName() mimeType: String?,
elementToDecrypt: ElementToDecrypt?): Boolean {
return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE
} }
override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean { internal data class CachedFiles(
return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists() // This is the downloaded file. Can be clear or encrypted
val file: File,
// This is the decrypted file. Null if the original file is not encrypted
val decryptedFile: File?
) {
fun getClearFile(): File = decryptedFile ?: file
} }
override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState { private fun getFiles(mxcUrl: String,
if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE fileName: String?,
mimeType: String?,
isEncrypted: Boolean): CachedFiles {
val hashFolder = mxcUrl.md5()
val safeFileName = safeFileName(fileName, mimeType)
return if (isEncrypted) {
// Encrypted file
CachedFiles(
File(downloadFolder, "$hashFolder/$ENCRYPTED_FILENAME"),
File(decryptedFolder, "$hashFolder/$safeFileName")
)
} else {
// Clear file
CachedFiles(
File(downloadFolder, "$hashFolder/$safeFileName"),
null
)
}
}
override fun fileState(mxcUrl: String?,
fileName: String,
mimeType: String?,
elementToDecrypt: ElementToDecrypt?): FileService.FileState {
mxcUrl ?: return FileService.FileState.UNKNOWN
if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE
val isDownloading = synchronized(ongoing) { val isDownloading = synchronized(ongoing) {
ongoing[mxcUrl] != null ongoing[mxcUrl] != null
} }
@ -224,26 +294,18 @@ internal class DefaultFileService @Inject constructor(
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION * 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) * (if not other app won't be able to access it)
*/ */
override fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? { override fun getTemporarySharableURI(mxcUrl: String?,
fileName: String,
mimeType: String?,
elementToDecrypt: ElementToDecrypt?): Uri? {
mxcUrl ?: return null
// this string could be extracted no? // this string could be extracted no?
val authority = "${context.packageName}.mx-sdk.fileprovider" val authority = "${context.packageName}.mx-sdk.fileprovider"
val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType)) val targetFile = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).getClearFile()
if (!targetFile.exists()) return null if (!targetFile.exists()) return null
return FileProvider.getUriForFile(context, authority, targetFile) 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 { override fun getCacheSize(): Int {
return downloadFolder.walkTopDown() return downloadFolder.walkTopDown()
.onEnter { .onEnter {
@ -256,4 +318,14 @@ internal class DefaultFileService @Inject constructor(
override fun clearCache() { override fun clearCache() {
downloadFolder.deleteRecursively() downloadFolder.deleteRecursively()
} }
override fun clearDecryptedCache() {
decryptedFolder.deleteRecursively()
}
companion object {
private const val ENCRYPTED_FILENAME = "encrypted.bin"
// The extension would be added from the mimetype
private const val DEFAULT_FILENAME = "file"
}
} }

View file

@ -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.RealmSessionProvider
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
import org.matrix.android.sdk.internal.di.Authenticated 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.DeviceId
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
@ -169,9 +170,9 @@ internal abstract class SessionModule {
@JvmStatic @JvmStatic
@Provides @Provides
@SessionDownloadsDirectory @SessionDownloadsDirectory
fun providesCacheDir(@SessionId sessionId: String, fun providesDownloadsCacheDir(@SessionId sessionId: String,
context: Context): File { @CacheDirectory cacheFile: File): File {
return File(context.cacheDir, "downloads/$sessionId") return File(cacheFile, "downloads/$sessionId")
} }
@JvmStatic @JvmStatic

View file

@ -20,6 +20,9 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @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 @Json(name = "content_uri") val contentUri: String
) )

View file

@ -174,14 +174,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
} }
val encryptedFile: File?
val contentUploadResponse = if (params.isEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
Timber.v("## FileService: Encrypt file") 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) } .also { filesToDelete.add(it) }
uploadedFileEncryptedFileInfo = uploadedFileEncryptedFileInfo =
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), encryptedFile) { read, total ->
notifyTracker(params) { notifyTracker(params) {
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
} }
@ -190,18 +191,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
Timber.v("## FileService: Uploading file") Timber.v("## FileService: Uploading file")
fileUploader fileUploader
.uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener) .uploadFile(encryptedFile, attachment.name, "application/octet-stream", progressListener)
} else { } else {
Timber.v("## FileService: Clear file") Timber.v("## FileService: Clear file")
encryptedFile = null
fileUploader fileUploader
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) .uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
} }
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
try { try {
context.contentResolver.openInputStream(attachment.queryUri)?.let { fileService.storeDataFor(
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) mxcUrl = contentUploadResponse.contentUri,
} filename = params.attachment.name,
mimeType = params.attachment.getSafeMimeType(),
originalFile = workingFile,
encryptedFile = encryptedFile
)
Timber.v("## FileService: cache storage updated") Timber.v("## FileService: cache storage updated")
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "## FileService: Failed to update file cache") Timber.e(failure, "## FileService: Failed to update file cache")

View file

@ -177,7 +177,7 @@ internal class DefaultSendService @AssistedInject constructor(
val attachmentData = ContentAttachmentData( val attachmentData = ContentAttachmentData(
size = messageContent.info!!.size, size = messageContent.info!!.size,
mimeType = messageContent.info.mimeType!!, mimeType = messageContent.info.mimeType!!,
name = messageContent.body, name = messageContent.getFileName(),
queryUri = Uri.parse(messageContent.url), queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.FILE type = ContentAttachmentData.Type.FILE
) )
@ -210,6 +210,8 @@ internal class DefaultSendService @AssistedInject constructor(
override fun cancelSend(eventId: String) { override fun cancelSend(eventId: String) {
cancelSendTracker.markLocalEchoForCancel(eventId, roomId) cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
// This is maybe the current task, so cancel it too
eventSenderProcessor.cancel(eventId, roomId)
taskExecutor.executorScope.launch { taskExecutor.executorScope.launch {
localEchoRepository.deleteFailedEcho(roomId, eventId) localEchoRepository.deleteFailedEcho(roomId, eventId)
} }

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.room.send.queue package org.matrix.android.sdk.internal.session.room.send.queue
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.SessionParams
@ -106,17 +107,21 @@ internal class EventSenderProcessor @Inject constructor(
// non blocking add to queue // non blocking add to queue
sendingQueue.add(task) sendingQueue.add(task)
markAsManaged(task) markAsManaged(task)
return object : Cancelable { return task
override fun cancel() { }
task.cancel()
} fun cancel(eventId: String, roomId: String) {
} (currentTask as? SendEventQueuedTask)
?.takeIf { it -> it.event.eventId == eventId && it.event.roomId == roomId }
?.cancel()
} }
companion object { companion object {
private const val RETRY_WAIT_TIME_MS = 10_000L private const val RETRY_WAIT_TIME_MS = 10_000L
} }
private var currentTask: QueuedTask? = null
private var sendingQueue = LinkedBlockingQueue<QueuedTask>() private var sendingQueue = LinkedBlockingQueue<QueuedTask>()
private var networkAvailableLock = Object() private var networkAvailableLock = Object()
@ -129,6 +134,7 @@ internal class EventSenderProcessor @Inject constructor(
while (!isInterrupted) { while (!isInterrupted) {
Timber.v("## SendThread wait for task to process") Timber.v("## SendThread wait for task to process")
val task = sendingQueue.take() val task = sendingQueue.take()
.also { currentTask = it }
Timber.v("## SendThread Found task to process $task") Timber.v("## SendThread Found task to process $task")
if (task.isCancelled()) { if (task.isCancelled()) {
@ -183,6 +189,10 @@ internal class EventSenderProcessor @Inject constructor(
task.onTaskFailed() task.onTaskFailed()
throw InterruptedException() throw InterruptedException()
} }
exception is CancellationException -> {
Timber.v("## SendThread task has been cancelled")
break@retryLoop
}
else -> { else -> {
Timber.v("## SendThread retryLoop Un-Retryable error, try next task") Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
// this task is in error, check next one? // this task is in error, check next one?

View file

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.session.room.send.queue package org.matrix.android.sdk.internal.session.room.send.queue
import android.content.Context 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.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState

View file

@ -16,14 +16,26 @@
package org.matrix.android.sdk.internal.session.room.send.queue 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 var retryCount = 0
abstract suspend fun execute() private var hasBeenCancelled: Boolean = false
suspend fun execute() {
if (!isCancelled()) {
doExecute()
}
}
abstract suspend fun doExecute()
abstract fun onTaskFailed() abstract fun onTaskFailed()
abstract fun isCancelled() : Boolean open fun isCancelled() = hasBeenCancelled
abstract fun cancel() final override fun cancel() {
hasBeenCancelled = true
}
} }

View file

@ -22,20 +22,18 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
internal class RedactQueuedTask( internal class RedactQueuedTask(
val toRedactEventId: String, private val toRedactEventId: String,
val redactionLocalEchoId: String, val redactionLocalEchoId: String,
val roomId: String, private val roomId: String,
val reason: String?, private val reason: String?,
val redactEventTask: RedactEventTask, private val redactEventTask: RedactEventTask,
val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
val cancelSendTracker: CancelSendTracker private val cancelSendTracker: CancelSendTracker
) : QueuedTask() { ) : QueuedTask() {
private var _isCancelled: Boolean = false override fun toString() = "[RedactQueuedTask $redactionLocalEchoId]"
override fun toString() = "[RedactEventRunnableTask $redactionLocalEchoId]" override suspend fun doExecute() {
override suspend fun execute() {
redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason)) redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason))
} }
@ -44,10 +42,6 @@ internal class RedactQueuedTask(
} }
override fun isCancelled(): Boolean { override fun isCancelled(): Boolean {
return _isCancelled || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId) return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId)
}
override fun cancel() {
_isCancelled = true
} }
} }

View file

@ -33,11 +33,9 @@ internal class SendEventQueuedTask(
val cancelSendTracker: CancelSendTracker val cancelSendTracker: CancelSendTracker
) : QueuedTask() { ) : QueuedTask() {
private var _isCancelled: Boolean = false override fun toString() = "[SendEventQueuedTask ${event.eventId}]"
override fun toString() = "[SendEventRunnableTask ${event.eventId}]" override suspend fun doExecute() {
override suspend fun execute() {
sendEventTask.execute(SendEventTask.Params(event, encrypt)) sendEventTask.execute(SendEventTask.Params(event, encrypt))
} }
@ -56,10 +54,6 @@ internal class SendEventQueuedTask(
} }
override fun isCancelled(): Boolean { override fun isCancelled(): Boolean {
return _isCancelled || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId) return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId)
}
override fun cancel() {
_isCancelled = true
} }
} }

View file

@ -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.api.session.sync.SyncState
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
import org.matrix.android.sdk.internal.session.sync.SyncTask 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.BackgroundDetectionObserver
import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.Debouncer
import org.matrix.android.sdk.internal.util.createUIHandler 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 private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
internal class SyncThread @Inject constructor(private val syncTask: SyncTask, internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val typingUsersTracker: DefaultTypingUsersTracker,
private val networkConnectivityChecker: NetworkConnectivityChecker, private val networkConnectivityChecker: NetworkConnectivityChecker,
private val backgroundDetectionObserver: BackgroundDetectionObserver, private val backgroundDetectionObserver: BackgroundDetectionObserver,
private val activeCallHandler: ActiveCallHandler private val activeCallHandler: ActiveCallHandler
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { ) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.Idle private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData<SyncState>(state) private var liveState = MutableLiveData(state)
private val lock = Object() private val lock = Object()
private val syncScope = CoroutineScope(SupervisorJob()) private val syncScope = CoroutineScope(SupervisorJob())
private val debouncer = Debouncer(createUIHandler()) private val debouncer = Debouncer(createUIHandler())
@ -231,7 +229,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
return return
} }
state = newState state = newState
debouncer.debounce("post_state", Runnable { debouncer.debounce("post_state", {
liveState.value = newState liveState.value = newState
}, 150) }, 150)
} }

View file

@ -25,6 +25,9 @@ import java.io.InputStream
*/ */
@WorkerThread @WorkerThread
fun writeToFile(inputStream: InputStream, outputFile: File) { fun writeToFile(inputStream: InputStream, outputFile: File) {
// Ensure the parent folder exists, else it will crash
outputFile.parentFile?.mkdirs()
outputFile.outputStream().use { outputFile.outputStream().use {
inputStream.copyTo(it) inputStream.copyTo(it)
} }

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.util package org.matrix.android.sdk.internal.util
import java.security.MessageDigest import java.security.MessageDigest
import java.util.Locale
/** /**
* Compute a Hash of a String, using md5 algorithm * Compute a Hash of a String, using md5 algorithm
@ -26,7 +27,7 @@ fun String.md5() = try {
digest.update(toByteArray()) digest.update(toByteArray())
digest.digest() digest.digest()
.joinToString("") { String.format("%02X", it) } .joinToString("") { String.format("%02X", it) }
.toLowerCase() .toLowerCase(Locale.ROOT)
} catch (exc: Exception) { } catch (exc: Exception) {
// Should not happen, but just in case // Should not happen, but just in case
hashCode().toString() hashCode().toString()

View file

@ -43,8 +43,8 @@ android {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0' 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.0' implementation 'androidx.exifinterface:exifinterface:1.3.1'
// Log // Log
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'

View file

@ -315,9 +315,8 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06" implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06"
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$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.sharetarget:sharetarget:1.0.0"
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
@ -362,11 +361,11 @@ dependencies {
implementation "io.arrow-kt:arrow-core:$arrow_version" implementation "io.arrow-kt:arrow-core:$arrow_version"
// Pref // Pref
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
// UI // UI
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' 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 'me.gujun.android:span:1.7'
implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version" implementation "io.noties.markwon:html:$markwon_version"
@ -374,7 +373,7 @@ dependencies {
implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1' implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version" 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 // Custom Tab
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.browser:browser:1.2.0'
@ -418,7 +417,7 @@ dependencies {
kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0' kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0'
// gplay flavor only // 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-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@ -441,6 +440,10 @@ dependencies {
implementation 'com.google.zxing:core:3.3.3' implementation 'com.google.zxing:core:3.3.3'
implementation 'me.dm7.barcodescanner:zxing:1.9.13' 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 // TESTS
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
testImplementation "org.amshove.kluent:kluent-android:$kluent_version" testImplementation "org.amshove.kluent:kluent-android:$kluent_version"

View file

@ -41,6 +41,7 @@
<issue id="ObsoleteSdkInt" severity="error" /> <issue id="ObsoleteSdkInt" severity="error" />
<issue id="Recycle" severity="error" /> <issue id="Recycle" severity="error" />
<issue id="KotlinPropertyAccess" severity="error" /> <issue id="KotlinPropertyAccess" severity="error" />
<issue id="DefaultLocale" severity="error" />
<issue id="InvalidPackage"> <issue id="InvalidPackage">
<!-- Ignore error from HtmlCompressor lib --> <!-- Ignore error from HtmlCompressor lib -->
@ -52,6 +53,9 @@
<!-- Manifest --> <!-- Manifest -->
<issue id="PermissionImpliesUnsupportedChromeOsHardware" severity="error" /> <issue id="PermissionImpliesUnsupportedChromeOsHardware" severity="error" />
<!-- Dependencies -->
<issue id="KtxExtensionAvailable" severity="error" />
<!-- Timber --> <!-- Timber -->
<!-- This rule is failing on CI because it's marked as unknwown rule id :/--> <!-- This rule is failing on CI because it's marked as unknwown rule id :/-->
<!-- <issue id="BinaryOperationInTimber" severity="error" />--> <!-- <issue id="BinaryOperationInTimber" severity="error" />-->

View file

@ -18,7 +18,7 @@ package im.vector.app.gplay.features.settings.troubleshoot
import android.content.Intent import android.content.Intent
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity 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.R
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.startAddGoogleAccountIntent import im.vector.app.core.utils.startAddGoogleAccountIntent
@ -36,29 +36,33 @@ class TestFirebaseToken @Inject constructor(private val context: AppCompatActivi
override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) { override fun perform(activityResultLauncher: ActivityResultLauncher<Intent>) {
status = TestStatus.RUNNING status = TestStatus.RUNNING
try { try {
FirebaseInstanceId.getInstance().instanceId FirebaseMessaging.getInstance().token
.addOnCompleteListener(context) { task -> .addOnCompleteListener(context) { task ->
if (!task.isSuccessful) { 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) // Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated)
if ("SERVICE_NOT_AVAILABLE".equals(errorMsg)) { description = when (val errorMsg = task.exception?.localizedMessage ?: "Unknown") {
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg) "SERVICE_NOT_AVAILABLE" -> {
} else if ("TOO_MANY_REGISTRATIONS".equals(errorMsg)) { stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg)
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg) }
} else if ("ACCOUNT_MISSING".equals(errorMsg)) { "TOO_MANY_REGISTRATIONS" -> {
description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg) stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg)
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) { }
override fun doFix() { "ACCOUNT_MISSING" -> {
startAddGoogleAccountIntent(context, activityResultLauncher) 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 status = TestStatus.FAILED
} else { } else {
task.result?.token?.let { token -> 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) description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok)
Timber.e("Retrieved FCM token success [$tok].") Timber.e("Retrieved FCM token success [$tok].")
// Ensure it is well store in our local storage // Ensure it is well store in our local storage

View file

@ -21,7 +21,7 @@ import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability 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.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.DefaultSharedPreferences 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' // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) { if (checkPlayServices(activity)) {
try { try {
FirebaseInstanceId.getInstance().instanceId FirebaseMessaging.getInstance().token
.addOnSuccessListener(activity) { instanceIdResult -> .addOnSuccessListener { token ->
storeFcmToken(activity, instanceIdResult.token) storeFcmToken(activity, token)
if (registerPusher) { 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) { } catch (e: Throwable) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
} }

View file

@ -347,11 +347,6 @@ SOFTWARE.
<br/> <br/>
Copyright 2017 Gabriel Ittner. Copyright 2017 Gabriel Ittner.
</li> </li>
<li>
<b>Android-multipicker-library</b>
<br/>
Copyright 2018 Kumar Bibek
</li>
<li> <li>
<b>htmlcompressor</b> <b>htmlcompressor</b>
<br/> <br/>
@ -390,6 +385,11 @@ SOFTWARE.
<br/> <br/>
Copyright 2018, Aleksandr Nikiforov Copyright 2018, Aleksandr Nikiforov
</li> </li>
<li>
<b>Emoji</b>
<br/>
Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors
</li>
</ul> </ul>
<pre> <pre>
Apache License Apache License

View file

@ -36,6 +36,8 @@ import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
import com.gabrielittner.threetenbp.LazyThreeTen 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.ActiveSessionHolder
import im.vector.app.core.di.DaggerVectorComponent import im.vector.app.core.di.DaggerVectorComponent
import im.vector.app.core.di.HasVectorInjector import im.vector.app.core.di.HasVectorInjector
@ -184,6 +186,8 @@ class VectorApplication :
addAction(Intent.ACTION_SCREEN_OFF) addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON) addAction(Intent.ACTION_SCREEN_ON)
}) })
EmojiManager.install(GoogleEmojiProvider())
} }
private fun enableStrictModeIfNeeded() { private fun enableStrictModeIfNeeded() {

View file

@ -28,7 +28,6 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.file.FileService
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -110,11 +109,9 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
} }
// Use the file vector service, will avoid flickering and redownload after upload // Use the file vector service, will avoid flickering and redownload after upload
fileService.downloadFile( fileService.downloadFile(
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
mimeType = data.mimeType,
id = data.eventId,
url = data.url,
fileName = data.filename, fileName = data.filename,
mimeType = data.mimeType,
url = data.url,
elementToDecrypt = data.elementToDecrypt, elementToDecrypt = data.elementToDecrypt,
callback = object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {

View file

@ -21,6 +21,7 @@ import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import im.vector.app.core.utils.getFileExtension import im.vector.app.core.utils.getFileExtension
import timber.log.Timber import timber.log.Timber
import java.util.Locale
/** /**
* Returns the mimetype from a uri. * Returns the mimetype from a uri.
@ -44,7 +45,7 @@ fun getMimeTypeFromUri(context: Context, uri: Uri): String? {
if (null != mimeType) { if (null != mimeType) {
// the mimetype is sometimes in uppercase. // the mimetype is sometimes in uppercase.
mimeType = mimeType.toLowerCase() mimeType = mimeType.toLowerCase(Locale.ROOT)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to open resource input stream") Timber.e(e, "Failed to open resource input stream")

View file

@ -43,7 +43,7 @@ abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : V
* so you can use this in a switchMap or a flatMap * so you can use this in a switchMap or a flatMap
*/ */
// False positive // False positive
@Suppress("USELESS_CAST") @Suppress("USELESS_CAST", "NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER")
fun <T> Single<T>.toAsync(stateReducer: S.(Async<T>) -> S): Single<Async<T>> { fun <T> Single<T>.toAsync(stateReducer: S.(Async<T>) -> S): Single<Async<T>> {
setState { stateReducer(Loading()) } setState { stateReducer(Loading()) }
return map { Success(it) as Async<T> } return map { Success(it) as Async<T> }
@ -56,7 +56,7 @@ abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : V
* so you can use this in a switchMap or a flatMap * so you can use this in a switchMap or a flatMap
*/ */
// False positive // False positive
@Suppress("USELESS_CAST") @Suppress("USELESS_CAST", "NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER")
fun <T> Observable<T>.toAsync(stateReducer: S.(Async<T>) -> S): Observable<Async<T>> { fun <T> Observable<T>.toAsync(stateReducer: S.(Async<T>) -> S): Observable<Async<T>> {
setState { stateReducer(Loading()) } setState { stateReducer(Loading()) }
return map { Success(it) as Async<T> } return map { Success(it) as Async<T> }

View file

@ -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<State : MvRxState>(initialState: State) :
VectorViewModel<State, EmptyAction, EmptyViewEvents>(initialState) {
override fun handle(action: EmptyAction) {
// No op
}
}

View file

@ -44,7 +44,7 @@ open class BehaviorDataSource<T>(private val defaultValue: T? = null) : MutableD
} }
override fun post(value: T) { override fun post(value: T) {
behaviorRelay.accept(value) behaviorRelay.accept(value!!)
} }
private fun createRelay(): BehaviorRelay<T> { private fun createRelay(): BehaviorRelay<T> {
@ -68,6 +68,6 @@ open class PublishDataSource<T> : MutableDataSource<T> {
} }
override fun post(value: T) { override fun post(value: T) {
publishRelay.accept(value) publishRelay.accept(value!!)
} }
} }

View file

@ -19,6 +19,7 @@ package im.vector.app.core.utils
import android.content.Context import android.content.Context
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.Locale
// Implementation should return true in case of success // Implementation should return true in case of success
typealias ActionOnFile = (file: File) -> Boolean typealias ActionOnFile = (file: File) -> Boolean
@ -113,7 +114,7 @@ fun getFileExtension(fileUri: String): String? {
val ext = filename.substring(dotPos + 1) val ext = filename.substring(dotPos + 1)
if (ext.isNotBlank()) { if (ext.isNotBlank()) {
return ext.toLowerCase() return ext.toLowerCase(Locale.ROOT)
} }
} }
} }

View file

@ -59,7 +59,6 @@ data class AttachmentsPreviewArgs(
) : Parcelable ) : Parcelable
class AttachmentsPreviewFragment @Inject constructor( class AttachmentsPreviewFragment @Inject constructor(
val viewModelFactory: AttachmentsPreviewViewModel.Factory,
private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController, private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController,
private val attachmentBigPreviewController: AttachmentBigPreviewController, private val attachmentBigPreviewController: AttachmentBigPreviewController,
private val colorProvider: ColorProvider private val colorProvider: ColorProvider

View file

@ -17,31 +17,12 @@
package im.vector.app.features.attachments.preview 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.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState) class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState)
: VectorViewModel<AttachmentsPreviewViewState, AttachmentsPreviewAction, AttachmentsPreviewViewEvents>(initialState) { : VectorViewModel<AttachmentsPreviewViewState, AttachmentsPreviewAction, AttachmentsPreviewViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: AttachmentsPreviewViewState): AttachmentsPreviewViewModel
}
companion object : MvRxViewModelFactory<AttachmentsPreviewViewModel, AttachmentsPreviewViewState> {
@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) { override fun handle(action: AttachmentsPreviewAction) {
when (action) { when (action) {
is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action) is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action)

View file

@ -71,12 +71,18 @@ class HomeActivityViewModel @AssistedInject constructor(
private var onceTrusted = false private var onceTrusted = false
init { init {
cleanupFiles()
observeInitialSync() observeInitialSync()
mayBeInitializeCrossSigning() mayBeInitializeCrossSigning()
checkSessionPushIsOn() checkSessionPushIsOn()
observeCrossSigningReset() observeCrossSigningReset()
} }
private fun cleanupFiles() {
// Mitigation: delete all cached decrypted files each time the application is started.
activeSessionHolder.getSafeActiveSession()?.fileService()?.clearDecryptedCache()
}
private fun observeCrossSigningReset() { private fun observeCrossSigningReset() {
val safeActiveSession = activeSessionHolder.getSafeActiveSession() ?: return val safeActiveSession = activeSessionHolder.getSafeActiveSession() ?: return

View file

@ -53,7 +53,6 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.epoxy.addGlidePreloader import com.airbnb.epoxy.addGlidePreloader
@ -69,6 +68,7 @@ import com.airbnb.mvrx.withState
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import com.vanniktech.emoji.EmojiPopup
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.ConfirmationDialogBuilder
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
@ -175,7 +175,6 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event 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.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.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.MessageContent
@ -186,7 +185,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent 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.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.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -195,7 +193,6 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem 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.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import timber.log.Timber import timber.log.Timber
@ -290,8 +287,6 @@ class RoomDetailFragment @Inject constructor(
private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var attachmentsHelper: AttachmentsHelper
private lateinit var keyboardStateUtils: KeyboardStateUtils private lateinit var keyboardStateUtils: KeyboardStateUtils
@BindView(R.id.composerLayout)
lateinit var composerLayout: TextComposerView
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
private var lockSendButton = false private var lockSendButton = false
@ -312,6 +307,7 @@ class RoomDetailFragment @Inject constructor(
setupActiveCallView() setupActiveCallView()
setupJumpToBottomView() setupJumpToBottomView()
setupConfBannerView() setupConfBannerView()
setupEmojiPopup()
roomToolbarContentView.debouncedClicks { roomToolbarContentView.debouncedClicks {
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
@ -479,6 +475,20 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun setupEmojiPopup() {
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) }
.build(composerLayout.composerEditText)
composerLayout.composerEmojiButton.debouncedClicks {
emojiPopup.toggle()
}
}
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
} }
@ -1212,9 +1222,6 @@ class RoomDetailFragment @Inject constructor(
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
timelineEventController.update(state) timelineEventController.update(state)
inviteView.visibility = View.GONE 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.tombstoneEvent == null) {
if (state.canSendMessage) { if (state.canSendMessage) {
composerLayout.visibility = View.VISIBLE composerLayout.visibility = View.VISIBLE
@ -1670,12 +1677,7 @@ class RoomDetailFragment @Inject constructor(
shareText(requireContext(), action.messageContent.body) shareText(requireContext(), action.messageContent.body)
} else if (action.messageContent is MessageWithAttachmentContent) { } else if (action.messageContent is MessageWithAttachmentContent) {
session.fileService().downloadFile( session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, messageContent = action.messageContent,
id = action.eventId,
fileName = action.messageContent.body,
mimeType = action.messageContent.mimeType,
url = action.messageContent.getFileUrl(),
elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
callback = object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {
if (isAdded) { if (isAdded) {
@ -1705,12 +1707,7 @@ class RoomDetailFragment @Inject constructor(
return return
} }
session.fileService().downloadFile( session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, messageContent = action.messageContent,
id = action.eventId,
fileName = action.messageContent.body,
mimeType = action.messageContent.mimeType,
url = action.messageContent.getFileUrl(),
elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
callback = object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {
if (isAdded) { if (isAdded) {

View file

@ -70,7 +70,6 @@ import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.toContent 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.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.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
@ -81,7 +80,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType 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.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.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
@ -93,7 +91,6 @@ import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.toOptional 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.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
@ -1019,10 +1016,10 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
val mxcUrl = action.messageFileContent.getFileUrl() val mxcUrl = action.messageFileContent.getFileUrl() ?: return
val isLocalSendingFile = action.senderId == session.myUserId val isLocalSendingFile = action.senderId == session.myUserId
&& mxcUrl?.startsWith("content://") ?: false && mxcUrl.startsWith("content://")
val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false val isDownloaded = session.fileService().isFileInCache(action.messageFileContent)
if (isLocalSendingFile) { if (isLocalSendingFile) {
tryOrNull { Uri.parse(mxcUrl) }?.let { tryOrNull { Uri.parse(mxcUrl) }?.let {
_viewEvents.post(RoomDetailViewEvents.OpenFile( _viewEvents.post(RoomDetailViewEvents.OpenFile(
@ -1033,7 +1030,7 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} else if (isDownloaded) { } else if (isDownloaded) {
// we can open it // we can open it
session.fileService().getTemporarySharableURI(mxcUrl!!, action.messageFileContent.mimeType)?.let { uri -> session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri ->
_viewEvents.post(RoomDetailViewEvents.OpenFile( _viewEvents.post(RoomDetailViewEvents.OpenFile(
action.messageFileContent.mimeType, action.messageFileContent.mimeType,
uri, uri,
@ -1042,12 +1039,7 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} else { } else {
session.fileService().downloadFile( session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, messageContent = action.messageFileContent,
id = action.eventId,
fileName = action.messageFileContent.getFileName(),
mimeType = action.messageFileContent.mimeType,
url = mxcUrl,
elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
callback = object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {
_viewEvents.post(RoomDetailViewEvents.DownloadFileState( _viewEvents.post(RoomDetailViewEvents.DownloadFileState(

View file

@ -24,16 +24,16 @@ import android.text.Editable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection import android.view.inputmethod.InputConnection
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputConnectionCompat
import com.vanniktech.emoji.EmojiEditText
import im.vector.app.core.extensions.ooi import im.vector.app.core.extensions.ooi
import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.platform.SimpleTextWatcher
import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillImageSpan
import timber.log.Timber import timber.log.Timber
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) 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 { interface Callback {
fun onRichContentSelected(contentUri: Uri): Boolean fun onRichContentSelected(contentUri: Uri): Boolean

View file

@ -72,8 +72,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
@BindView(R.id.composerEditText) @BindView(R.id.composerEditText)
lateinit var composerEditText: ComposerEditText lateinit var composerEditText: ComposerEditText
@BindView(R.id.composer_avatar_view) @BindView(R.id.composer_emoji)
lateinit var composerAvatarImageView: ImageView lateinit var composerEmojiButton: ImageButton
@BindView(R.id.composer_shield) @BindView(R.id.composer_shield)
lateinit var composerShieldImageView: ImageView lateinit var composerShieldImageView: ImageView

View file

@ -82,10 +82,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
when (cryptoError) { when (cryptoError) {
MXCryptoError.ErrorType.KEYS_WITHHELD -> { MXCryptoError.ErrorType.KEYS_WITHHELD -> {
span { span {
apply { drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let { image(it, "baseline")
image(it, "baseline") +" "
}
} }
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) { span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) {
textStyle = "italic" textStyle = "italic"
@ -95,10 +94,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
} }
else -> { else -> {
span { span {
apply { drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let { image(it, "baseline")
image(it, "baseline") +" "
}
} }
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) { span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) {
textStyle = "italic" textStyle = "italic"

View file

@ -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.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_BUTTONS
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL 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.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.session.room.timeline.getLastMessageContent
@ -204,7 +205,12 @@ class MessageItemFactory @Inject constructor(
return MessageFileItem_() return MessageFileItem_()
.attributes(attributes) .attributes(attributes)
.izLocalFile(fileUrl.isLocalFile()) .izLocalFile(fileUrl.isLocalFile())
.izDownloaded(session.fileService().isFileInCache(fileUrl, messageContent.mimeType)) .izDownloaded(session.fileService().isFileInCache(
fileUrl,
messageContent.getFileName(),
messageContent.mimeType,
messageContent.encryptedFileInfo?.toElementToDecrypt())
)
.mxcUrl(fileUrl) .mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
@ -264,7 +270,7 @@ class MessageItemFactory @Inject constructor(
.attributes(attributes) .attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.izLocalFile(messageContent.getFileUrl().isLocalFile()) .izLocalFile(messageContent.getFileUrl().isLocalFile())
.izDownloaded(session.fileService().isFileInCache(mxcUrl, messageContent.mimeType)) .izDownloaded(session.fileService().isFileInCache(messageContent))
.mxcUrl(mxcUrl) .mxcUrl(mxcUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)

View file

@ -153,12 +153,10 @@ abstract class BaseAttachmentProvider<Type>(
} else { } else {
target.onVideoFileLoading(info.uid) target.onVideoFileLoading(info.uid)
fileService.downloadFile( fileService.downloadFile(
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
id = data.eventId,
mimeType = data.mimeType,
elementToDecrypt = data.elementToDecrypt,
fileName = data.filename, fileName = data.filename,
mimeType = data.mimeType,
url = data.url, url = data.url,
elementToDecrypt = data.elementToDecrypt,
callback = object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {
target.onVideoFileReady(info.uid, data) target.onVideoFileReady(info.uid, data)

View file

@ -77,11 +77,9 @@ class DataAttachmentRoomProvider(
override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
val item = getItem(position) val item = getItem(position)
fileService.downloadFile( fileService.downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
id = item.eventId,
fileName = item.filename, fileName = item.filename,
mimeType = item.mimeType, mimeType = item.mimeType,
url = item.url ?: "", url = item.url,
elementToDecrypt = item.elementToDecrypt, elementToDecrypt = item.elementToDecrypt,
callback = object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {

View file

@ -23,6 +23,7 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import com.bumptech.glide.load.DataSource 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.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
@ -142,6 +143,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
GlideApp GlideApp
.with(contextView) .with(contextView)
.load(data) .load(data)
.diskCacheStrategy(DiskCacheStrategy.NONE)
} else { } else {
// Clear image // Clear image
val resolvedUrl = resolveUrl(data) val resolvedUrl = resolveUrl(data)
@ -196,6 +198,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
GlideApp GlideApp
.with(imageView) .with(imageView)
.load(data) .load(data)
.diskCacheStrategy(DiskCacheStrategy.NONE)
} else { } else {
// Clear image // Clear image
val resolvedUrl = resolveUrl(data) val resolvedUrl = resolveUrl(data)
@ -227,14 +230,16 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView) .into(imageView)
} }
fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> { private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
return createGlideRequest(data, mode, GlideApp.with(imageView), size) return createGlideRequest(data, mode, GlideApp.with(imageView), size)
} }
fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest<Drawable> { fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest<Drawable> {
return if (data.elementToDecrypt != null) { return if (data.elementToDecrypt != null) {
// Encrypted image // Encrypted image
glideRequests.load(data) glideRequests
.load(data)
.diskCacheStrategy(DiskCacheStrategy.NONE)
} else { } else {
// Clear image // Clear image
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()

View file

@ -125,8 +125,6 @@ class RoomEventsAttachmentProvider(
as? MessageWithAttachmentContent as? MessageWithAttachmentContent
?: return@let ?: return@let
fileService.downloadFile( fileService.downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
id = timelineEvent.eventId,
fileName = messageContent.body, fileName = messageContent.body,
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(), url = messageContent.getFileUrl(),

View file

@ -27,7 +27,6 @@ import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.utils.isLocalFile import im.vector.app.core.utils.isLocalFile
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import org.matrix.android.sdk.api.MatrixCallback 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 org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -76,8 +75,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
activeSessionHolder.getActiveSession().fileService() activeSessionHolder.getActiveSession().fileService()
.downloadFile( .downloadFile(
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
id = data.eventId,
fileName = data.filename, fileName = data.filename,
mimeType = data.mimeType, mimeType = data.mimeType,
url = data.url, url = data.url,
@ -116,8 +113,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
activeSessionHolder.getActiveSession().fileService() activeSessionHolder.getActiveSession().fileService()
.downloadFile( .downloadFile(
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
id = data.eventId,
fileName = data.filename, fileName = data.filename,
mimeType = data.mimeType, mimeType = data.mimeType,
url = data.url, url = data.url,

View file

@ -74,6 +74,10 @@ class PinFragment @Inject constructor(
Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show() 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) { override fun onCodeCreated(encodedCode: String) {
lifecycleScope.launch { lifecycleScope.launch {
pinCodeStore.storeEncodedPin(encodedCode) pinCodeStore.storeEncodedPin(encodedCode)

View file

@ -37,7 +37,6 @@ class RoomHistoryVisibilityBottomSheet : BottomSheetGeneric<RoomHistoryVisibilit
private lateinit var roomHistoryVisibilitySharedActionViewModel: RoomHistoryVisibilitySharedActionViewModel private lateinit var roomHistoryVisibilitySharedActionViewModel: RoomHistoryVisibilitySharedActionViewModel
@Inject lateinit var controller: RoomHistoryVisibilityController @Inject lateinit var controller: RoomHistoryVisibilityController
@Inject lateinit var roomHistoryVisibilityViewModelFactory: RoomHistoryVisibilityViewModel.Factory
private val viewModel: RoomHistoryVisibilityViewModel by fragmentViewModel(RoomHistoryVisibilityViewModel::class) private val viewModel: RoomHistoryVisibilityViewModel by fragmentViewModel(RoomHistoryVisibilityViewModel::class)
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {

View file

@ -16,32 +16,7 @@
package im.vector.app.features.roomprofile.settings.historyvisibility package im.vector.app.features.roomprofile.settings.historyvisibility
import com.airbnb.mvrx.FragmentViewModelContext import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel
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.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
class RoomHistoryVisibilityViewModel @AssistedInject constructor(@Assisted initialState: RoomHistoryVisibilityState) class RoomHistoryVisibilityViewModel(initialState: RoomHistoryVisibilityState)
: VectorViewModel<RoomHistoryVisibilityState, EmptyAction, EmptyViewEvents>(initialState) { : BottomSheetGenericViewModel<RoomHistoryVisibilityState>(initialState)
@AssistedInject.Factory
interface Factory {
fun create(initialState: RoomHistoryVisibilityState): RoomHistoryVisibilityViewModel
}
companion object : MvRxViewModelFactory<RoomHistoryVisibilityViewModel, RoomHistoryVisibilityState> {
@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
}
}

View file

@ -39,7 +39,6 @@ class RoomJoinRuleBottomSheet : BottomSheetGeneric<RoomJoinRuleState, RoomJoinRu
private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel
@Inject lateinit var controller: RoomJoinRuleController @Inject lateinit var controller: RoomJoinRuleController
@Inject lateinit var roomJoinRuleViewModelFactory: RoomJoinRuleViewModel.Factory
private val viewModel: RoomJoinRuleViewModel by fragmentViewModel(RoomJoinRuleViewModel::class) private val viewModel: RoomJoinRuleViewModel by fragmentViewModel(RoomJoinRuleViewModel::class)
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {

View file

@ -16,32 +16,7 @@
package im.vector.app.features.roomprofile.settings.joinrule package im.vector.app.features.roomprofile.settings.joinrule
import com.airbnb.mvrx.FragmentViewModelContext import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel
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.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
class RoomJoinRuleViewModel @AssistedInject constructor(@Assisted initialState: RoomJoinRuleState) class RoomJoinRuleViewModel(initialState: RoomJoinRuleState)
: VectorViewModel<RoomJoinRuleState, EmptyAction, EmptyViewEvents>(initialState) { : BottomSheetGenericViewModel<RoomJoinRuleState>(initialState)
@AssistedInject.Factory
interface Factory {
fun create(initialState: RoomJoinRuleState): RoomJoinRuleViewModel
}
companion object : MvRxViewModelFactory<RoomJoinRuleViewModel, RoomJoinRuleState> {
@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
}
}

View file

@ -30,10 +30,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session 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.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.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap import org.matrix.android.sdk.rx.unwrap
@ -134,12 +131,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
try { try {
val file = awaitCallback<File> { val file = awaitCallback<File> {
session.fileService().downloadFile( session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, messageContent = action.uploadEvent.contentWithAttachmentContent,
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(),
callback = it callback = it
) )
} }
@ -155,12 +147,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
try { try {
val file = awaitCallback<File> { val file = awaitCallback<File> {
session.fileService().downloadFile( session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, messageContent = action.uploadEvent.contentWithAttachmentContent,
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(),
callback = it) callback = it)
} }
_viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body)) _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body))

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@color/riotx_accent" />
</shape>
</item>
<item android:drawable="?attr/selectableItemBackground" />
</layer-list>

View file

@ -1,7 +1,21 @@
<vector android:autoMirrored="true" android:height="24dp" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="32dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="32dp"
<path android:fillColor="#ffffff" android:viewportWidth="32"
android:pathData="M21,12C21,16.9706 16.9706,21 12,21C7.0294,21 3,16.9706 3,12C3,7.0294 7.0294,3 12,3C16.9706,3 21,7.0294 21,12ZM8,10C6.8954,10 6,10.8954 6,12C6,13.1046 6.8954,14 8,14H10V16C10,17.1046 10.8954,18 12,18C13.1046,18 14,17.1046 14,16V14H16C17.1046,14 18,13.1046 18,12C18,10.8954 17.1046,10 16,10H14V8C14,6.8954 13.1046,6 12,6C10.8954,6 10,6.8954 10,8V10H8Z" android:viewportHeight="32">
android:strokeColor="#ffffff" android:strokeWidth="2"/> <path
android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:fillColor="#E3E8F0"/>
<path
android:pathData="M10.0009,16H22.0009"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#61708B"
android:strokeLineCap="round"/>
<path
android:pathData="M16.0009,10V22"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#61708B"
android:strokeLineCap="round"/>
</vector> </vector>

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
android:fillColor="#E3E8F0"/>
<path
android:pathData="M9.276,19.6667C10.7004,21.8728 13.1806,23.3333 16.0019,23.3333C18.8233,23.3333 21.3035,21.8728 22.7278,19.6667"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#61708B"
android:strokeLineCap="round"/>
<path
android:pathData="M18.6667,12.6667a2,2.6667 0,1 0,4 0a2,2.6667 0,1 0,-4 0z"
android:fillColor="#61708B"/>
<path
android:pathData="M8.6667,12.6667a2,2.6667 0,1 0,4 0a2,2.6667 0,1 0,-4 0z"
android:fillColor="#61708B"/>
</vector>

View file

@ -0,0 +1,4 @@
<vector android:height="32dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#E3E8F0" android:pathData="M20,5L4,5c-1.1,0 -1.99,0.9 -1.99,2L2,17c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,7c0,-1.1 -0.9,-2 -2,-2zM11,8h2v2h-2L11,8zM11,11h2v2h-2v-2zM8,8h2v2L8,10L8,8zM8,11h2v2L8,13v-2zM7,13L5,13v-2h2v2zM7,10L5,10L5,8h2v2zM16,17L8,17v-2h8v2zM16,13h-2v-2h2v2zM16,10h-2L14,8h2v2zM19,13h-2v-2h2v2zM19,10h-2L17,8h2v2z"/>
</vector>

View file

@ -77,9 +77,9 @@
android:alpha="0" android:alpha="0"
app:layout_constraintEnd_toStartOf="parent" app:layout_constraintEnd_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent" app:layout_constraintTop_toBottomOf="parent"
app:tint="?riotx_text_primary"
tools:ignore="MissingConstraints,MissingPrefix" tools:ignore="MissingConstraints,MissingPrefix"
tools:src="@drawable/ic_edit" tools:src="@drawable/ic_edit" />
app:tint="?riotx_text_primary" />
<ImageButton <ImageButton
@ -88,54 +88,50 @@
android:layout_height="22dp" android:layout_height="22dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:src="@drawable/ic_close_round" android:src="@drawable/ic_close_round"
app:tint="@color/riotx_notice"
android:visibility="invisible" android:visibility="invisible"
app:layout_constraintBottom_toTopOf="parent" app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintStart_toEndOf="parent" app:layout_constraintStart_toEndOf="parent"
tools:visibility="visible" app:tint="@color/riotx_notice"
tools:ignore="MissingPrefix" /> tools:ignore="MissingPrefix"
tools:visibility="visible" />
<ImageView <ImageButton
android:id="@+id/composer_avatar_view" android:id="@+id/attachmentButton"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:layout_marginStart="8dp" android:layout_margin="12dp"
android:layout_marginTop="8dp" android:background="?android:attr/selectableItemBackground"
android:layout_marginEnd="4dp" android:src="@drawable/ic_attachment"
app:layout_goneMarginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/composer_shield"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1" tools:ignore="MissingPrefix" />
app:layout_constraintEnd_toStartOf="@+id/composer_shield"
tools:src="@tools:sample/avatars" />
<ImageView <ImageView
android:id="@+id/composer_shield" android:id="@+id/composer_shield"
android:layout_width="16dp" android:layout_width="16dp"
android:layout_height="16dp" android:layout_height="16dp"
app:layout_constraintTop_toTopOf="@id/composer_avatar_view" app:layout_constraintBottom_toBottomOf="@id/attachmentButton"
app:layout_constraintEnd_toStartOf="@+id/composerEditText" app:layout_constraintEnd_toStartOf="@+id/attachmentButton"
app:layout_constraintBottom_toBottomOf="@id/composer_avatar_view" app:layout_constraintStart_toEndOf="@+id/attachmentButton"
app:layout_constraintStart_toEndOf="@+id/composer_avatar_view" app:layout_constraintTop_toTopOf="@id/attachmentButton"
tools:src="@drawable/ic_shield_black" tools:src="@drawable/ic_shield_black"
tools:visibility="visible" tools:visibility="visible" />
/>
<ImageButton <ImageButton
android:id="@+id/attachmentButton" android:id="@+id/composer_emoji"
android:layout_width="48dp" android:layout_width="32dp"
android:layout_height="48dp" android:layout_height="32dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:src="@drawable/ic_attachment" android:src="@drawable/ic_insert_emoji"
app:tint="?attr/colorAccent" app:layout_constraintBottom_toBottomOf="@id/sendButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/sendButton" app:layout_constraintEnd_toStartOf="@+id/sendButton"
app:layout_constraintStart_toEndOf="@id/composerEditText" app:layout_constraintStart_toEndOf="@id/composerEditText"
tools:ignore="MissingPrefix" /> app:layout_constraintTop_toTopOf="@id/sendButton"
app:layout_goneMarginEnd="8dp" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/composer_preview_barrier" android:id="@+id/composer_preview_barrier"
@ -149,14 +145,15 @@
<ImageButton <ImageButton
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="48dp" android:layout_width="36dp"
android:layout_height="48dp" android:layout_height="36dp"
android:background="?android:attr/selectableItemBackground" android:layout_marginEnd="12dp"
android:layout_marginBottom="10dp"
android:background="@drawable/bg_send"
android:src="@drawable/ic_send" android:src="@drawable/ic_send"
app:tint="?attr/colorAccent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/attachmentButton" app:layout_constraintStart_toEndOf="@id/composer_emoji"
tools:ignore="MissingPrefix" /> tools:ignore="MissingPrefix" />
<im.vector.app.features.home.room.detail.composer.ComposerEditText <im.vector.app.features.home.room.detail.composer.ComposerEditText
@ -168,7 +165,7 @@
android:nextFocusLeft="@id/composerEditText" android:nextFocusLeft="@id/composerEditText"
android:nextFocusUp="@id/composerEditText" android:nextFocusUp="@id/composerEditText"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/attachmentButton" app:layout_constraintEnd_toStartOf="@+id/composer_emoji"
app:layout_constraintStart_toEndOf="@+id/composer_shield" app:layout_constraintStart_toEndOf="@+id/composer_shield"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random" /> tools:text="@tools:sample/lorem/random" />

View file

@ -83,9 +83,9 @@
app:layout_constraintEnd_toEndOf="@id/composer_related_message_avatar_view" app:layout_constraintEnd_toEndOf="@id/composer_related_message_avatar_view"
app:layout_constraintStart_toStartOf="@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" app:layout_constraintTop_toBottomOf="@id/composer_related_message_avatar_view"
tools:src="@drawable/ic_edit"
app:tint="?riotx_text_primary" app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" /> tools:ignore="MissingPrefix"
tools:src="@drawable/ic_edit" />
<ImageButton <ImageButton
@ -94,51 +94,47 @@
android:layout_height="48dp" android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:src="@drawable/ic_close_round" android:src="@drawable/ic_close_round"
app:tint="@color/riotx_notice"
app:layout_constraintBottom_toBottomOf="@id/composer_related_message_preview" app:layout_constraintBottom_toBottomOf="@id/composer_related_message_preview"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/composer_related_message_preview" app:layout_constraintTop_toTopOf="@id/composer_related_message_preview"
app:tint="@color/riotx_notice"
tools:ignore="MissingPrefix" /> tools:ignore="MissingPrefix" />
<ImageView
android:id="@+id/composer_avatar_view"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="4dp"
app:layout_goneMarginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/composer_shield"
app:layout_constraintStart_toStartOf="parent"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/composer_shield"
android:layout_width="16dp"
android:layout_height="16dp"
app:layout_constraintTop_toTopOf="@id/composer_avatar_view"
app:layout_constraintEnd_toStartOf="@+id/composerEditText"
app:layout_constraintBottom_toBottomOf="@id/composer_avatar_view"
app:layout_constraintStart_toEndOf="@+id/composer_avatar_view"
tools:src="@drawable/ic_shield_black" />
<ImageButton <ImageButton
android:id="@+id/attachmentButton" android:id="@+id/attachmentButton"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:src="@drawable/ic_attachment" android:src="@drawable/ic_attachment"
app:tint="?attr/colorAccent" app:layout_constraintStart_toStartOf="@id/composerEditText"
app:layout_constraintEnd_toStartOf="@+id/sendButton"
app:layout_constraintTop_toBottomOf="parent" app:layout_constraintTop_toBottomOf="parent"
tools:ignore="MissingPrefix" /> tools:ignore="MissingPrefix" />
<ImageView
android:id="@+id/composer_shield"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="@id/composer_emoji"
app:layout_constraintEnd_toStartOf="@+id/composerEditText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/composer_emoji"
tools:src="@drawable/ic_shield_black" />
<ImageButton
android:id="@+id/composer_emoji"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:background="?android:attr/selectableItemBackground"
android:src="@drawable/ic_insert_emoji"
app:layout_constraintBottom_toBottomOf="@id/sendButton"
app:layout_constraintEnd_toStartOf="@+id/sendButton"
app:layout_constraintStart_toEndOf="@id/composerEditText"
app:layout_constraintTop_toTopOf="@id/sendButton"
app:layout_goneMarginEnd="8dp" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/composer_preview_barrier" android:id="@+id/composer_preview_barrier"
android:layout_width="0dp" android:layout_width="0dp"
@ -151,11 +147,13 @@
<ImageButton <ImageButton
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="48dp" android:layout_width="36dp"
android:layout_height="48dp" android:layout_height="36dp"
android:background="?android:attr/selectableItemBackground" android:background="@drawable/bg_send"
android:src="@drawable/ic_send" android:src="@drawable/ic_send"
app:tint="?attr/colorAccent" android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginEnd="12dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier" app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
@ -170,8 +168,8 @@
android:nextFocusLeft="@id/composerEditText" android:nextFocusLeft="@id/composerEditText"
android:nextFocusUp="@id/composerEditText" android:nextFocusUp="@id/composerEditText"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/sendButton" app:layout_constraintEnd_toStartOf="@+id/composer_emoji"
app:layout_constraintStart_toEndOf="@id/composer_shield" app:layout_constraintStart_toEndOf="@+id/composer_shield"
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier" app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
tools:text="@tools:sample/lorem/random" /> tools:text="@tools:sample/lorem/random" />

View file

@ -70,8 +70,8 @@
android:id="@+id/composer_related_message_action_image" android:id="@+id/composer_related_message_action_image"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
tools:ignore="MissingConstraints,MissingPrefix" app:tint="?riotx_text_primary"
app:tint="?riotx_text_primary" /> tools:ignore="MissingConstraints,MissingPrefix" />
<ImageButton <ImageButton
android:id="@+id/composer_related_message_close" android:id="@+id/composer_related_message_close"
@ -83,13 +83,13 @@
android:tint="@color/riotx_notice" android:tint="@color/riotx_notice"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />
<ImageButton
<ImageView android:id="@+id/composer_emoji"
android:id="@+id/composer_avatar_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
tools:ignore="MissingConstraints" android:background="?android:attr/selectableItemBackground"
tools:src="@tools:sample/avatars" /> android:src="@drawable/ic_insert_emoji"
tools:ignore="MissingConstraints" />
<ImageButton <ImageButton
android:id="@+id/attachmentButton" android:id="@+id/attachmentButton"
@ -98,7 +98,6 @@
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/option_send_files" android:contentDescription="@string/option_send_files"
android:src="@drawable/ic_attachment" android:src="@drawable/ic_attachment"
android:tint="?attr/colorAccent"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
@ -115,10 +114,9 @@
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:background="?android:attr/selectableItemBackground" android:background="@drawable/bg_send"
android:contentDescription="@string/send" android:contentDescription="@string/send"
android:src="@drawable/ic_send" android:src="@drawable/ic_send"
android:tint="?attr/colorAccent"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />
<im.vector.app.features.home.room.detail.composer.ComposerEditText <im.vector.app.features.home.room.detail.composer.ComposerEditText