Merge pull request #850 from vector-im/feature/e2e_attachments_cleanup

Feature/e2e attachments cleanup
This commit is contained in:
Benoit Marty 2020-01-17 12:12:48 +01:00 committed by GitHub
commit 626ebd9c63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 183 additions and 128 deletions

View File

@ -7,4 +7,4 @@
- [ ] Pull request is based on the develop branch - [ ] Pull request is based on the develop branch
- [ ] Pull request updates [CHANGES.md](https://github.com/vector-im/riotX-android/blob/develop/CHANGES.md) - [ ] Pull request updates [CHANGES.md](https://github.com/vector-im/riotX-android/blob/develop/CHANGES.md)
- [ ] Pull request includes screenshots or videos if containing UI changes - [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst#sign-off) - [ ] Pull request includes a [sign off](https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md#sign-off)

View File

@ -1,6 +1,6 @@
# Contributing code to Matrix # Contributing code to Matrix
Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md
Android support can be found in this [![Riot Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riot-android:matrix.org.svg?label=%23riot-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riot-android:matrix.org) room. Android support can be found in this [![Riot Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riot-android:matrix.org.svg?label=%23riot-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riot-android:matrix.org) room.

View File

@ -55,8 +55,6 @@ class AttachmentEncryptionTest {
assertNotNull(decryptedStream) assertNotNull(decryptedStream)
inputStream.close()
val buffer = ByteArray(100) val buffer = ByteArray(100)
val len = decryptedStream!!.read(buffer) val len = decryptedStream!!.read(buffer)

View File

@ -24,10 +24,13 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.di.DaggerMatrixComponent import im.vector.matrix.android.internal.di.DaggerMatrixComponent
import im.vector.matrix.android.internal.network.UserAgentHolder import im.vector.matrix.android.internal.network.UserAgentHolder
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import java.io.InputStream
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
@ -96,5 +99,9 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun getSdkVersion(): String { fun getSdkVersion(): String {
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
} }
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
}
} }
} }

View File

