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

View file

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

View file

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

View file

@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m
org.gradle.vfs.watch=true
vector.debugPrivateData=false
vector.httpLogLevel=NONE
vector.httpLogLevel=BASIC
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
#vector.debugPrivateData=true

View file

@ -36,9 +36,9 @@ android {
dependencies {
implementation project(":matrix-sdk-android")
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.fragment:fragment:1.3.0-beta01"
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
// Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.2"

View file

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

View file

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

View file

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

View file

@ -18,8 +18,12 @@ package org.matrix.android.sdk.api.session.file
import android.net.Uri
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import java.io.File
/**
@ -27,23 +31,6 @@ import java.io.File
*/
interface FileService {
enum class DownloadMode {
/**
* Download file in external storage
*/
TO_EXPORT,
/**
* Download file in cache
*/
FOR_INTERNAL_USE,
/**
* Download file in file provider path
*/
FOR_EXTERNAL_SHARE
}
enum class FileState {
IN_CACHE,
DOWNLOADING,
@ -54,34 +41,79 @@ interface FileService {
* Download a file.
* Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
*/
fun downloadFile(
downloadMode: DownloadMode,
id: String,
fileName: String,
mimeType: String?,
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>): Cancelable
fun downloadFile(fileName: String,
mimeType: String?,
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
* (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.
* 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()
/**
* Clears all the decrypted files by the service
*/
fun clearDecryptedCache()
/**
* 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.OlmUtility
import timber.log.Timber
import java.util.Locale
/**
* Represents an ongoing short code interactive key verification between two devices.
@ -344,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction(
}
protected fun hashUsingAgreedHashMethod(toHash: String): String? {
if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) {
if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) {
val olmUtil = OlmUtility()
val hashBytes = olmUtil.sha256(toHash)
olmUtil.releaseUtility()
@ -354,12 +355,11 @@ internal abstract class SASDefaultVerificationTransaction(
}
private fun macUsingAgreedMethod(message: String, info: String): String? {
if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) {
return getSAS().calculateMacLongKdf(message, info)
} else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) {
return getSAS().calculateMac(message, info)
return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) {
SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info)
SAS_MAC_SHA256 -> getSAS().calculateMac(message, info)
else -> null
}
return null
}
override fun getDecimalCodeRepresentation(): String {

View file

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

View file

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

View file

@ -16,14 +16,15 @@
package org.matrix.android.sdk.internal.network
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.internal.network.ssl.CertUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.internal.network.ssl.CertUtil
import retrofit2.Call
import retrofit2.awaitResponse
import timber.log.Timber
import java.io.IOException
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
@ -49,6 +50,9 @@ internal class Request<DATA : Any>(private val eventBus: EventBus?) {
throw response.toFailure(eventBus)
}
} catch (exception: Throwable) {
// Log some details about the request which has failed
Timber.e("Exception when executing request ${apiCall.request().method} ${apiCall.request().url.toString().substringBefore("?")}")
// Check if this is a certificateException
CertUtil.getCertificateException(exception)
// TODO Support certificate error once logged

View file

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

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

View file

@ -20,6 +20,9 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ContentUploadResponse(
internal data class ContentUploadResponse(
/**
* Required. The MXC URI to the uploaded content.
*/
@Json(name = "content_uri") val contentUri: String
)

View file

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

View file

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

View file

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

View file

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.session.room.send.queue
import android.content.Context
import org.matrix.android.sdk.api.auth.data.sessionId
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.room.send.SendState

View file

@ -16,14 +16,26 @@
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
abstract suspend fun execute()
private var hasBeenCancelled: Boolean = false
suspend fun execute() {
if (!isCancelled()) {
doExecute()
}
}
abstract suspend fun doExecute()
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
internal class RedactQueuedTask(
val toRedactEventId: String,
private val toRedactEventId: String,
val redactionLocalEchoId: String,
val roomId: String,
val reason: String?,
val redactEventTask: RedactEventTask,
val localEchoRepository: LocalEchoRepository,
val cancelSendTracker: CancelSendTracker
private val roomId: String,
private val reason: String?,
private val redactEventTask: RedactEventTask,
private val localEchoRepository: LocalEchoRepository,
private val cancelSendTracker: CancelSendTracker
) : QueuedTask() {
private var _isCancelled: Boolean = false
override fun toString() = "[RedactQueuedTask $redactionLocalEchoId]"
override fun toString() = "[RedactEventRunnableTask $redactionLocalEchoId]"
override suspend fun execute() {
override suspend fun doExecute() {
redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason))
}
@ -44,10 +42,6 @@ internal class RedactQueuedTask(
}
override fun isCancelled(): Boolean {
return _isCancelled || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId)
}
override fun cancel() {
_isCancelled = true
return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId)
}
}

View file

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

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

View file

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

View file

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

View file

@ -43,8 +43,8 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.fragment:fragment:1.3.0-beta01"
implementation 'androidx.exifinterface:exifinterface:1.3.0'
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01"
implementation 'androidx.exifinterface:exifinterface:1.3.1'
// Log
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.appcompat:appcompat:1.2.0'
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.sharetarget:sharetarget:1.0.0"
implementation 'androidx.core:core-ktx:1.3.2'
@ -362,11 +361,11 @@ dependencies {
implementation "io.arrow-kt:arrow-core:$arrow_version"
// Pref
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.preference:preference-ktx:1.1.1'
// UI
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.3.0-alpha02'
implementation 'com.google.android.material:material:1.3.0-alpha04'
implementation 'me.gujun.android:span:1.7'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
@ -374,7 +373,7 @@ dependencies {
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version"
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta10'
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
// Custom Tab
implementation 'androidx.browser:browser:1.2.0'
@ -418,7 +417,7 @@ dependencies {
kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0'
// gplay flavor only
gplayImplementation('com.google.firebase:firebase-messaging:20.3.0') {
gplayImplementation('com.google.firebase:firebase-messaging:21.0.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@ -441,6 +440,10 @@ dependencies {
implementation 'com.google.zxing:core:3.3.3'
implementation 'me.dm7.barcodescanner:zxing:1.9.13'
// Emoji Keyboard
implementation 'com.vanniktech:emoji-material:0.7.0'
implementation 'com.vanniktech:emoji-google:0.7.0'
// TESTS
testImplementation 'junit:junit:4.13'
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
*/
// 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>> {
setState { stateReducer(Loading()) }
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
*/
// 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>> {
setState { stateReducer(Loading()) }
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) {
behaviorRelay.accept(value)
behaviorRelay.accept(value!!)
}
private fun createRelay(): BehaviorRelay<T> {
@ -68,6 +68,6 @@ open class PublishDataSource<T> : MutableDataSource<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 timber.log.Timber
import java.io.File
import java.util.Locale
// Implementation should return true in case of success
typealias ActionOnFile = (file: File) -> Boolean
@ -113,7 +114,7 @@ fun getFileExtension(fileUri: String): String? {
val ext = filename.substring(dotPos + 1)
if (ext.isNotBlank()) {
return ext.toLowerCase()
return ext.toLowerCase(Locale.ROOT)
}
}
}

View file

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

View file

@ -17,31 +17,12 @@
package im.vector.app.features.attachments.preview
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState)
class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState)
: VectorViewModel<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) {
when (action) {
is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action)

View file

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

View file

@ -53,7 +53,6 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.epoxy.addGlidePreloader
@ -69,6 +68,7 @@ import com.airbnb.mvrx.withState
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.jakewharton.rxbinding3.widget.textChanges
import com.vanniktech.emoji.EmojiPopup
import im.vector.app.R
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
@ -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.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@ -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.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -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.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import timber.log.Timber
@ -290,8 +287,6 @@ class RoomDetailFragment @Inject constructor(
private lateinit var attachmentsHelper: AttachmentsHelper
private lateinit var keyboardStateUtils: KeyboardStateUtils
@BindView(R.id.composerLayout)
lateinit var composerLayout: TextComposerView
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
private var lockSendButton = false
@ -312,6 +307,7 @@ class RoomDetailFragment @Inject constructor(
setupActiveCallView()
setupJumpToBottomView()
setupConfBannerView()
setupEmojiPopup()
roomToolbarContentView.debouncedClicks {
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) {
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
timelineEventController.update(state)
inviteView.visibility = View.GONE
val uid = session.myUserId
val meMember = state.myRoomMember()
avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView)
if (state.tombstoneEvent == null) {
if (state.canSendMessage) {
composerLayout.visibility = View.VISIBLE
@ -1670,12 +1677,7 @@ class RoomDetailFragment @Inject constructor(
shareText(requireContext(), action.messageContent.body)
} else if (action.messageContent is MessageWithAttachmentContent) {
session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
id = action.eventId,
fileName = action.messageContent.body,
mimeType = action.messageContent.mimeType,
url = action.messageContent.getFileUrl(),
elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
messageContent = action.messageContent,
callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) {
if (isAdded) {
@ -1705,12 +1707,7 @@ class RoomDetailFragment @Inject constructor(
return
}
session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
id = action.eventId,
fileName = action.messageContent.body,
mimeType = action.messageContent.mimeType,
url = action.messageContent.getFileUrl(),
elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
messageContent = action.messageContent,
callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) {
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.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
@ -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.MessageType
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
@ -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.WidgetType
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import org.matrix.android.sdk.internal.util.awaitCallback
@ -1019,10 +1016,10 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
val mxcUrl = action.messageFileContent.getFileUrl()
val mxcUrl = action.messageFileContent.getFileUrl() ?: return
val isLocalSendingFile = action.senderId == session.myUserId
&& mxcUrl?.startsWith("content://") ?: false
val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false
&& mxcUrl.startsWith("content://")
val isDownloaded = session.fileService().isFileInCache(action.messageFileContent)
if (isLocalSendingFile) {
tryOrNull { Uri.parse(mxcUrl) }?.let {
_viewEvents.post(RoomDetailViewEvents.OpenFile(
@ -1033,7 +1030,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
} else if (isDownloaded) {
// we can open it
session.fileService().getTemporarySharableURI(mxcUrl!!, action.messageFileContent.mimeType)?.let { uri ->
session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri ->
_viewEvents.post(RoomDetailViewEvents.OpenFile(
action.messageFileContent.mimeType,
uri,
@ -1042,12 +1039,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
} else {
session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
id = action.eventId,
fileName = action.messageFileContent.getFileName(),
mimeType = action.messageFileContent.mimeType,
url = mxcUrl,
elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
messageContent = action.messageFileContent,
callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) {
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -74,6 +74,10 @@ class PinFragment @Inject constructor(
Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show()
}
override fun onPinCodeEnteredFirst(pinCode: String?): Boolean {
return false
}
override fun onCodeCreated(encodedCode: String) {
lifecycleScope.launch {
pinCodeStore.storeEncodedPin(encodedCode)

View file

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

View file

@ -16,32 +16,7 @@
package im.vector.app.features.roomprofile.settings.historyvisibility
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.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel
class RoomHistoryVisibilityViewModel @AssistedInject constructor(@Assisted initialState: RoomHistoryVisibilityState)
: VectorViewModel<RoomHistoryVisibilityState, EmptyAction, EmptyViewEvents>(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
}
}
class RoomHistoryVisibilityViewModel(initialState: RoomHistoryVisibilityState)
: BottomSheetGenericViewModel<RoomHistoryVisibilityState>(initialState)

View file

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

View file

@ -16,32 +16,7 @@
package im.vector.app.features.roomprofile.settings.joinrule
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.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel
class RoomJoinRuleViewModel @AssistedInject constructor(@Assisted initialState: RoomJoinRuleState)
: VectorViewModel<RoomJoinRuleState, EmptyAction, EmptyViewEvents>(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
}
}
class RoomJoinRuleViewModel(initialState: RoomJoinRuleState)
: BottomSheetGenericViewModel<RoomJoinRuleState>(initialState)

View file

@ -30,10 +30,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
@ -134,12 +131,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
try {
val file = awaitCallback<File> {
session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
id = action.uploadEvent.eventId,
fileName = action.uploadEvent.contentWithAttachmentContent.body,
url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
messageContent = action.uploadEvent.contentWithAttachmentContent,
callback = it
)
}
@ -155,12 +147,7 @@ class RoomUploadsViewModel @AssistedInject constructor(
try {
val file = awaitCallback<File> {
session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
id = action.uploadEvent.eventId,
fileName = action.uploadEvent.contentWithAttachmentContent.body,
mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
messageContent = action.uploadEvent.contentWithAttachmentContent,
callback = it)
}
_viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body))

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"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
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:strokeColor="#ffffff" android:strokeWidth="2"/>
<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="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>

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

View file

@ -83,9 +83,9 @@
app:layout_constraintEnd_toEndOf="@id/composer_related_message_avatar_view"
app:layout_constraintStart_toStartOf="@id/composer_related_message_avatar_view"
app:layout_constraintTop_toBottomOf="@id/composer_related_message_avatar_view"
tools:src="@drawable/ic_edit"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_edit" />
<ImageButton
@ -94,51 +94,47 @@
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
android:src="@drawable/ic_close_round"
app:tint="@color/riotx_notice"
app:layout_constraintBottom_toBottomOf="@id/composer_related_message_preview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/composer_related_message_preview"
app:tint="@color/riotx_notice"
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
android:id="@+id/attachmentButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
android:src="@drawable/ic_attachment"
app:tint="?attr/colorAccent"
app:layout_constraintEnd_toStartOf="@+id/sendButton"
app:layout_constraintStart_toStartOf="@id/composerEditText"
app:layout_constraintTop_toBottomOf="parent"
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
android:id="@+id/composer_preview_barrier"
android:layout_width="0dp"
@ -151,11 +147,13 @@
<ImageButton
android:id="@+id/sendButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/bg_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_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
@ -170,8 +168,8 @@
android:nextFocusLeft="@id/composerEditText"
android:nextFocusUp="@id/composerEditText"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/sendButton"
app:layout_constraintStart_toEndOf="@id/composer_shield"
app:layout_constraintEnd_toStartOf="@+id/composer_emoji"
app:layout_constraintStart_toEndOf="@+id/composer_shield"
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
tools:text="@tools:sample/lorem/random" />

View file

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