Merge pull request #261 from vector-im/feature/e2e_file

Encrypt attachment in e2e rooms
This commit is contained in:
Benoit Marty 2019-07-02 16:44:17 +02:00 committed by GitHub
commit 498b1f2b06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 941 additions and 161 deletions

View File

@ -228,4 +228,8 @@ data class Event(
} }
} }


/**
* Tells if the event is redacted
*/
fun isRedacted() = unsignedData?.redactedEvent != null
} }

View File

@ -128,6 +128,7 @@ class CreateRoomParams {
contentMap["algorithm"] = algorithm contentMap["algorithm"] = algorithm


val algoEvent = Event(type = EventType.ENCRYPTION, val algoEvent = Event(type = EventType.ENCRYPTION,
stateKey = "",
content = contentMap.toContent() content = contentMap.toContent()
) )


@ -161,6 +162,7 @@ class CreateRoomParams {
contentMap["history_visibility"] = historyVisibility contentMap["history_visibility"] = historyVisibility


val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY, val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
stateKey = "",
content = contentMap.toContent()) content = contentMap.toContent())


if (null == initialStates) { if (null == initialStates) {

View File

@ -21,7 +21,18 @@ import com.squareup.moshi.JsonClass


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class AudioInfo( data class AudioInfo(
/**
* The mimetype of the audio e.g. "audio/aac".
*/
@Json(name = "mimetype") val mimeType: String, @Json(name = "mimetype") val mimeType: String,

/**
* The size of the audio clip in bytes.
*/
@Json(name = "size") val size: Long = 0, @Json(name = "size") val size: Long = 0,

/**
* The duration of the audio in milliseconds.
*/
@Json(name = "duration") val duration: Int = 0 @Json(name = "duration") val duration: Int = 0
) )

View File

@ -18,11 +18,32 @@ package im.vector.matrix.android.api.session.room.model.message


import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class FileInfo( data class FileInfo(
/**
* The mimetype of the file e.g. application/msword.
*/
@Json(name = "mimetype") val mimeType: String?, @Json(name = "mimetype") val mimeType: String?,

/**
* The size of the file in bytes.
*/
@Json(name = "size") val size: Long = 0, @Json(name = "size") val size: Long = 0,

/**
* Metadata about the image referred to in thumbnail_url.
*/
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null
/**
* The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted.
*/
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null,

/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
*/
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
) )

View File

@ -18,15 +18,52 @@ package im.vector.matrix.android.api.session.room.model.message


import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ImageInfo( data class ImageInfo(
/**
* The mimetype of the image, e.g. "image/jpeg".
*/
@Json(name = "mimetype") val mimeType: String?, @Json(name = "mimetype") val mimeType: String?,

/**
* The intended display width of the image in pixels. This may differ from the intrinsic dimensions of the image file.
*/
@Json(name = "w") val width: Int = 0, @Json(name = "w") val width: Int = 0,

/**
* The intended display height of the image in pixels. This may differ from the intrinsic dimensions of the image file.
*/
@Json(name = "h") val height: Int = 0, @Json(name = "h") val height: Int = 0,

/**
* Size of the image in bytes.
*/
@Json(name = "size") val size: Int = 0, @Json(name = "size") val size: Int = 0,

/**
* Not documented
*/
@Json(name = "rotation") val rotation: Int = 0, @Json(name = "rotation") val rotation: Int = 0,

/**
* Not documented
*/
@Json(name = "orientation") val orientation: Int = 0, @Json(name = "orientation") val orientation: Int = 0,

/**
* Metadata about the image referred to in thumbnail_url.
*/
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null
/**
* The URL (typically MXC URI) to a thumbnail of the image. Only present if the thumbnail is unencrypted.
*/
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null,

/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
*/
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
) )

View File

@ -18,9 +18,22 @@ package im.vector.matrix.android.api.session.room.model.message


import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationInfo( data class LocationInfo(
/**
* The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted.
*/
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null, @Json(name = "thumbnail_url") val thumbnailUrl: String? = null,
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null
/**
* Metadata about the image referred to in thumbnail_url.
*/
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,

/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
*/
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
) )

View File

@ -20,13 +20,35 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessageAudioContent( data class MessageAudioContent(
/**
* Not documented
*/
@Json(name = "msgtype") override val type: String, @Json(name = "msgtype") override val type: String,

/**
* Required. A description of the audio e.g. 'Bee Gees - Stayin' Alive', or some kind of content description for accessibility e.g. 'audio attachment'.
*/
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,
@Json(name = "info") val info: AudioInfo? = null,
/**
* Metadata for the audio clip referred to in url.
*/
@Json(name = "info") val audioInfo: AudioInfo? = null,

/**
* Required. Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip.
*/
@Json(name = "url") val url: String? = null, @Json(name = "url") val url: String? = null,

@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null,
) : MessageContent
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent

View File

@ -14,21 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */


package im.vector.riotredesign.core.glide; package im.vector.matrix.android.api.session.room.model.message


import android.content.Context; import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
import android.util.Log;


import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;

@GlideModule
public final class MyAppGlideModule extends AppGlideModule {

@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setLogLevel(Log.ERROR);
}


/**
* Interface for message which can contains encrypted data
*/
interface MessageEncyptedContent : MessageContent {
val encryptedFileInfo: EncryptedFileInfo?
} }

View File

@ -20,14 +20,37 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessageFileContent( data class MessageFileContent(
/**
* Not documented
*/
@Json(name = "msgtype") override val type: String, @Json(name = "msgtype") override val type: String,

/**
* Required. A human-readable description of the file. This is recommended to be the filename of the original upload.
*/
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,

/**
* The original filename of the uploaded file.
*/
@Json(name = "filename") val filename: String? = null, @Json(name = "filename") val filename: String? = null,

/**
* Information about the file referred to in url.
*/
@Json(name = "info") val info: FileInfo? = null, @Json(name = "info") val info: FileInfo? = null,

/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the file.
*/
@Json(name = "url") val url: String? = null, @Json(name = "url") val url: String? = null,

@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null,
) : MessageContent
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent

View File

@ -20,13 +20,36 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessageImageContent( data class MessageImageContent(
/**
* Required. Must be 'm.image'.
*/
@Json(name = "msgtype") override val type: String, @Json(name = "msgtype") override val type: String,

/**
* Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
* or some kind of content description for accessibility e.g. 'image attachment'.
*/
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,

/**
* Metadata about the image referred to in url.
*/
@Json(name = "info") val info: ImageInfo? = null, @Json(name = "info") val info: ImageInfo? = null,

/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/
@Json(name = "url") val url: String? = null, @Json(name = "url") val url: String? = null,

@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null,
) : MessageContent
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent

View File

@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessageLocationContent( data class MessageLocationContent(
/**
* Not documented
*/
@Json(name = "msgtype") override val type: String, @Json(name = "msgtype") override val type: String,

/**
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'.
*/
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,

/**
* Required. A geo URI representing this location.
*/
@Json(name = "geo_uri") val geoUri: String, @Json(name = "geo_uri") val geoUri: String,
@Json(name = "info") val info: LocationInfo? = null,
/**
*
*/
@Json(name = "info") val locationInfo: LocationInfo? = null,

@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContent

View File

@ -20,13 +20,35 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessageVideoContent( data class MessageVideoContent(
/**
* Required. Must be 'm.video'.
*/
@Json(name = "msgtype") override val type: String, @Json(name = "msgtype") override val type: String,

/**
* Required. A description of the video e.g. 'Gangnam style', or some kind of content description for accessibility e.g. 'video attachment'.
*/
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,
@Json(name = "info") val info: VideoInfo? = null,
/**
* Metadata about the video clip referred to in url.
*/
@Json(name = "info") val videoInfo: VideoInfo? = null,

/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the video clip.
*/
@Json(name = "url") val url: String? = null, @Json(name = "url") val url: String? = null,

@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null,
) : MessageContent
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent

View File

@ -21,8 +21,23 @@ import com.squareup.moshi.JsonClass


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ThumbnailInfo( data class ThumbnailInfo(
/**
* The intended display width of the image in pixels. This may differ from the intrinsic dimensions of the image file.
*/
@Json(name = "w") val width: Int = 0, @Json(name = "w") val width: Int = 0,

/**
* The intended display height of the image in pixels. This may differ from the intrinsic dimensions of the image file.
*/
@Json(name = "h") val height: Int = 0, @Json(name = "h") val height: Int = 0,

/**
* Size of the image in bytes.
*/
@Json(name = "size") val size: Long = 0, @Json(name = "size") val size: Long = 0,

/**
* The mimetype of the image, e.g. "image/jpeg".
*/
@Json(name = "mimetype") val mimeType: String @Json(name = "mimetype") val mimeType: String
) )

View File

@ -18,14 +18,47 @@ package im.vector.matrix.android.api.session.room.model.message


import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VideoInfo( data class VideoInfo(
/**
* The mimetype of the video e.g. "video/mp4".
*/
@Json(name = "mimetype") val mimeType: String, @Json(name = "mimetype") val mimeType: String,

/**
* The width of the video in pixels.
*/
@Json(name = "w") val width: Int = 0, @Json(name = "w") val width: Int = 0,

/**
* The height of the video in pixels.
*/
@Json(name = "h") val height: Int = 0, @Json(name = "h") val height: Int = 0,

/**
* The size of the video in bytes.
*/
@Json(name = "size") val size: Long = 0, @Json(name = "size") val size: Long = 0,

/**
* The duration of the video in milliseconds.
*/
@Json(name = "duration") val duration: Int = 0, @Json(name = "duration") val duration: Int = 0,

/**
* Metadata about the image referred to in thumbnail_url.
*/
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null
/**
* The URL (typically MXC URI) to an image thumbnail of the video clip. Only present if the thumbnail is unencrypted.
*/
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null,

/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
*/
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
) )

View File

@ -0,0 +1,46 @@
/*
* Copyright 2016 OpenMarket 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 android.os.Parcelable
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
import kotlinx.android.parcel.Parcelize


fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? {
// Check the validity of some fields
if (isValid()) {
return ElementToDecrypt(
iv = this.iv!!,
k = this.key!!.k!!,
sha256 = this.hashes!!["sha256"] ?: error("")
)
}

return null
}


/**
* Represent data to decode an attachment
*/
@Parcelize
data class ElementToDecrypt(
val iv: String,
val k: String,
val sha256: String
) : Parcelable

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */


package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto.attachments


import android.text.TextUtils import android.text.TextUtils
import android.util.Base64 import android.util.Base64
@ -42,7 +42,7 @@ object MXEncryptedAttachments {
*/ */
data class EncryptionResult( data class EncryptionResult(
var encryptedFileInfo: EncryptedFileInfo, var encryptedFileInfo: EncryptedFileInfo,
var encryptedStream: InputStream var encryptedByteArray: ByteArray
) )


/*** /***
@ -112,7 +112,7 @@ 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"
), ),
encryptedStream = ByteArrayInputStream(outStream.toByteArray()) encryptedByteArray = outStream.toByteArray()
) )


outStream.close() outStream.close()
@ -142,43 +142,37 @@ object MXEncryptedAttachments {
* @return the decrypted attachment stream * @return the decrypted attachment stream
*/ */
fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? { fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? {
if (encryptedFileInfo?.isValid() != true) {
Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields")
return null
}

val elementToDecrypt = encryptedFileInfo.toElementToDecrypt()

return decryptAttachment(attachmentStream, elementToDecrypt)
}

/**
* Decrypt an attachment
*
* @param attachmentStream the attachment stream
* @param elementToDecrypt the elementToDecrypt info
* @return the decrypted attachment stream
*/
fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? {
// sanity checks // sanity checks
if (null == attachmentStream || null == encryptedFileInfo) { if (null == attachmentStream || elementToDecrypt == null) {
Timber.e("## decryptAttachment() : null parameters") Timber.e("## decryptAttachment() : null stream")
return null return null
} }


if (TextUtils.isEmpty(encryptedFileInfo.iv)
|| null == encryptedFileInfo.key
|| null == encryptedFileInfo.hashes
|| !encryptedFileInfo.hashes.containsKey("sha256")) {
Timber.e("## decryptAttachment() : some fields are not defined")
return null
}

if (!TextUtils.equals(encryptedFileInfo.key!!.alg, "A256CTR")
|| !TextUtils.equals(encryptedFileInfo.key!!.kty, "oct")
|| TextUtils.isEmpty(encryptedFileInfo.key!!.k)) {
Timber.e("## decryptAttachment() : invalid key fields")
return null
}

// detect if there is no data to decrypt
try {
if (0 == attachmentStream.available()) {
return ByteArrayInputStream(ByteArray(0))
}
} catch (e: Exception) {
Timber.e(e, "Fail to retrieve the file size")
}

val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()


val outStream = ByteArrayOutputStream() val outStream = ByteArrayOutputStream()


try { try {
val key = Base64.decode(base64UrlToBase64(encryptedFileInfo.key!!.k), Base64.DEFAULT) val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
val initVectorBytes = Base64.decode(encryptedFileInfo.iv, Base64.DEFAULT) val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)


val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
@ -205,7 +199,7 @@ object MXEncryptedAttachments {


val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))


if (!TextUtils.equals(encryptedFileInfo.hashes["sha256"], currentDigestValue)) { if (!TextUtils.equals(elementToDecrypt.sha256, currentDigestValue)) {
Timber.e("## decryptAttachment() : Digest value mismatch") Timber.e("## decryptAttachment() : Digest value mismatch")
outStream.close() outStream.close()
return null return null

View File

@ -15,14 +15,75 @@
*/ */
package im.vector.matrix.android.internal.crypto.model.rest package im.vector.matrix.android.internal.crypto.model.rest


import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass


/**
* In Matrix specs: EncryptedFile
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class EncryptedFileInfo( data class EncryptedFileInfo(
/**
* Required. The URL to the file.
*/
@Json(name = "url")
var url: String? = null, var url: String? = null,
var mimetype: String,
/**
* Not documented
*/
@Json(name = "mimetype")
var mimetype: String? = null,

/**
* Required. A JSON Web Key object.
*/
@Json(name = "key")
var key: EncryptedFileKey? = null, var key: EncryptedFileKey? = null,
var iv: String,
var hashes: Map<String, String>, /**
* Required. The Initialisation Vector used by AES-CTR, encoded as unpadded base64.
*/
@Json(name = "iv")
var iv: String? = null,

/**
* Required. A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64.
* Clients should support the SHA-256 hash, which uses the key "sha256".
*/
@Json(name = "hashes")
var hashes: Map<String, String>? = null,

/**
* Required. Version of the encrypted attachments protocol. Must be "v2".
*/
@Json(name = "v")
var v: String? = null var v: String? = null
) ) {
/**
* Check what the spec tells us
*/
fun isValid(): Boolean {
if (url.isNullOrBlank()) {
return false
}

if (key?.isValid() != true) {
return false
}

if (iv.isNullOrBlank()) {
return false
}

if (hashes?.containsKey("sha256") != true) {
return false
}

if (v != "v2") {
return false
}

return true
}
}

View File

@ -15,14 +15,66 @@
*/ */
package im.vector.matrix.android.internal.crypto.model.rest package im.vector.matrix.android.internal.crypto.model.rest


import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class EncryptedFileKey( data class EncryptedFileKey(
var alg: String, /**
var ext: Boolean? = null, * Required. Algorithm. Must be "A256CTR".
var key_ops: List<String>, */
var kty: String, @Json(name = "alg")
var k: String var alg: String? = null,
)
/**
* Required. Extractable. Must be true. This is a W3C extension.
*/
@Json(name = "ext")
var ext: Boolean? = null,

/**
* Required. Key operations. Must at least contain "encrypt" and "decrypt".
*/
@Json(name = "key_ops")
var key_ops: List<String>? = null,

/**
* Required. Key type. Must be "oct".
*/
@Json(name = "kty")
var kty: String? = null,

/**
* Required. The key, encoded as urlsafe unpadded base64.
*/
@Json(name = "k")
var k: String? = null
) {
/**
* Check what the spec tells us
*/
fun isValid(): Boolean {
if (alg != "A256CTR") {
return false
}

if (ext != true) {
return false
}

if (key_ops?.contains("encrypt") != true || key_ops?.contains("decrypt") != true) {
return false
}

if (kty != "oct") {
return false
}

if (k.isNullOrBlank()) {
return false
}

return true
}
}



View File

@ -54,7 +54,6 @@ internal class FileUploader @Inject constructor(@Authenticated


val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray) val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray)
return upload(uploadBody, filename, progressListener) return upload(uploadBody, filename, progressListener)

} }





View File

@ -25,13 +25,17 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
import im.vector.matrix.android.internal.network.ProgressRequestBody import im.vector.matrix.android.internal.network.ProgressRequestBody
import im.vector.matrix.android.internal.session.room.send.SendEventWorker import im.vector.matrix.android.internal.session.room.send.SendEventWorker
import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.SessionWorkerParams
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent import im.vector.matrix.android.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.io.FileInputStream
import javax.inject.Inject import javax.inject.Inject




@ -42,7 +46,9 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
override val userId: String, override val userId: String,
val roomId: String, val roomId: String,
val event: Event, val event: Event,
val attachment: ContentAttachmentData val attachment: ContentAttachmentData,
val isRoomEncrypted: Boolean,
override var lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams


@Inject lateinit var fileUploader: FileUploader @Inject lateinit var fileUploader: FileUploader
@ -52,19 +58,41 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success() ?: return Result.success()


if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
}

val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
sessionComponent.inject(this) sessionComponent.inject(this)


val eventId = params.event.eventId ?: return Result.success() val eventId = params.event.eventId ?: return Result.success()
val attachment = params.attachment val attachment = params.attachment


val isRoomEncrypted = params.isRoomEncrypted


val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment) val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment)
val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure() val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure()
var uploadedThumbnailUrl: String? = null var uploadedThumbnailUrl: String? = null
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null


if (thumbnailData != null) { if (thumbnailData != null) {
fileUploader val contentUploadResponse = if (isRoomEncrypted) {
.uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType) Timber.v("Encrypt thumbnail")
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
?: return Result.failure()

uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo

fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", thumbnailData.mimeType)
} else {
fileUploader
.uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType)
}

contentUploadResponse
.fold( .fold(
{ Timber.e(it) }, { Timber.e(it) },
{ uploadedThumbnailUrl = it.contentUri } { uploadedThumbnailUrl = it.contentUri }
@ -76,11 +104,28 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
contentUploadStateTracker.setProgress(eventId, current, total) contentUploadStateTracker.setProgress(eventId, current, total)
} }
} }
return fileUploader
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null

val contentUploadResponse = if (isRoomEncrypted) {
Timber.v("Encrypt file")

val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
?: return Result.failure()

uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo

fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
} else {
fileUploader
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
}

return contentUploadResponse
.fold( .fold(
{ handleFailure(params) }, { handleFailure(params, it) },
{ handleSuccess(params, it.contentUri, uploadedThumbnailUrl) } { handleSuccess(params, it.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo) }
) )
} }


