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 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 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
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.

View File

@ -55,8 +55,6 @@ class AttachmentEncryptionTest {
assertNotNull(decryptedStream)
inputStream.close()
val buffer = ByteArray(100)
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.api.auth.AuthenticationService
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.network.UserAgentHolder
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager
import java.io.InputStream
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@ -96,5 +99,9 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun getSdkVersion(): String {
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.database.RealmKeysUtils
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.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
@ -53,7 +53,7 @@ internal abstract class CryptoModule {
@Provides
@CryptoDatabase
@SessionScope
fun providesRealmConfiguration(@UserCacheDirectory directory: File,
fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String,
realmKeysUtils: RealmKeysUtils): RealmConfiguration {
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.SecretKeySpec
object MXEncryptedAttachments {
internal object MXEncryptedAttachments {
private const val CRYPTO_BUFFER_SIZE = 32 * 1024
private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
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.
* @param attachmentStream the attachment stream
* @param attachmentStream the attachment stream. Will be closed after this method call.
* @param mimetype the mime type
* @return the encryption file info
*/
@ -67,9 +59,7 @@ object MXEncryptedAttachments {
val key = ByteArray(32)
secureRandom.nextBytes(key)
val outStream = ByteArrayOutputStream()
outStream.use {
ByteArrayOutputStream().use { outputStream ->
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
@ -81,20 +71,22 @@ object MXEncryptedAttachments {
var read: Int
var encodedBytes: ByteArray
read = attachmentStream.read(data)
while (read != -1) {
encodedBytes = encryptCipher.update(data, 0, read)
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outStream.write(encodedBytes)
read = attachmentStream.read(data)
attachmentStream.use { inputStream ->
read = inputStream.read(data)
while (read != -1) {
encodedBytes = encryptCipher.update(data, 0, read)
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes)
read = inputStream.read(data)
}
}
// encrypt the latest chunk
encodedBytes = encryptCipher.doFinal()
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outStream.write(encodedBytes)
outputStream.write(encodedBytes)
val result = EncryptionResult(
return EncryptionResult(
encryptedFileInfo = EncryptedFileInfo(
url = null,
mimetype = mimetype,
@ -109,18 +101,16 @@ object MXEncryptedAttachments {
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2"
),
encryptedByteArray = outStream.toByteArray()
encryptedByteArray = outputStream.toByteArray()
)
Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms")
return result
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
}
}
/**
* 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
* @return the decrypted attachment stream
*/
@ -138,7 +128,7 @@ object MXEncryptedAttachments {
/**
* 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
* @return the decrypted attachment stream
*/
@ -151,59 +141,50 @@ object MXEncryptedAttachments {
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 key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val decryptCipher = Cipher.getInstance(CIPHER_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)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
var read: Int
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray
var read: Int
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray
attachmentStream.use { inputStream ->
read = inputStream.read(data)
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)
while (read != -1) {
messageDigest.update(data, 0, read)
decodedBytes = decryptCipher.update(data, 0, read)
outStream.write(decodedBytes)
read = attachmentStream.read(data)
// decrypt the last chunk
decodedBytes = decryptCipher.doFinal()
outputStream.write(decodedBytes)
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
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

View File

@ -18,8 +18,8 @@ package im.vector.matrix.android.internal.database
import android.content.Context
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.UserCacheDirectory
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionModule
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's clearly not perfect but there is no way to catch the native crash.
*/
internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils,
@UserCacheDirectory val directory: File,
@SessionId val sessionId: String,
@UserMd5 val userMd5: String,
context: Context) {
internal class SessionRealmConfigurationFactory @Inject constructor(
private val realmKeysUtils: RealmKeysUtils,
@SessionFilesDirectory val directory: File,
@SessionId val sessionId: String,
@UserMd5 val userMd5: String,
context: Context) {
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)

View File

@ -14,16 +14,14 @@
* 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
import javax.inject.Qualifier
@Qualifier
@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
import android.content.Context
import android.os.Environment
import arrow.core.Try
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.internal.crypto.attachments.ElementToDecrypt
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.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5
@ -41,12 +41,13 @@ import java.io.File
import java.io.IOException
import javax.inject.Inject
internal class DefaultFileService @Inject constructor(private val context: Context,
@SessionId private val sessionId: String,
private val contentUrlResolver: ContentUrlResolver,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {
val okHttpClient = OkHttpClient()
internal class DefaultFileService @Inject constructor(
@SessionCacheDirectory
private val cacheDirectory: File,
private val contentUrlResolver: ContentUrlResolver,
@Unauthenticated
private val okHttpClient: OkHttpClient,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {
/**
* 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) {
FileService.DownloadMode.FOR_INTERNAL_USE -> {
// Create dir tree (MF stands for Matrix File):
// <cache>/MF/<sessionId>/<md5(id)>/
val tmpFolderRoot = File(context.cacheDir, "MF")
val tmpFolderUser = File(tmpFolderRoot, sessionId)
File(tmpFolderUser, id.md5())
// <cache>/<sessionId>/MF/<md5(id)>/
val tmpFolderSession = File(cacheDirectory, "MF")
File(tmpFolderSession, id.md5())
}
FileService.DownloadMode.TO_EXPORT -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)

View File

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

View File

@ -52,7 +52,8 @@ internal class DefaultSignOutTask @Inject constructor(
private val sessionParamsStore: SessionParamsStore,
@SessionDatabase private val clearSessionDataTask: 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,
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@ -98,7 +99,8 @@ internal class DefaultSignOutTask @Inject constructor(
clearCryptoDataTask.execute(Unit)
Timber.d("SignOut: clear file system")
userFile.deleteRecursively()
sessionFiles.deleteRecursively()
sessionCache.deleteRecursively()
Timber.d("SignOut: clear the database keys")
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.MultiModelLoaderFactory
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.features.media.ImageContentRenderer
import okhttp3.OkHttpClient
@ -116,7 +116,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
return
}
stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) {
MXEncryptedAttachments.decryptAttachment(inputStream, data.elementToDecrypt)
Matrix.decryptStream(inputStream, data.elementToDecrypt)
} else {
inputStream
}

View File

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

View File

@ -129,7 +129,7 @@
<color name="link_color_dark">#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="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="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 name="AppTheme.Dark" parent="AppTheme.Base.Dark" />

View File

@ -206,6 +206,14 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</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 name="AppTheme.Light" parent="AppTheme.Base.Light" />