@ -31,7 +31,7 @@ import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
import im.vector.matrix.android.internal.crypto.tasks.* import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.UserCacheDirectory import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.session.cache.ClearCacheTask
@ -53,7 +53,7 @@ internal abstract class CryptoModule {
@Provides @Provides
@CryptoDatabase @CryptoDatabase
@SessionScope @SessionScope
fun providesRealmConfiguration(@UserCacheDirectory directory: File, fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String, @UserMd5 userMd5: String,
realmKeysUtils: RealmKeysUtils): RealmConfiguration { realmKeysUtils: RealmKeysUtils): RealmConfiguration {
return RealmConfiguration.Builder() return RealmConfiguration.Builder()

View File

@ -0,0 +1,27 @@
/*
* Copyright 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.matrix.android.internal.crypto.attachments
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
/**
* Define the result of an encryption file
*/
internal data class EncryptionResult(
var encryptedFileInfo: EncryptedFileInfo,
var encryptedByteArray: ByteArray
)

View File

@ -29,23 +29,15 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
object MXEncryptedAttachments { internal object MXEncryptedAttachments {
private const val CRYPTO_BUFFER_SIZE = 32 * 1024 private const val CRYPTO_BUFFER_SIZE = 32 * 1024
private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding" private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
private const val SECRET_KEY_SPEC_ALGORITHM = "AES" private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
/**
* Define the result of an encryption file
*/
data class EncryptionResult(
var encryptedFileInfo: EncryptedFileInfo,
var encryptedByteArray: ByteArray
)
/*** /***
* Encrypt an attachment stream. * Encrypt an attachment stream.
* @param attachmentStream the attachment stream * @param attachmentStream the attachment stream. Will be closed after this method call.
* @param mimetype the mime type * @param mimetype the mime type
* @return the encryption file info * @return the encryption file info
*/ */
@ -67,9 +59,7 @@ object MXEncryptedAttachments {
val key = ByteArray(32) val key = ByteArray(32)
secureRandom.nextBytes(key) secureRandom.nextBytes(key)
val outStream = ByteArrayOutputStream() ByteArrayOutputStream().use { outputStream ->
outStream.use {
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes) val ivParameterSpec = IvParameterSpec(initVectorBytes)
@ -81,20 +71,22 @@ object MXEncryptedAttachments {
var read: Int var read: Int
var encodedBytes: ByteArray var encodedBytes: ByteArray
read = attachmentStream.read(data) attachmentStream.use { inputStream ->
while (read != -1) { read = inputStream.read(data)
encodedBytes = encryptCipher.update(data, 0, read) while (read != -1) {
messageDigest.update(encodedBytes, 0, encodedBytes.size) encodedBytes = encryptCipher.update(data, 0, read)
outStream.write(encodedBytes) messageDigest.update(encodedBytes, 0, encodedBytes.size)
read = attachmentStream.read(data) outputStream.write(encodedBytes)
read = inputStream.read(data)
}
} }
// encrypt the latest chunk // encrypt the latest chunk
encodedBytes = encryptCipher.doFinal() encodedBytes = encryptCipher.doFinal()
messageDigest.update(encodedBytes, 0, encodedBytes.size) messageDigest.update(encodedBytes, 0, encodedBytes.size)
outStream.write(encodedBytes) outputStream.write(encodedBytes)
val result = EncryptionResult( return EncryptionResult(
encryptedFileInfo = EncryptedFileInfo( encryptedFileInfo = EncryptedFileInfo(
url = null, url = null,
mimetype = mimetype, mimetype = mimetype,
@ -109,18 +101,16 @@ object MXEncryptedAttachments {
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2" v = "v2"
), ),
encryptedByteArray = outStream.toByteArray() encryptedByteArray = outputStream.toByteArray()
) )
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms")
return result
} }
} }
/** /**
* Decrypt an attachment * Decrypt an attachment
* *
* @param attachmentStream the attachment stream * @param attachmentStream the attachment stream. Will be closed after this method call.
* @param encryptedFileInfo the encryption file info * @param encryptedFileInfo the encryption file info
* @return the decrypted attachment stream * @return the decrypted attachment stream
*/ */
@ -138,7 +128,7 @@ object MXEncryptedAttachments {
/** /**
* Decrypt an attachment * Decrypt an attachment
* *
* @param attachmentStream the attachment stream * @param attachmentStream the attachment stream. Will be closed after this method call.
* @param elementToDecrypt the elementToDecrypt info * @param elementToDecrypt the elementToDecrypt info
* @return the decrypted attachment stream * @return the decrypted attachment stream
*/ */
@ -151,59 +141,50 @@ object MXEncryptedAttachments {
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
val outStream = ByteArrayOutputStream() ByteArrayOutputStream().use { outputStream ->
try {
val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
try { val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) var read: Int
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray
var read: Int attachmentStream.use { inputStream ->
val data = ByteArray(CRYPTO_BUFFER_SIZE) read = inputStream.read(data)
var decodedBytes: ByteArray while (read != -1) {
messageDigest.update(data, 0, read)
decodedBytes = decryptCipher.update(data, 0, read)
outputStream.write(decodedBytes)
read = inputStream.read(data)
}
}
read = attachmentStream.read(data) // decrypt the last chunk
while (read != -1) { decodedBytes = decryptCipher.doFinal()
messageDigest.update(data, 0, read) outputStream.write(decodedBytes)
decodedBytes = decryptCipher.update(data, 0, read)
outStream.write(decodedBytes) val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
read = attachmentStream.read(data)
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
return null
}
return ByteArrayInputStream(outputStream.toByteArray())
.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") }
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## decryptAttachment() failed: OOM")
} catch (e: Exception) {
Timber.e(e, "## decryptAttachment() failed")
} }
// decrypt the last chunk
decodedBytes = decryptCipher.doFinal()
outStream.write(decodedBytes)
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
outStream.close()
return null
}
val decryptedStream = ByteArrayInputStream(outStream.toByteArray())
outStream.close()
Timber.v("Decrypt in ${System.currentTimeMillis() - t0} ms")
return decryptedStream
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## decryptAttachment() : failed ${oom.message}")
} catch (e: Exception) {
Timber.e(e, "## decryptAttachment() : failed ${e.message}")
}
try {
outStream.close()
} catch (closeException: Exception) {
Timber.e(closeException, "## decryptAttachment() : fail to close the file")
} }
return null return null

View File

@ -18,8 +18,8 @@ package im.vector.matrix.android.internal.database
import android.content.Context import android.content.Context
import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserCacheDirectory
import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionModule import im.vector.matrix.android.internal.session.SessionModule
import io.realm.Realm import io.realm.Realm
@ -36,11 +36,12 @@ private const val REALM_NAME = "disk_store.realm"
* It will handle corrupted realm by clearing the db file. It allows to just clear cache without losing your crypto keys. * It will handle corrupted realm by clearing the db file. It allows to just clear cache without losing your crypto keys.
* It's clearly not perfect but there is no way to catch the native crash. * It's clearly not perfect but there is no way to catch the native crash.
*/ */
internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils, internal class SessionRealmConfigurationFactory @Inject constructor(
@UserCacheDirectory val directory: File, private val realmKeysUtils: RealmKeysUtils,
@SessionId val sessionId: String, @SessionFilesDirectory val directory: File,
@UserMd5 val userMd5: String, @SessionId val sessionId: String,
context: Context) { @UserMd5 val userMd5: String,
context: Context) {
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)

View File

@ -14,16 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
/*
* Unfortunatly "ktlint-disable filename" this does not work so this file is renamed to UserCacheDirectory.kt
* If a new qualifier is added, please rename this file ti FileQualifiers.kt...
*/
/* ktlint-disable filename */
package im.vector.matrix.android.internal.di package im.vector.matrix.android.internal.di
import javax.inject.Qualifier import javax.inject.Qualifier
@Qualifier @Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class UserCacheDirectory annotation class SessionFilesDirectory
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class SessionCacheDirectory

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session package im.vector.matrix.android.internal.session
import android.content.Context
import android.os.Environment import android.os.Environment
import arrow.core.Try import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
@ -25,7 +24,8 @@ import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.SessionCacheDirectory
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.md5
@ -41,12 +41,13 @@ import java.io.File
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
internal class DefaultFileService @Inject constructor(private val context: Context, internal class DefaultFileService @Inject constructor(
@SessionId private val sessionId: String, @SessionCacheDirectory
private val contentUrlResolver: ContentUrlResolver, private val cacheDirectory: File,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { private val contentUrlResolver: ContentUrlResolver,
@Unauthenticated
val okHttpClient = OkHttpClient() private val okHttpClient: OkHttpClient,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {
/** /**
* Download file in the cache folder, and eventually decrypt it * Download file in the cache folder, and eventually decrypt it
@ -103,10 +104,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
return when (downloadMode) { return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> { FileService.DownloadMode.FOR_INTERNAL_USE -> {
// Create dir tree (MF stands for Matrix File): // Create dir tree (MF stands for Matrix File):
// <cache>/MF/<sessionId>/<md5(id)>/ // <cache>/<sessionId>/MF/<md5(id)>/
val tmpFolderRoot = File(context.cacheDir, "MF") val tmpFolderSession = File(cacheDirectory, "MF")
val tmpFolderUser = File(tmpFolderRoot, sessionId) File(tmpFolderSession, id.md5())
File(tmpFolderUser, id.md5())
} }
FileService.DownloadMode.TO_EXPORT -> { FileService.DownloadMode.TO_EXPORT -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)

View File

@ -93,7 +93,7 @@ internal abstract class SessionModule {
@JvmStatic @JvmStatic
@Provides @Provides
@UserCacheDirectory @SessionFilesDirectory
fun providesFilesDir(@UserMd5 userMd5: String, fun providesFilesDir(@UserMd5 userMd5: String,
@SessionId sessionId: String, @SessionId sessionId: String,
context: Context): File { context: Context): File {
@ -106,6 +106,14 @@ internal abstract class SessionModule {
return File(context.filesDir, sessionId) return File(context.filesDir, sessionId)
} }
@JvmStatic
@Provides
@SessionCacheDirectory
fun providesCacheDir(@SessionId sessionId: String,
context: Context): File {
return File(context.cacheDir, sessionId)
}
@JvmStatic @JvmStatic
@Provides @Provides
@SessionDatabase @SessionDatabase

View File

@ -52,7 +52,8 @@ internal class DefaultSignOutTask @Inject constructor(
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore,
@SessionDatabase private val clearSessionDataTask: ClearCacheTask, @SessionDatabase private val clearSessionDataTask: ClearCacheTask,
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@UserCacheDirectory private val userFile: File, @SessionFilesDirectory private val sessionFiles: File,
@SessionCacheDirectory private val sessionCache: File,
private val realmKeysUtils: RealmKeysUtils, private val realmKeysUtils: RealmKeysUtils,
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration, @SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@ -98,7 +99,8 @@ internal class DefaultSignOutTask @Inject constructor(
clearCryptoDataTask.execute(Unit) clearCryptoDataTask.execute(Unit)
Timber.d("SignOut: clear file system") Timber.d("SignOut: clear file system")
userFile.deleteRecursively() sessionFiles.deleteRecursively()
sessionCache.deleteRecursively()
Timber.d("SignOut: clear the database keys") Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5)) realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))

View File

@ -24,7 +24,7 @@ import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.api.Matrix
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.ImageContentRenderer
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -116,7 +116,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
return return
} }
stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) { stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) {
MXEncryptedAttachments.decryptAttachment(inputStream, data.elementToDecrypt) Matrix.decryptStream(inputStream, data.elementToDecrypt)
} else { } else {
inputStream inputStream
} }

View File

@ -251,7 +251,7 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) }
roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair -> roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair ->
val message = requireContext().getString(pair.first, *pair.second.toTypedArray()) val message = getString(pair.first, *pair.second.toTypedArray())
showSnackWithMessage(message, Snackbar.LENGTH_LONG) showSnackWithMessage(message, Snackbar.LENGTH_LONG)
} }
sharedActionViewModel sharedActionViewModel
@ -280,11 +280,12 @@ class RoomDetailFragment @Inject constructor(
} }
roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState -> roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState ->
val activity = requireActivity()
if (downloadFileState.throwable != null) { if (downloadFileState.throwable != null) {
requireActivity().toast(errorFormatter.toHumanReadable(downloadFileState.throwable)) activity.toast(errorFormatter.toHumanReadable(downloadFileState.throwable))
} else if (downloadFileState.file != null) { } else if (downloadFileState.file != null) {
requireActivity().toast(getString(R.string.downloaded_file, downloadFileState.file.path)) activity.toast(getString(R.string.downloaded_file, downloadFileState.file.path))
addEntryToDownloadManager(requireContext(), downloadFileState.file, downloadFileState.mimeType) addEntryToDownloadManager(activity, downloadFileState.file, downloadFileState.mimeType)
} }
} }
@ -369,9 +370,9 @@ class RoomDetailFragment @Inject constructor(
AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.error_file_too_big, .setMessage(getString(R.string.error_file_too_big,
error.filename, error.filename,
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
)) ))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
@ -454,11 +455,11 @@ class RoomDetailFragment @Inject constructor(
updateComposerText(defaultContent) updateComposerText(defaultContent)
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
composerLayout.sendButton.setContentDescription(getString(descriptionRes)) composerLayout.sendButton.contentDescription = getString(descriptionRes)
avatarRenderer.render( avatarRenderer.render(
MatrixItem.UserItem(event.root.senderId MatrixItem.UserItem(event.root.senderId
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
composerLayout.composerRelatedMessageAvatar composerLayout.composerRelatedMessageAvatar
) )
@ -477,7 +478,7 @@ class RoomDetailFragment @Inject constructor(
// Ignore update to avoid saving a draft // Ignore update to avoid saving a draft
composerLayout.composerEditText.setText(text) composerLayout.composerEditText.setText(text)
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length
?: 0) ?: 0)
} }
} }
@ -928,6 +929,7 @@ class RoomDetailFragment @Inject constructor(
val action = RoomDetailAction.DownloadFile(eventId, messageFileContent) val action = RoomDetailAction.DownloadFile(eventId, messageFileContent)
// We need WRITE_EXTERNAL permission // We need WRITE_EXTERNAL permission
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) {
showSnackWithMessage(getString(R.string.downloading_file, messageFileContent.getFileName()))
roomDetailViewModel.handle(action) roomDetailViewModel.handle(action)
} else { } else {
roomDetailViewModel.pendingAction = action roomDetailViewModel.pendingAction = action
@ -940,6 +942,10 @@ class RoomDetailFragment @Inject constructor(
PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> { PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
val action = roomDetailViewModel.pendingAction val action = roomDetailViewModel.pendingAction
if (action != null) { if (action != null) {
(action as? RoomDetailAction.DownloadFile)
?.messageFileContent
?.getFileName()
?.let { showSnackWithMessage(getString(R.string.downloading_file, it)) }
roomDetailViewModel.pendingAction = null roomDetailViewModel.pendingAction = null
roomDetailViewModel.handle(action) roomDetailViewModel.handle(action)
} }
@ -1052,8 +1058,7 @@ class RoomDetailFragment @Inject constructor(
is EventSharedAction.Copy -> { is EventSharedAction.Copy -> {
// I need info about the current selected message :/ // I need info about the current selected message :/
copyToClipboard(requireContext(), action.content, false) copyToClipboard(requireContext(), action.content, false)
val msg = requireContext().getString(R.string.copied_to_clipboard) showSnackWithMessage(getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
} }
is EventSharedAction.Delete -> { is EventSharedAction.Delete -> {
roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
@ -1127,7 +1132,7 @@ class RoomDetailFragment @Inject constructor(
is EventSharedAction.CopyPermalink -> { is EventSharedAction.CopyPermalink -> {
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
copyToClipboard(requireContext(), permalink, false) copyToClipboard(requireContext(), permalink, false)
showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) showSnackWithMessage(getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
} }
is EventSharedAction.Resend -> { is EventSharedAction.Resend -> {
roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId)) roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId))
@ -1171,7 +1176,7 @@ class RoomDetailFragment @Inject constructor(
val startToCompose = composerLayout.composerEditText.text.isNullOrBlank() val startToCompose = composerLayout.composerEditText.text.isNullOrBlank()
if (startToCompose if (startToCompose
&& userId == session.myUserId) { && userId == session.myUserId) {
// Empty composer, current user: start an emote // Empty composer, current user: start an emote
composerLayout.composerEditText.setText(Command.EMOTE.command + " ") composerLayout.composerEditText.setText(Command.EMOTE.command + " ")
composerLayout.composerEditText.setSelection(Command.EMOTE.length) composerLayout.composerEditText.setSelection(Command.EMOTE.length)
@ -1217,9 +1222,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
val snack = Snackbar.make(view!!, message, duration) Snackbar.make(view!!, message, duration).show()
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
snack.show()
} }
// VectorInviteView.Callback // VectorInviteView.Callback

View File

@ -129,7 +129,7 @@
<color name="link_color_dark">#368BD6</color> <color name="link_color_dark">#368BD6</color>
<color name="link_color_status">#368BD6</color> <color name="link_color_status">#368BD6</color>
<!-- Notification (do not depends on theme --> <!-- Notification (do not depends on theme) -->
<color name="notification_accent_color">#368BD6</color> <color name="notification_accent_color">#368BD6</color>
<color name="key_share_req_accent_color">#ff812d</color> <color name="key_share_req_accent_color">#ff812d</color>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="VectorSnackBarStyle" parent="@style/Widget.MaterialComponents.Snackbar">
<item name="android:background">@color/notification_accent_color</item>
</style>
<style name="VectorSnackBarButton" parent="@style/Widget.MaterialComponents.Button" />
<style name="VectorSnackBarText" parent="@style/Widget.MaterialComponents.Snackbar.TextView">
<item name="android:textColor">@color/white</item>
</style>
</resources>

View File

@ -206,6 +206,14 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="bottomSheetDialogTheme">@style/Vector.BottomSheet.Dark</item> <item name="bottomSheetDialogTheme">@style/Vector.BottomSheet.Dark</item>
<!-- SnackBar -->
<!-- Style to use for SnackBars in this theme. -->
<item name="snackbarStyle">@style/VectorSnackBarStyle</item>
<!-- Style to use for action button within a SnackBar in this theme. -->
<item name="snackbarButtonStyle">@style/VectorSnackBarButton</item>
<!-- Style to use for message text within a SnackBar in this theme. -->
<item name="snackbarTextViewStyle">@style/VectorSnackBarText</item>
</style> </style>
<style name="AppTheme.Dark" parent="AppTheme.Base.Dark" /> <style name="AppTheme.Dark" parent="AppTheme.Base.Dark" />

View File

@ -206,6 +206,14 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="bottomSheetDialogTheme">@style/Vector.BottomSheet.Light</item> <item name="bottomSheetDialogTheme">@style/Vector.BottomSheet.Light</item>
<!-- SnackBar -->
<!-- Style to use for SnackBars in this theme. -->
<item name="snackbarStyle">@style/VectorSnackBarStyle</item>
<!-- Style to use for action button within a SnackBar in this theme. -->
<item name="snackbarButtonStyle">@style/VectorSnackBarButton</item>
<!-- Style to use for message text within a SnackBar in this theme. -->
<item name="snackbarTextViewStyle">@style/VectorSnackBarText</item>
</style> </style>
<style name="AppTheme.Light" parent="AppTheme.Base.Light" /> <style name="AppTheme.Light" parent="AppTheme.Base.Light" />