@ -93,46 +138,79 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
} }
} }


private fun handleFailure(params: Params): Result { private fun handleFailure(params: Params, failure: Throwable): Result {
contentUploadStateTracker.setFailure(params.event.eventId!!) contentUploadStateTracker.setFailure(params.event.eventId!!)
return Result.success() return Result.success(
WorkerParamsFactory.toData(
params.copy(
lastFailureMessage = failure.localizedMessage
)
)
)
} }


private fun handleSuccess(params: Params, private fun handleSuccess(params: Params,
attachmentUrl: String, attachmentUrl: String,
thumbnailUrl: String?): Result { encryptedFileInfo: EncryptedFileInfo?,
thumbnailUrl: String?,
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result {
contentUploadStateTracker.setSuccess(params.event.eventId!!) contentUploadStateTracker.setSuccess(params.event.eventId!!)
val event = updateEvent(params.event, attachmentUrl, thumbnailUrl) val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
val sendParams = SendEventWorker.Params(params.userId, params.roomId, event) val sendParams = SendEventWorker.Params(params.userId, params.roomId, event)
return Result.success(WorkerParamsFactory.toData(sendParams)) return Result.success(WorkerParamsFactory.toData(sendParams))
} }


private fun updateEvent(event: Event, url: String, thumbnailUrl: String? = null): Event { private fun updateEvent(event: Event,
url: String,
encryptedFileInfo: EncryptedFileInfo?,
thumbnailUrl: String? = null,
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Event {
val messageContent: MessageContent = event.content.toModel() ?: return event val messageContent: MessageContent = event.content.toModel() ?: return event
val updatedContent = when (messageContent) { val updatedContent = when (messageContent) {
is MessageImageContent -> messageContent.update(url) is MessageImageContent -> messageContent.update(url, encryptedFileInfo)
is MessageVideoContent -> messageContent.update(url, thumbnailUrl) is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
is MessageFileContent -> messageContent.update(url) is MessageFileContent -> messageContent.update(url, encryptedFileInfo)
is MessageAudioContent -> messageContent.update(url) is MessageAudioContent -> messageContent.update(url, encryptedFileInfo)
else -> messageContent else -> messageContent
} }
return event.copy(content = updatedContent.toContent()) return event.copy(content = updatedContent.toContent())
} }


private fun MessageImageContent.update(url: String): MessageImageContent { private fun MessageImageContent.update(url: String,
return copy(url = url) encryptedFileInfo: EncryptedFileInfo?): MessageImageContent {
return copy(
url = if (encryptedFileInfo == null) url else null,
encryptedFileInfo = encryptedFileInfo?.copy(url = url)
)
} }


private fun MessageVideoContent.update(url: String, thumbnailUrl: String?): MessageVideoContent { private fun MessageVideoContent.update(url: String,
return copy(url = url, info = info?.copy(thumbnailUrl = thumbnailUrl)) encryptedFileInfo: EncryptedFileInfo?,
thumbnailUrl: String?,
thumbnailEncryptedFileInfo: EncryptedFileInfo?): MessageVideoContent {
return copy(
url = if (encryptedFileInfo == null) url else null,
videoInfo = videoInfo?.copy(
thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null,
thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = url)
)
)
} }


private fun MessageFileContent.update(url: String): MessageFileContent { private fun MessageFileContent.update(url: String,
return copy(url = url) encryptedFileInfo: EncryptedFileInfo?): MessageFileContent {
return copy(
url = if (encryptedFileInfo == null) url else null,
encryptedFileInfo = encryptedFileInfo?.copy(url = url)
)
} }


private fun MessageAudioContent.update(url: String): MessageAudioContent { private fun MessageAudioContent.update(url: String,
return copy(url = url) encryptedFileInfo: EncryptedFileInfo?): MessageAudioContent {
return copy(
url = if (encryptedFileInfo == null) url else null,
encryptedFileInfo = encryptedFileInfo?.copy(url = url)
)
} }


} }

View File

@ -20,28 +20,26 @@ import android.content.Context
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import arrow.core.Try import arrow.core.Try
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.worker.DelegateWorkerFactory
import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.SessionWorkerParams
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent import im.vector.matrix.android.internal.worker.getSessionComponent
import javax.inject.Inject import javax.inject.Inject


internal class GetGroupDataWorker (context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val userId: String, override val userId: String,
val groupIds: List<String> val groupIds: List<String>,
): SessionWorkerParams override var lastFailureMessage: String? = null
) : SessionWorkerParams


@Inject lateinit var getGroupDataTask: GetGroupDataTask @Inject lateinit var getGroupDataTask: GetGroupDataTask


override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()


val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
sessionComponent.inject(this) sessionComponent.inject(this)

View File

@ -31,10 +31,8 @@ import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.helper.addSendingEvent
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
@ -107,9 +105,12 @@ internal class DefaultRelationService @Inject constructor(private val context: C


//TODO duplicate with send service? //TODO duplicate with send service?
private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest { private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest {

val sendContentWorkerParams = RedactEventWorker.Params(
val sendContentWorkerParams = RedactEventWorker.Params(credentials.userId, localEvent.eventId!!, credentials.userId,
roomId, eventId, reason) localEvent.eventId!!,
roomId,
eventId,
reason)
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData) return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
} }

View File

@ -39,7 +39,8 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
override val userId: String, override val userId: String,
val roomId: String, val roomId: String,
val event: Event, val event: Event,
val relationType: String? = null val relationType: String? = null,
override var lastFailureMessage: String?
) : SessionWorkerParams ) : SessionWorkerParams


@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@ -48,6 +49,11 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()


if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
}

val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
sessionComponent.inject(this) sessionComponent.inject(this)



View File

@ -101,13 +101,26 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
saveLocalEcho(it) saveLocalEcho(it)
} }
val uploadWork = createUploadMediaWork(event, attachment)
val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)

val uploadWork = createUploadMediaWork(event, attachment, isRoomEncrypted)
val sendWork = createSendEventWork(event) val sendWork = createSendEventWork(event)


WorkManager.getInstance(context) if (isRoomEncrypted) {
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) val encryptWork = createEncryptEventWork(event)
.then(sendWork)
.enqueue() WorkManager.getInstance(context)
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
.then(encryptWork)
.then(sendWork)
.enqueue()
} else {
WorkManager.getInstance(context)
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
.then(sendWork)
.enqueue()
}


return CancelableWork(context, sendWork.id) return CancelableWork(context, sendWork.id)
} }
@ -148,8 +161,8 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData) return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
} }


private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest { private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData, isRoomEncrypted: Boolean): OneTimeWorkRequest {
val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment) val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment, isRoomEncrypted)
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)


return OneTimeWorkRequestBuilder<UploadContentWorker>() return OneTimeWorkRequestBuilder<UploadContentWorker>()

View File

@ -41,7 +41,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
val roomId: String, val roomId: String,
val event: Event, val event: Event,
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
val keepKeys: List<String>? = null val keepKeys: List<String>? = null,
override var lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams


@Inject lateinit var crypto: CryptoService @Inject lateinit var crypto: CryptoService
@ -52,6 +53,11 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success() ?: return Result.success()


if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
}

val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
sessionComponent.inject(this) sessionComponent.inject(this)


@ -105,16 +111,15 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
) )
val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, encryptedEvent) val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, encryptedEvent)
return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) return Result.success(WorkerParamsFactory.toData(nextWorkerParams))

} else {
val sendState = when (error) {
is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
else -> SendState.UNDELIVERED
}
localEchoUpdater.updateSendState(localEvent.eventId, sendState)
//always return success, or the chain will be stuck for ever!
val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, localEvent, error?.localizedMessage ?: "Error")
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
} }

val safeError = error
val sendState = when (safeError) {
is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
else -> SendState.UNDELIVERED
}
localEchoUpdater.updateSendState(localEvent.eventId, sendState)
//always return success, or the chain will be stuck for ever!
return Result.success()
} }
} }

View File

@ -179,7 +179,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
val content = MessageVideoContent( val content = MessageVideoContent(
type = MessageType.MSGTYPE_VIDEO, type = MessageType.MSGTYPE_VIDEO,
body = attachment.name ?: "video", body = attachment.name ?: "video",
info = VideoInfo( videoInfo = VideoInfo(
mimeType = attachment.mimeType, mimeType = attachment.mimeType,
width = width, width = width,
height = height, height = height,
@ -198,7 +198,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
val content = MessageAudioContent( val content = MessageAudioContent(
type = MessageType.MSGTYPE_AUDIO, type = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio", body = attachment.name ?: "audio",
info = AudioInfo( audioInfo = AudioInfo(
mimeType = attachment.mimeType ?: "audio/mpeg", mimeType = attachment.mimeType ?: "audio/mpeg",
size = attachment.size size = attachment.size
), ),

View File

@ -35,7 +35,8 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
val txID: String, val txID: String,
val roomId: String, val roomId: String,
val eventId: String, val eventId: String,
val reason: String? val reason: String?,
override var lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams


@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@ -44,6 +45,11 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()


if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
}

val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
sessionComponent.inject(this) sessionComponent.inject(this)


@ -62,7 +68,9 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
else -> { else -> {
//TODO mark as failed to send? //TODO mark as failed to send?
//always return success, or the chain will be stuck for ever! //always return success, or the chain will be stuck for ever!
Result.success() Result.success(WorkerParamsFactory.toData(params.copy(
lastFailureMessage = it.localizedMessage
)))
} }
} }
}, { }, {

View File

@ -38,14 +38,14 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
internal data class Params( internal data class Params(
override val userId: String, override val userId: String,
val roomId: String, val roomId: String,
val event: Event val event: Event,
override var lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams


@Inject lateinit var localEchoUpdater: LocalEchoUpdater @Inject lateinit var localEchoUpdater: LocalEchoUpdater
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI


override suspend fun doWork(): Result { override suspend fun doWork(): Result {

val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success() ?: return Result.success()


@ -57,6 +57,13 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
return Result.success() return Result.success()
} }


if (params.lastFailureMessage != null) {
localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED)

// Transmit the error
return Result.success(inputData)
}

localEchoUpdater.updateSendState(event.eventId, SendState.SENDING) localEchoUpdater.updateSendState(event.eventId, SendState.SENDING)
val result = executeRequest<SendResponse> { val result = executeRequest<SendResponse> {
apiCall = roomAPI.send( apiCall = roomAPI.send(

View File

@ -107,7 +107,7 @@ internal class InMemoryTimelineEventFactory @Inject constructor(private val room
senderRoomMember?.avatarUrl) senderRoomMember?.avatarUrl)
} }
val event = eventEntity.asDomain() val event = eventEntity.asDomain()
if (event.getClearType() == EventType.ENCRYPTED) { if (event.getClearType() == EventType.ENCRYPTED && !event.isRedacted()) {
handleEncryptedEvent(event, eventEntity.localId) handleEncryptedEvent(event, eventEntity.localId)
} }


@ -141,6 +141,9 @@ internal class InMemoryTimelineEventFactory @Inject constructor(private val room
Timber.e(failure, "Encrypted event: decryption failed") Timber.e(failure, "Encrypted event: decryption failed")
if (failure is MXDecryptionException) { if (failure is MXDecryptionException) {
event.setCryptoError(failure.cryptoError) event.setCryptoError(failure.cryptoError)
} else {
// Other error
Timber.e("Other error, should be handled")
} }
} }
} }

View File

@ -18,4 +18,7 @@ package im.vector.matrix.android.internal.worker


interface SessionWorkerParams { interface SessionWorkerParams {
val userId: String val userId: String
}
// Null is no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers
var lastFailureMessage: String?
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.riotredesign.core.glide

import com.bumptech.glide.load.Option
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt

const val ElementToDecryptOptionKey = "im.vector.riotx.core.glide.ElementToDecrypt"


val ELEMENT_TO_DECRYPT = Option.memory(
ElementToDecryptOptionKey, ElementToDecrypt("", "", ""))

View File

@ -0,0 +1,43 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.riotredesign.core.glide

import android.content.Context
import android.util.Log

import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule
import im.vector.riotredesign.core.extensions.vectorComponent
import im.vector.riotredesign.features.media.ImageContentRenderer
import java.io.InputStream

@GlideModule
class MyAppGlideModule : AppGlideModule() {

override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setLogLevel(Log.ERROR)
}

override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry.append(ImageContentRenderer.Data::class.java,
InputStream::class.java,
VectorGlideModelLoaderFactory(context.vectorComponent().activeSessionHolder()))
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.riotredesign.core.glide

import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
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.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.features.media.ImageContentRenderer
import okhttp3.OkHttpClient
import okhttp3.Request
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import com.bumptech.glide.load.engine.Resource as Resource1


class VectorGlideModelLoaderFactory(private val activeSessionHolder: ActiveSessionHolder)
: ModelLoaderFactory<ImageContentRenderer.Data, InputStream> {

override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ImageContentRenderer.Data, InputStream> {
return VectorGlideModelLoader(activeSessionHolder)
}

override fun teardown() {
// Is there something to do here?
}

}

class VectorGlideModelLoader(private val activeSessionHolder: ActiveSessionHolder)
: ModelLoader<ImageContentRenderer.Data, InputStream> {
override fun handles(model: ImageContentRenderer.Data): Boolean {
// Always handle
return true
}

override fun buildLoadData(model: ImageContentRenderer.Data, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
return ModelLoader.LoadData(ObjectKey(model), VectorGlideDataFetcher(activeSessionHolder, model, width, height))
}
}

class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolder,
private val data: ImageContentRenderer.Data,
private val width: Int,
private val height: Int)
: DataFetcher<InputStream> {

val client = OkHttpClient()

override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}

private var stream: InputStream? = null

override fun cleanup() {
cancel()
}

override fun getDataSource(): DataSource {
// ?
return DataSource.REMOTE
}

override fun cancel() {
if (stream != null) {
try {
stream?.close() // interrupts decode if any
stream = null
} catch (ignore: IOException) {
Timber.e(ignore)
}
}
}

override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
Timber.v("Load data: $data")
if (data.isLocalFile()) {
val initialFile = File(data.url)
callback.onDataReady(FileInputStream(initialFile))
return
}
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val url = contentUrlResolver.resolveFullSize(data.url)
?: return

val request = Request.Builder()
.url(url)
.build()

val response = client.newCall(request).execute()
val inputStream = response.body()?.byteStream()
Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}")
if (!response.isSuccessful) {
callback.onLoadFailed(IOException("Unexpected code $response"))
return
}
stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) {
MXEncryptedAttachments.decryptAttachment(inputStream, data.elementToDecrypt)
} else {
inputStream
}
callback.onDataReady(stream)
}
}

View File

@ -21,9 +21,9 @@ import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns


fun getFilenameFromUri(context: Context, uri: Uri): String? { fun getFilenameFromUri(context: Context?, uri: Uri): String? {
var result: String? = null var result: String? = null
if (uri.scheme == "content") { if (context != null && uri.scheme == "content") {
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
try { try {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {

View File

@ -616,11 +616,11 @@ class RoomDetailFragment :
} }


override fun onFileMessageClicked(messageFileContent: MessageFileContent) { override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
vectorBaseActivity.notImplemented() vectorBaseActivity.notImplemented("open file")
} }


override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) {
vectorBaseActivity.notImplemented() vectorBaseActivity.notImplemented("open audio file")
} }


override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {

View File

@ -16,6 +16,7 @@


package im.vector.riotredesign.features.home.room.detail package im.vector.riotredesign.features.home.room.detail


import android.net.Uri
import android.text.TextUtils import android.text.TextUtils
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -36,6 +37,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.intent.getFilenameFromUri
import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.resources.UserPreferencesProvider import im.vector.riotredesign.core.resources.UserPreferencesProvider
import im.vector.riotredesign.core.utils.LiveEvent import im.vector.riotredesign.core.utils.LiveEvent
@ -360,13 +362,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro


private fun handleSendMedia(action: RoomDetailActions.SendMedia) { private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachments = action.mediaFiles.map { val attachments = action.mediaFiles.map {
val nameWithExtension = getFilenameFromUri(null, Uri.parse(it.path))

ContentAttachmentData( ContentAttachmentData(
size = it.size, size = it.size,
duration = it.duration, duration = it.duration,
date = it.date, date = it.date,
height = it.height, height = it.height,
width = it.width, width = it.width,
name = it.name, name = nameWithExtension ?: it.name,
path = it.path, path = it.path,
mimeType = it.mimeType, mimeType = it.mimeType,
type = ContentAttachmentData.Type.values()[it.mediaType] type = ContentAttachmentData.Type.values()[it.mediaType]

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.riotredesign.EmojiCompatFontProvider import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
@ -71,7 +72,7 @@ class MessageItemFactory @Inject constructor(


val informationData = messageInformationDataFactory.create(event, nextEvent) val informationData = messageInformationDataFactory.create(event, nextEvent)


if (event.root.unsignedData?.redactedEvent != null) { if (event.root.isRedacted()) {
//message is redacted //message is redacted
return buildRedactedItem(informationData, highlight, callback) return buildRedactedItem(informationData, highlight, callback)
} }
@ -179,7 +180,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
filename = messageContent.body, filename = messageContent.body,
url = messageContent.url, url = messageContent.encryptedFileInfo?.url ?: messageContent.url,
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageContent.info?.height, height = messageContent.info?.height,
maxHeight = maxHeight, maxHeight = maxHeight,
width = messageContent.info?.width, width = messageContent.info?.width,
@ -220,10 +222,11 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body, filename = messageContent.body,
url = messageContent.info?.thumbnailUrl, url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
height = messageContent.info?.height, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight, maxHeight = maxHeight,
width = messageContent.info?.width, width = messageContent.videoInfo?.width,
maxWidth = maxWidth maxWidth = maxWidth
) )



View File

@ -55,7 +55,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me


// Crypto // Crypto
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, highlight, callback) EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(event, nextEvent, highlight, callback)
} else {
encryptedItemFactory.create(event, nextEvent, highlight, callback)
}
}


// Unhandled event types (yet) // Unhandled event types (yet)
EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_THIRD_PARTY_INVITE,

View File

@ -22,8 +22,8 @@ import android.widget.ImageView
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.github.piasy.biv.view.BigImageView import com.github.piasy.biv.view.BigImageView
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.riotredesign.core.di.ActiveSessionHolder import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
@ -37,6 +37,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
data class Data( data class Data(
val filename: String, val filename: String,
val url: String?, val url: String?,
val elementToDecrypt: ElementToDecrypt?,
val height: Int?, val height: Int?,
val maxHeight: Int, val maxHeight: Int,
val width: Int?, val width: Int?,
@ -59,17 +60,28 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
val (width, height) = processSize(data, mode) val (width, height) = processSize(data, mode)
imageView.layoutParams.height = height imageView.layoutParams.height = height
imageView.layoutParams.width = width imageView.layoutParams.width = width
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) {
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
//Fallback to base url
?: data.url


GlideApp val glideRequest = if (data.elementToDecrypt != null) {
.with(imageView) // Encrypted image
.load(resolvedUrl) GlideApp
.with(imageView)
.load(data)
} else {
// Clear image
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) {
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
//Fallback to base url
?: data.url

GlideApp
.with(imageView)
.load(resolvedUrl)
}

glideRequest
.dontAnimate() .dontAnimate()
.transform(RoundedCorners(dpToPx(8, imageView.context))) .transform(RoundedCorners(dpToPx(8, imageView.context)))
.thumbnail(0.3f) .thumbnail(0.3f)
@ -81,6 +93,8 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val fullSize = contentUrlResolver.resolveFullSize(data.url) val fullSize = contentUrlResolver.resolveFullSize(data.url)
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)

// TODO DECRYPT_FILE Decrypt file
imageView.showImage( imageView.showImage(
Uri.parse(thumbnail), Uri.parse(thumbnail),
Uri.parse(fullSize) Uri.parse(fullSize)

View File

@ -20,9 +20,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
import com.github.piasy.biv.view.GlideImageViewFactory import com.github.piasy.biv.view.GlideImageViewFactory
import im.vector.riotredesign.core.di.ScreenComponent import im.vector.riotredesign.core.di.ScreenComponent
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.VectorBaseActivity import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_image_media_viewer.* import kotlinx.android.synthetic.main.activity_image_media_viewer.*
import javax.inject.Inject import javax.inject.Inject
@ -44,9 +46,26 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
finish() finish()
} else { } else {
configureToolbar(imageMediaViewerToolbar, mediaData) configureToolbar(imageMediaViewerToolbar, mediaData)
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator()) if (mediaData.elementToDecrypt != null) {
imageContentRenderer.render(mediaData, imageMediaViewerImageView) // Encrypted image
imageMediaViewerImageView.isVisible = false
encryptedImageView.isVisible = true

GlideApp
.with(this)
.load(mediaData)
.dontAnimate()
.into(encryptedImageView)
} else {
// Clear image
imageMediaViewerImageView.isVisible = true
encryptedImageView.isVisible = false

imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
imageContentRenderer.render(mediaData, imageMediaViewerImageView)
}
} }
} }



View File

@ -26,6 +26,7 @@ import javax.inject.Inject


class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){


// TODO DECRYPT_FILE Encrypted data
@Parcelize @Parcelize
data class Data( data class Data(
val filename: String, val filename: String,

View File

@ -32,6 +32,7 @@ import androidx.core.view.isVisible
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import com.bumptech.glide.Glide
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.vector.riotredesign.R import im.vector.riotredesign.R
@ -46,6 +47,10 @@ import im.vector.riotredesign.core.utils.toast
import im.vector.riotredesign.features.MainActivity import im.vector.riotredesign.features.MainActivity
import im.vector.riotredesign.features.themes.ThemeUtils import im.vector.riotredesign.features.themes.ThemeUtils
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*


@ -197,6 +202,18 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {


it.onPreferenceClickListener = Preference.OnPreferenceClickListener { it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notImplemented() notImplemented()

// TODO DECRYPT_FILE Quick implementation of clear cache, finish this
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
Glide.get(requireContext()).clearMemory()

withContext(Dispatchers.IO) {
// On BG thread
Glide.get(requireContext()).clearDiskCache()
}
}

/* TODO /* TODO
displayLoadingView() displayLoadingView()



View File

@ -119,7 +119,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() {
context?.let { context: Context -> context?.let { context: Context ->
AlertDialog.Builder(context) AlertDialog.Builder(context)
.setSingleChoiceItems(R.array.media_saving_choice, .setSingleChoiceItems(R.array.media_saving_choice,
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n -> PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
PreferencesManager.setSelectedMediasSavingPeriod(activity, n) PreferencesManager.setSelectedMediasSavingPeriod(activity, n)
d.cancel() d.cancel()



View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
@ -18,4 +19,11 @@
app:failureImageInitScaleType="center" app:failureImageInitScaleType="center"
app:optimizeDisplay="true" /> app:optimizeDisplay="true" />


<ImageView
android:id="@+id/encryptedImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible" />

</LinearLayout> </LinearLayout>