diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 0f978413..90f52474 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult +import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult @@ -107,4 +108,8 @@ interface CryptoService { fun clearCryptoCache(callback: MatrixCallback) + fun addNewSessionListener(newSessionListener: NewSessionListener) + + fun removeSessionListener(listener: NewSessionListener) + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 5ae49f78..26587359 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -228,4 +228,8 @@ data class Event( } } + /** + * Tells if the event is redacted + */ + fun isRedacted() = unsignedData?.redactedEvent != null } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index 5cf6941b..5758de3e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -128,6 +128,7 @@ class CreateRoomParams { contentMap["algorithm"] = algorithm val algoEvent = Event(type = EventType.ENCRYPTION, + stateKey = "", content = contentMap.toContent() ) @@ -161,6 +162,7 @@ class CreateRoomParams { contentMap["history_visibility"] = historyVisibility val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY, + stateKey = "", content = contentMap.toContent()) if (null == initialStates) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/AudioInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/AudioInfo.kt index 6a28a1b3..de30d460 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/AudioInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/AudioInfo.kt @@ -21,7 +21,18 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class AudioInfo( + /** + * The mimetype of the audio e.g. "audio/aac". + */ @Json(name = "mimetype") val mimeType: String, + + /** + * The size of the audio clip in bytes. + */ @Json(name = "size") val size: Long = 0, + + /** + * The duration of the audio in milliseconds. + */ @Json(name = "duration") val duration: Int = 0 ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/FileInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/FileInfo.kt index 16dc4d23..d5dfb04f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/FileInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/FileInfo.kt @@ -18,11 +18,32 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class FileInfo( + /** + * The mimetype of the file e.g. application/msword. + */ @Json(name = "mimetype") val mimeType: String?, + + /** + * The size of the file in bytes. + */ @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_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 ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ImageInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ImageInfo.kt index 69e88e05..729bc604 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ImageInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ImageInfo.kt @@ -18,15 +18,52 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class ImageInfo( + /** + * The mimetype of the image, e.g. "image/jpeg". + */ @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, + + /** + * 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, + + /** + * Size of the image in bytes. + */ @Json(name = "size") val size: Int = 0, + + /** + * Not documented + */ @Json(name = "rotation") val rotation: Int = 0, + + /** + * Not documented + */ @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_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 ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/LocationInfo.kt index 0f911a9b..f00d4882 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/LocationInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/LocationInfo.kt @@ -18,9 +18,22 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) 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_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 ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt index 9b33b007..7a9ccf7a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt @@ -20,13 +20,35 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass 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.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class MessageAudioContent( + /** + * Not documented + */ @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 = "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 = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, - @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent \ No newline at end of file + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * 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 \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncyptedContent.kt similarity index 57% rename from vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncyptedContent.kt index 08fed579..3a98701c 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEncyptedContent.kt @@ -14,21 +14,14 @@ * 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 android.util.Log; +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo -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? } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt index 8f58294c..1b7f1798 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt @@ -20,14 +20,37 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass 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.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class MessageFileContent( + /** + * Not documented + */ @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, + + /** + * The original filename of the uploaded file. + */ @Json(name = "filename") val filename: String? = null, + + /** + * Information about the file referred to in url. + */ @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 = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, - @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent \ No newline at end of file + @Json(name = "m.new_content") override val newContent: Content? = null, + + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageEncyptedContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt index 2c978b97..50feb484 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt @@ -20,13 +20,36 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass 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.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class MessageImageContent( + /** + * Required. Must be 'm.image'. + */ @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, + + /** + * Metadata about the image referred to in url. + */ @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 = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, - @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent \ No newline at end of file + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * 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 \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt index ddd67af9..deddec12 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt @@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC @JsonClass(generateAdapter = true) data class MessageLocationContent( + /** + * Not documented + */ @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, + + /** + * Required. A geo URI representing this location. + */ @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.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt index 40c29942..11845d4d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt @@ -20,13 +20,35 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass 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.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class MessageVideoContent( + /** + * Required. Must be 'm.video'. + */ @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 = "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 = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, - @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent \ No newline at end of file + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * 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 \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ThumbnailInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ThumbnailInfo.kt index aa68d8f0..1e308d79 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ThumbnailInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ThumbnailInfo.kt @@ -21,8 +21,23 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) 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, + + /** + * 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, + + /** + * Size of the image in bytes. + */ @Json(name = "size") val size: Long = 0, + + /** + * The mimetype of the image, e.g. "image/jpeg". + */ @Json(name = "mimetype") val mimeType: String ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt index 701a6272..06220378 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt @@ -18,14 +18,47 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class VideoInfo( + /** + * The mimetype of the video e.g. "video/mp4". + */ @Json(name = "mimetype") val mimeType: String, + + /** + * The width of the video in pixels. + */ @Json(name = "w") val width: Int = 0, + + /** + * The height of the video in pixels. + */ @Json(name = "h") val height: Int = 0, + + /** + * The size of the video in bytes. + */ @Json(name = "size") val size: Long = 0, + + /** + * The duration of the video in milliseconds. + */ @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_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 ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 3341d87e..14b9e334 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -34,6 +34,7 @@ data class TimelineEvent( val isUniqueDisplayName: Boolean, val senderAvatar: String?, val sendState: SendState, + val hasClearEventFlag: Boolean = false, val annotations: EventAnnotationsSummary? = null ) { diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt similarity index 99% rename from vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt index 1cbb28ae..6dc0d789 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotredesign.core.utils +package im.vector.matrix.android.api.util import android.content.Context import android.os.Build diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 472d653d..2459c354 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -23,8 +23,8 @@ import dagger.Provides import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore +import im.vector.matrix.android.internal.database.configureEncryption import im.vector.matrix.android.internal.di.AuthDatabase -import im.vector.matrix.android.internal.di.MatrixScope import io.realm.RealmConfiguration import java.io.File @@ -42,6 +42,7 @@ internal abstract class AuthModule { old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm")) } return RealmConfiguration.Builder() + .configureEncryption("matrix-sdk-auth", context) .name("matrix-sdk-auth.realm") .modules(AuthRealmModule()) .deleteRealmIfMigrationNeeded() @@ -50,7 +51,6 @@ internal abstract class AuthModule { } - @Binds abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt index 8f34a080..d666bab3 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt @@ -1064,6 +1064,13 @@ internal class CryptoManager @Inject constructor( .executeBy(taskExecutor) } + override fun addNewSessionListener(newSessionListener: NewSessionListener) { + roomDecryptorProvider.addNewSessionListener(newSessionListener) + } + + override fun removeSessionListener(listener: NewSessionListener) { + roomDecryptorProvider.removeSessionListener(listener) + } /* ========================================================================================== * DEBUG INFO * ========================================================================================== */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 89baf1c6..32a5b3d2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -29,12 +29,13 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule -import im.vector.matrix.android.internal.crypto.store.db.hash import im.vector.matrix.android.internal.crypto.tasks.* +import im.vector.matrix.android.internal.database.configureEncryption import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask +import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import retrofit2.Retrofit import java.io.File @@ -45,14 +46,16 @@ internal abstract class CryptoModule { @Module companion object { - // Realm configuration, named to avoid clash with main cache realm configuration @JvmStatic @Provides @CryptoDatabase @SessionScope fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration { + val userIDHash = credentials.userId.md5() + return RealmConfiguration.Builder() - .directory(File(context.filesDir, credentials.userId.hash())) + .directory(File(context.filesDir, userIDHash)) + .configureEncryption("crypto_module_$userIDHash", context) .name("crypto_store.realm") .modules(RealmCryptoStoreModule()) .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) @@ -169,6 +172,4 @@ internal abstract class CryptoModule { @Binds abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask - - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/NewSessionListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/NewSessionListener.kt new file mode 100644 index 00000000..7881b1ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/NewSessionListener.kt @@ -0,0 +1,21 @@ +/* + * 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.matrix.android.internal.crypto + + +interface NewSessionListener { + fun onNewSession(roomId: String?, senderKey: String, sessionId: String) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt index 7d424f56..3dd7b932 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt @@ -34,6 +34,16 @@ internal class RoomDecryptorProvider @Inject constructor( // A map from algorithm to MXDecrypting instance, for each room private val roomDecryptors: MutableMap> = HashMap() + private val newSessionListeners = ArrayList() + + fun addNewSessionListener(listener: NewSessionListener) { + if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) + } + + fun removeSessionListener(listener: NewSessionListener) { + newSessionListeners.remove(listener) + } + /** * Get a decryptor for a given room and algorithm. * If we already have a decryptor for the given room and algorithm, return @@ -64,7 +74,19 @@ internal class RoomDecryptorProvider @Inject constructor( val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm) if (decryptingClass) { val alg = when (algorithm) { - MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create() + MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply { + this.newSessionListener = object : NewSessionListener { + override fun onNewSession(rid: String?, senderKey: String, sessionId: String) { + newSessionListeners.forEach { + try { + it.onNewSession(roomId, senderKey, sessionId) + } catch (e: Throwable) { + + } + } + } + } + } else -> olmDecryptionFactory.create() } if (roomId != null && !TextUtils.isEmpty(roomId)) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 086d4af5..69992df6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -55,6 +55,8 @@ internal class MXMegolmDecryption(private val credentials: Credentials, private val coroutineDispatchers: MatrixCoroutineDispatchers) : IMXDecrypting { + var newSessionListener: NewSessionListener? = null + /** * Events which we couldn't decrypt due to unknown sessions / indexes: map from * senderKey|sessionId to timelines to list of MatrixEvents. @@ -203,7 +205,8 @@ internal class MXMegolmDecryption(private val credentials: Credentials, if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { Timber.v("## onRoomKeyEvent(), forward adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId + " sessionKey " + roomKeyContent.sessionKey) // from " + event); - val forwardedRoomKeyContent = event.getClearContent().toModel() ?: return + val forwardedRoomKeyContent = event.getClearContent().toModel() + ?: return forwardingCurve25519KeyChain = if (forwardedRoomKeyContent.forwardingCurve25519KeyChain == null) { ArrayList() } else { @@ -275,43 +278,8 @@ internal class MXMegolmDecryption(private val credentials: Credentials, * @param sessionId the session id */ override fun onNewSession(senderKey: String, sessionId: String) { - //TODO see how to handle this Timber.v("ON NEW SESSION $sessionId - $senderKey") - /*val k = "$senderKey|$sessionId" - - val pending = pendingEvents[k] - - if (null != pending) { - // Have another go at decrypting events sent with this session. - pendingEvents.remove(k) - - val timelineIds = pending.keys - - for (timelineId in timelineIds) { - val events = pending[timelineId] - - for (event in events!!) { - var result: MXEventDecryptionResult? = null - - try { - result = decryptEvent(event, timelineId) - } catch (e: MXDecryptionException) { - Timber.e(e, "## onNewSession() : Still can't decrypt " + event.eventId + ". Error") - event.setCryptoError(e.cryptoError) - } - - if (null != result) { - val fResut = result - CryptoAsyncHelper.getUiHandler().post { - event.setClearData(fResut) - //mSession!!.onEventDecrypted(event) - } - Timber.v("## onNewSession() : successful re-decryption of " + event.eventId) - } - } - } - } - */ + newSessionListener?.onNewSession(null, senderKey, sessionId) } override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/ElementToDecrypt.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/ElementToDecrypt.kt new file mode 100644 index 00000000..9f1bf708 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/ElementToDecrypt.kt @@ -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 \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt similarity index 85% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEncryptedAttachments.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index 47b84af1..7fe82421 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -14,7 +14,7 @@ * 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.util.Base64 @@ -42,7 +42,7 @@ object MXEncryptedAttachments { */ data class EncryptionResult( 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))!!), v = "v2" ), - encryptedStream = ByteArrayInputStream(outStream.toByteArray()) + encryptedByteArray = outStream.toByteArray() ) outStream.close() @@ -142,43 +142,37 @@ object MXEncryptedAttachments { * @return the decrypted attachment stream */ 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 - if (null == attachmentStream || null == encryptedFileInfo) { - Timber.e("## decryptAttachment() : null parameters") + if (null == attachmentStream || elementToDecrypt == null) { + Timber.e("## decryptAttachment() : null stream") 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 outStream = ByteArrayOutputStream() try { - val key = Base64.decode(base64UrlToBase64(encryptedFileInfo.key!!.k), Base64.DEFAULT) - val initVectorBytes = Base64.decode(encryptedFileInfo.iv, Base64.DEFAULT) + 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) @@ -205,7 +199,7 @@ object MXEncryptedAttachments { 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") outStream.close() return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt index 3e0d912f..5e09b20c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt @@ -15,14 +15,75 @@ */ package im.vector.matrix.android.internal.crypto.model.rest +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +/** + * In Matrix specs: EncryptedFile + */ @JsonClass(generateAdapter = true) data class EncryptedFileInfo( + /** + * Required. The URL to the file. + */ + @Json(name = "url") 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 iv: String, - var hashes: Map, + + /** + * 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? = null, + + /** + * Required. Version of the encrypted attachments protocol. Must be "v2". + */ + @Json(name = "v") 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 + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt index 433a3619..3cf1e308 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt @@ -15,14 +15,66 @@ */ package im.vector.matrix.android.internal.crypto.model.rest +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class EncryptedFileKey( - var alg: String, - var ext: Boolean? = null, - var key_ops: List, - var kty: String, - var k: String -) + /** + * Required. Algorithm. Must be "A256CTR". + */ + @Json(name = "alg") + 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? = 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 + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index d03d0721..1b19db2b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.store import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper @@ -376,4 +377,8 @@ internal interface IMXCryptoStore { * @return an IncomingRoomKeyRequest if it exists, else null */ fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? + + fun addNewSessionListener(listener: NewSessionListener) + + fun removeSessionListener(listener: NewSessionListener) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt index cbf941f1..4ba484ae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt @@ -25,27 +25,9 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream -import java.security.MessageDigest import java.util.zip.GZIPInputStream -/** - * Compute a Hash of a String, using md5 algorithm - */ -fun String.hash() = try { - val digest = MessageDigest.getInstance("md5") - digest.update(toByteArray()) - val bytes = digest.digest() - val sb = StringBuilder() - for (i in bytes.indices) { - sb.append(String.format("%02X", bytes[i])) - } - sb.toString().toLowerCase() -} catch (exc: Exception) { - // Should not happen, but just in case - hashCode().toString() -} - /** * Get realm, invoke the action, close realm, and return the result of the action */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index 1cbd21a1..fa944465 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.store.db import android.text.TextUtils import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper @@ -57,6 +58,17 @@ internal class RealmCryptoStore(private val enableFileEncryption: Boolean = fals // Cache for InboundGroupSession, to release them properly private val inboundGroupSessionToRelease = HashMap() + + private val newSessionListeners = ArrayList() + + override fun addNewSessionListener(listener: NewSessionListener) { + if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) + } + + override fun removeSessionListener(listener: NewSessionListener) { + newSessionListeners.remove(listener) + } + /* ========================================================================================== * Other data * ========================================================================================== */ @@ -718,4 +730,5 @@ internal class RealmCryptoStore(private val enableFileEncryption: Boolean = fals } .toMutableList() } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt new file mode 100644 index 00000000..ee8ee418 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt @@ -0,0 +1,106 @@ +/* + * 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.matrix.android.internal.database + +import android.content.Context +import android.util.Base64 +import im.vector.matrix.android.api.util.SecretStoringUtils +import io.realm.RealmConfiguration +import timber.log.Timber +import java.security.SecureRandom + +/** + * On creation a random key is generated, this key is then encrypted using the system KeyStore. + * The encrypted key is stored in shared preferences. + * When the database is opened again, the encrypted key is taken from the shared pref, + * then the Keystore is used to decrypt the key. The decrypted key is passed to the RealConfiguration. + * + * On android >=M, the KeyStore generates an AES key to encrypt/decrypt the database key, + * and the encrypted key is stored with the initialization vector in base64 in the shared pref. + * On android , val eventEntity = event.toEntity(roomId).apply { this.stateIndex = stateIndex this.isUnlinked = isUnlinked + this.sendState = SendState.SYNCED } untimelinedStateEvents.add(0, eventEntity) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 10d4abc9..1a3abc40 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.database.LiveEntityObserver +import im.vector.matrix.android.internal.database.configureEncryption import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.SessionDatabase @@ -74,6 +75,7 @@ internal abstract class SessionModule { return RealmConfiguration.Builder() .directory(directory) .name("disk_store.realm") + .configureEncryption("session_db_$childPath", context) .modules(SessionRealmModule()) .deleteRealmIfMigrationNeeded() .build() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 61052084..2ec17248 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -54,7 +54,6 @@ internal class FileUploader @Inject constructor(@Authenticated val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray) return upload(uploadBody, filename, progressListener) - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 4c3235e1..2597ef4e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -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.toModel 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.session.room.send.SendEventWorker import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import timber.log.Timber +import java.io.ByteArrayInputStream import java.io.File +import java.io.FileInputStream import javax.inject.Inject @@ -42,7 +46,9 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : override val userId: String, val roomId: String, val event: Event, - val attachment: ContentAttachmentData + val attachment: ContentAttachmentData, + val isRoomEncrypted: Boolean, + override var lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var fileUploader: FileUploader @@ -52,19 +58,41 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + } + val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() sessionComponent.inject(this) val eventId = params.event.eventId ?: return Result.success() val attachment = params.attachment + val isRoomEncrypted = params.isRoomEncrypted + + val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment) val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure() var uploadedThumbnailUrl: String? = null + var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null if (thumbnailData != null) { - fileUploader - .uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType) + val contentUploadResponse = if (isRoomEncrypted) { + 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( { Timber.e(it) }, { uploadedThumbnailUrl = it.contentUri } @@ -76,11 +104,28 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : 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( - { handleFailure(params) }, - { handleSuccess(params, it.contentUri, uploadedThumbnailUrl) } + { handleFailure(params, it) }, + { 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!!) - return Result.success() + return Result.success( + WorkerParamsFactory.toData( + params.copy( + lastFailureMessage = failure.localizedMessage + ) + ) + ) } private fun handleSuccess(params: Params, attachmentUrl: String, - thumbnailUrl: String?): Result { + encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String?, + thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result { 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) 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 updatedContent = when (messageContent) { - is MessageImageContent -> messageContent.update(url) - is MessageVideoContent -> messageContent.update(url, thumbnailUrl) - is MessageFileContent -> messageContent.update(url) - is MessageAudioContent -> messageContent.update(url) + is MessageImageContent -> messageContent.update(url, encryptedFileInfo) + is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) + is MessageFileContent -> messageContent.update(url, encryptedFileInfo) + is MessageAudioContent -> messageContent.update(url, encryptedFileInfo) else -> messageContent } return event.copy(content = updatedContent.toContent()) } - private fun MessageImageContent.update(url: String): MessageImageContent { - return copy(url = url) + private fun MessageImageContent.update(url: String, + 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 { - return copy(url = url, info = info?.copy(thumbnailUrl = thumbnailUrl)) + private fun MessageVideoContent.update(url: String, + 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 { - return copy(url = url) + private fun MessageFileContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?): MessageFileContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url) + ) } - private fun MessageAudioContent.update(url: String): MessageAudioContent { - return copy(url = url) + private fun MessageAudioContent.update(url: String, + encryptedFileInfo: EncryptedFileInfo?): MessageAudioContent { + return copy( + url = if (encryptedFileInfo == null) url else null, + encryptedFileInfo = encryptedFileInfo?.copy(url = url) + ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt index 4b3d6453..a619b83a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt @@ -20,28 +20,26 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import arrow.core.Try -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject 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.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent 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) internal data class Params( override val userId: String, - val groupIds: List - ): SessionWorkerParams + val groupIds: List, + override var lastFailureMessage: String? = null + ) : SessionWorkerParams @Inject lateinit var getGroupDataTask: GetGroupDataTask override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.failure() + ?: return Result.failure() val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() sessionComponent.inject(this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 51e5ba0e..d49471fb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -64,7 +64,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, fun create(roomId: String): Room { val timelineEventFactory = InMemoryTimelineEventFactory(SenderRoomMemberExtractor(), EventRelationExtractor(), cryptoService) - val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask) + val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, cryptoService, paginationTask) val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 3edfed82..7b90a8ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -39,6 +39,7 @@ internal class DefaultReadService @Inject constructor(private val roomId: String private val credentials: Credentials) : ReadService { override fun markAllAsRead(callback: MatrixCallback) { + //TODO shouldn't it be latest synced event? val latestEvent = getLatestEvent() val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId) setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 2146fdf0..7ff2d009 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.read import arrow.core.Try import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity @@ -29,10 +28,11 @@ import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoo import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.tryTransactionAsync +import timber.log.Timber import javax.inject.Inject internal interface SetReadMarkersTask : Task { @@ -48,21 +48,28 @@ private const val READ_MARKER = "m.fully_read" private const val READ_RECEIPT = "m.read" internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI, - private val credentials: Credentials, - private val monarchy: Monarchy + private val credentials: Credentials, + private val monarchy: Monarchy ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params): Try { val markers = HashMap() - if (params.fullyReadEventId != null && MatrixPatterns.isEventId(params.fullyReadEventId)) { - markers[READ_MARKER] = params.fullyReadEventId + if (params.fullyReadEventId != null) { + if (LocalEchoEventFactory.isLocalEchoId(params.fullyReadEventId)) { + Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") + } else { + markers[READ_MARKER] = params.fullyReadEventId + } } if (params.readReceiptEventId != null - && MatrixPatterns.isEventId(params.readReceiptEventId) && !isEventRead(params.roomId, params.readReceiptEventId)) { - updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId) - markers[READ_RECEIPT] = params.readReceiptEventId + if (LocalEchoEventFactory.isLocalEchoId(params.readReceiptEventId)) { + Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") + } else { + updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId) + markers[READ_RECEIPT] = params.readReceiptEventId + } } return if (markers.isEmpty()) { Try.just(Unit) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 8d93c8b0..31c381f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -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.helper.addSendingEvent 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.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.session.room.send.EncryptEventWorker 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? private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest { - - val sendContentWorkerParams = RedactEventWorker.Params(credentials.userId, localEvent.eventId!!, - roomId, eventId, reason) + val sendContentWorkerParams = RedactEventWorker.Params( + credentials.userId, + localEvent.eventId!!, + roomId, + eventId, + reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(redactWorkData) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt index 16e5f52f..a04c7b3e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt @@ -39,7 +39,8 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : override val userId: String, val roomId: String, val event: Event, - val relationType: String? = null + val relationType: String? = null, + override var lastFailureMessage: String? ) : SessionWorkerParams @Inject lateinit var roomAPI: RoomAPI @@ -48,6 +49,11 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + } + val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() sessionComponent.inject(this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index fb6f1be0..27e9c62e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -101,13 +101,26 @@ internal class DefaultSendService @Inject constructor(private val context: Conte val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { saveLocalEcho(it) } - val uploadWork = createUploadMediaWork(event, attachment) + + val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) + + val uploadWork = createUploadMediaWork(event, attachment, isRoomEncrypted) val sendWork = createSendEventWork(event) - WorkManager.getInstance(context) - .beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) - .then(sendWork) - .enqueue() + if (isRoomEncrypted) { + val encryptWork = createEncryptEventWork(event) + + 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) } @@ -148,8 +161,8 @@ internal class DefaultSendService @Inject constructor(private val context: Conte return TimelineSendEventWorkCommon.createWork(redactWorkData) } - private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest { - val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment) + private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData, isRoomEncrypted: Boolean): OneTimeWorkRequest { + val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment, isRoomEncrypted) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return OneTimeWorkRequestBuilder() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index 9e68f6d1..44046a1d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -41,7 +41,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) val roomId: String, val event: Event, /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ - val keepKeys: List? = null + val keepKeys: List? = null, + override var lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var crypto: CryptoService @@ -52,6 +53,11 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + } + val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() sessionComponent.inject(this) @@ -105,16 +111,15 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) ) val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, encryptedEvent) 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() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 6e80e7c6..ce2d6e4c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -29,9 +29,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent import im.vector.matrix.android.internal.database.helper.addSendingEvent -import im.vector.matrix.android.internal.database.model.ChunkEntity 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.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater @@ -70,7 +68,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials } private fun isFormattedTextPertinent(text: String, htmlText: String?) = - text != htmlText && htmlText != "

$text

\n" + text != htmlText && htmlText != "

${text.trim()}

\n" fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event { val content = MessageTextContent( @@ -179,7 +177,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials val content = MessageVideoContent( type = MessageType.MSGTYPE_VIDEO, body = attachment.name ?: "video", - info = VideoInfo( + videoInfo = VideoInfo( mimeType = attachment.mimeType, width = width, height = height, @@ -198,7 +196,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials val content = MessageAudioContent( type = MessageType.MSGTYPE_AUDIO, body = attachment.name ?: "audio", - info = AudioInfo( + audioInfo = AudioInfo( mimeType = attachment.mimeType ?: "audio/mpeg", size = attachment.size ), @@ -238,7 +236,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials } private fun dummyEventId(roomId: String): String { - return "m.${UUID.randomUUID()}" + return "$LOCAL_ID_PREFIX${UUID.randomUUID()}" } fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? { @@ -353,4 +351,9 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials } } + companion object { + const val LOCAL_ID_PREFIX = "local." + + fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt index b3e2edcf..38e0e23b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt @@ -35,7 +35,8 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C val txID: String, val roomId: String, val eventId: String, - val reason: String? + val reason: String?, + override var lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var roomAPI: RoomAPI @@ -44,6 +45,11 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() + if (params.lastFailureMessage != null) { + // Transmit the error + return Result.success(inputData) + } + val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() sessionComponent.inject(this) @@ -62,7 +68,9 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C else -> { //TODO mark as failed to send? //always return success, or the chain will be stuck for ever! - Result.success() + Result.success(WorkerParamsFactory.toData(params.copy( + lastFailureMessage = it.localizedMessage + ))) } } }, { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 6b6585ef..315ea457 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -38,14 +38,14 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam internal data class Params( override val userId: String, val roomId: String, - val event: Event + val event: Event, + override var lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var localEchoUpdater: LocalEchoUpdater @Inject lateinit var roomAPI: RoomAPI override suspend fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() @@ -57,6 +57,13 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam 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) val result = executeRequest { apiCall = roomAPI.send( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 42d4a22c..72c07527 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -20,11 +20,15 @@ import android.os.Handler import android.os.HandlerThread import android.os.Looper import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.addTo +import im.vector.matrix.android.internal.crypto.NewSessionListener +import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.findIncludingEvent @@ -56,6 +60,7 @@ internal class DefaultTimeline( private val contextOfEventTask: GetContextOfEventTask, private val timelineEventFactory: CacheableTimelineEventFactory, private val paginationTask: PaginationTask, + private val cryptoService: CryptoService, private val allowedTypes: List? ) : Timeline { @@ -159,6 +164,33 @@ internal class DefaultTimeline( postSnapshot() } + private val newSessionListener = object : NewSessionListener { + override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { + if (roomId == this@DefaultTimeline.roomId) { + Timber.v("New session id detected for this room") + backgroundHandler.get()?.post { + val realm = backgroundRealm.get() + var hasChange = false + builtEvents.forEachIndexed { index, timelineEvent -> + if (timelineEvent.isEncrypted()) { + val eventContent = timelineEvent.root.content.toModel() + if (eventContent?.sessionId == sessionId + && (timelineEvent.root.mClearEvent == null || timelineEvent.root.mCryptoError != null)) { + //we need to rebuild this event + EventEntity.where(realm, eventId = timelineEvent.root.eventId!!).findFirst()?.let { + builtEvents[index] = timelineEventFactory.create(it, realm) + hasChange = true + } + } + } + } + if (hasChange) postSnapshot() + } + } + } + + } + // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { @@ -184,6 +216,7 @@ internal class DefaultTimeline( val handler = Handler(handlerThread.looper) this.backgroundHandlerThread.set(handlerThread) this.backgroundHandler.set(handler) + cryptoService.addNewSessionListener(newSessionListener) handler.post { val realm = Realm.getInstance(realmConfiguration) backgroundRealm.set(realm) @@ -211,6 +244,7 @@ internal class DefaultTimeline( override fun dispose() { if (isStarted.compareAndSet(true, false)) { + cryptoService.removeSessionListener(newSessionListener) Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") backgroundHandler.get()?.post { cancelableBag.cancel() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 40aa4677..1d3462e2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.timeline import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineService @@ -35,11 +36,20 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St private val taskExecutor: TaskExecutor, private val timelineEventFactory: CacheableTimelineEventFactory, private val contextOfEventTask: GetContextOfEventTask, + private val cryptoService: CryptoService, private val paginationTask: PaginationTask ) : TimelineService { override fun createTimeline(eventId: String?, allowedTypes: List?): Timeline { - return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, allowedTypes) + return DefaultTimeline(roomId, + eventId, + monarchy.realmConfiguration, + taskExecutor, + contextOfEventTask, + timelineEventFactory, + paginationTask, + cryptoService, + allowedTypes) } override fun getTimeLineEvent(eventId: String): TimelineEvent? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt index e98e0d35..e9a10902 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -69,6 +69,7 @@ internal class SimpleTimelineEventFactory @Inject constructor(private val roomMe isUniqueDisplayName, senderRoomMember?.avatarUrl, eventEntity.sendState, + event.mClearEvent != null, relations ) } @@ -107,7 +108,7 @@ internal class InMemoryTimelineEventFactory @Inject constructor(private val room senderRoomMember?.avatarUrl) } val event = eventEntity.asDomain() - if (event.getClearType() == EventType.ENCRYPTED) { + if (event.getClearType() == EventType.ENCRYPTED && !event.isRedacted()) { handleEncryptedEvent(event, eventEntity.localId) } @@ -120,6 +121,7 @@ internal class InMemoryTimelineEventFactory @Inject constructor(private val room senderData.isUniqueDisplayName, senderData.senderAvatar, eventEntity.sendState, + event.mClearEvent != null, relations ) } @@ -138,9 +140,12 @@ internal class InMemoryTimelineEventFactory @Inject constructor(private val room } event.setClearData(result) } catch (failure: Throwable) { - Timber.e(failure, "Encrypted event: decryption failed") + Timber.e("Encrypted event: decryption failed ${failure.localizedMessage}") if (failure is MXDecryptionException) { event.setCryptoError(failure.cryptoError) + } else { + // Other error + Timber.e("Other error, should be handled") } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt index b97de018..e57c2893 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt @@ -18,6 +18,9 @@ package im.vector.matrix.android.internal.util import java.security.MessageDigest +/** + * Compute a Hash of a String, using md5 algorithm + */ fun String.md5() = try { val digest = MessageDigest.getInstance("md5") digest.update(toByteArray()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt index 874254cc..99fe3142 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/SessionWorkerParams.kt @@ -18,4 +18,7 @@ package im.vector.matrix.android.internal.worker interface SessionWorkerParams { val userId: String -} \ No newline at end of file + + // 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? +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/glide/ElementToDecryptOption.kt b/vector/src/main/java/im/vector/riotredesign/core/glide/ElementToDecryptOption.kt new file mode 100644 index 00000000..79805029 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/glide/ElementToDecryptOption.kt @@ -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("", "", "")) + diff --git a/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.kt b/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.kt new file mode 100644 index 00000000..54d3104c --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.kt @@ -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())) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/riotredesign/core/glide/VectorGlideModelLoader.kt new file mode 100644 index 00000000..dd62cedc --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/glide/VectorGlideModelLoader.kt @@ -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 { + + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return VectorGlideModelLoader(activeSessionHolder) + } + + override fun teardown() { + // Is there something to do here? + } + +} + +class VectorGlideModelLoader(private val activeSessionHolder: ActiveSessionHolder) + : ModelLoader { + 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? { + 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 { + + val client = OkHttpClient() + + override fun getDataClass(): Class { + 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) { + 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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/intent/Filename.kt b/vector/src/main/java/im/vector/riotredesign/core/intent/Filename.kt index eba9d01b..78a04716 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/intent/Filename.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/intent/Filename.kt @@ -21,9 +21,9 @@ import android.database.Cursor import android.net.Uri import android.provider.OpenableColumns -fun getFilenameFromUri(context: Context, uri: Uri): String? { +fun getFilenameFromUri(context: Context?, uri: Uri): String? { 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) try { if (cursor != null && cursor.moveToFirst()) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 3deda667..8c15900d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -616,11 +616,11 @@ class RoomDetailFragment : } override fun onFileMessageClicked(messageFileContent: MessageFileContent) { - vectorBaseActivity.notImplemented() + vectorBaseActivity.notImplemented("open file") } override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { - vectorBaseActivity.notImplemented() + vectorBaseActivity.notImplemented("open audio file") } override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 8d23ac0f..7134110d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotredesign.features.home.room.detail +import android.net.Uri import android.text.TextUtils import androidx.lifecycle.LiveData 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.rx.rx import im.vector.riotredesign.R +import im.vector.riotredesign.core.intent.getFilenameFromUri import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.resources.UserPreferencesProvider import im.vector.riotredesign.core.utils.LiveEvent @@ -360,13 +362,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleSendMedia(action: RoomDetailActions.SendMedia) { val attachments = action.mediaFiles.map { + val nameWithExtension = getFilenameFromUri(null, Uri.parse(it.path)) + ContentAttachmentData( size = it.size, duration = it.duration, date = it.date, height = it.height, width = it.width, - name = it.name, + name = nameWithExtension ?: it.name, path = it.path, mimeType = it.mimeType, type = ContentAttachmentData.Type.values()[it.mediaType] @@ -376,7 +380,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { - displayedEventsObservable.accept(action) + if (action.event.sendState.isSent()) { //ignore pending/local events + displayedEventsObservable.accept(action) + } //We need to update this with the related m.replace also (to move read receipt) action.event.annotations?.editSummary?.sourceEvents?.forEach { room.getTimeLineEvent(it)?.let { event -> diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 82e54ef9..72f73cd5 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -299,9 +299,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it } - MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) { + (MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) { mergeItemCollapseStates[event.localId] = it requestModelBuild() + }).also { + it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) } } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index b8917f1d..0d816480 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -39,6 +39,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent 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.internal.crypto.attachments.toElementToDecrypt import im.vector.riotredesign.EmojiCompatFontProvider import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.VectorEpoxyModel @@ -90,7 +91,7 @@ class MessageItemFactory @Inject constructor( val informationData = messageInformationDataFactory.create(event, nextEvent) - if (event.root.unsignedData?.redactedEvent != null) { + if (event.root.isRedacted()) { //message is redacted return buildRedactedItem(informationData, highlight, callback) } @@ -198,7 +199,8 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val data = ImageContentRenderer.Data( filename = messageContent.body, - url = messageContent.url, + url = messageContent.encryptedFileInfo?.url ?: messageContent.url, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), height = messageContent.info?.height, maxHeight = maxHeight, width = messageContent.info?.width, @@ -239,10 +241,11 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, - url = messageContent.info?.thumbnailUrl, - height = messageContent.info?.height, + url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, + elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), + height = messageContent.videoInfo?.height, maxHeight = maxHeight, - width = messageContent.info?.width, + width = messageContent.videoInfo?.width, maxWidth = maxWidth ) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 32edb0b8..84f4f287 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -55,7 +55,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me // Crypto 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) EventType.STATE_ROOM_THIRD_PARTY_INVITE, diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt index ba6cf27a..0b39bdf5 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt @@ -31,4 +31,19 @@ class TimelineEventVisibilityStateChangedListener(private val callback: Timeline } } +} + + +class MergedTimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, + private val events: List) + : VectorEpoxyModel.OnVisibilityStateChangedListener { + + override fun onVisibilityStateChanged(visibilityState: Int) { + if (visibilityState == VisibilityState.VISIBLE) { + events.forEach { + callback?.onEventVisible(it) + } + } + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt index 1e50f90b..15b7e82f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt @@ -22,8 +22,8 @@ import android.widget.ImageView import androidx.exifinterface.media.ExifInterface import com.bumptech.glide.load.resource.bitmap.RoundedCorners 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.internal.crypto.attachments.ElementToDecrypt import im.vector.riotredesign.core.di.ActiveSessionHolder import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx @@ -37,6 +37,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: data class Data( val filename: String, val url: String?, + val elementToDecrypt: ElementToDecrypt?, val height: Int?, val maxHeight: Int, val width: Int?, @@ -59,17 +60,28 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: val (width, height) = processSize(data, mode) imageView.layoutParams.height = height 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 - .with(imageView) - .load(resolvedUrl) + val glideRequest = if (data.elementToDecrypt != null) { + // Encrypted image + 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() .transform(RoundedCorners(dpToPx(8, imageView.context))) .thumbnail(0.3f) @@ -81,6 +93,8 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val fullSize = contentUrlResolver.resolveFullSize(data.url) val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) + + // TODO DECRYPT_FILE Decrypt file imageView.showImage( Uri.parse(thumbnail), Uri.parse(fullSize) diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt index c68f594f..fc5e2725 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt @@ -20,9 +20,11 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.appcompat.widget.Toolbar +import androidx.core.view.isVisible import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator import com.github.piasy.biv.view.GlideImageViewFactory import im.vector.riotredesign.core.di.ScreenComponent +import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.platform.VectorBaseActivity import kotlinx.android.synthetic.main.activity_image_media_viewer.* import javax.inject.Inject @@ -44,9 +46,26 @@ class ImageMediaViewerActivity : VectorBaseActivity() { finish() } else { configureToolbar(imageMediaViewerToolbar, mediaData) - imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory()) - imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator()) - imageContentRenderer.render(mediaData, imageMediaViewerImageView) + + if (mediaData.elementToDecrypt != null) { + // 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) + } } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt index 8fd4b43e..2eb5f062 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt @@ -26,6 +26,7 @@ import javax.inject.Inject class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ + // TODO DECRYPT_FILE Encrypted data @Parcelize data class Data( val filename: String, diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt index 8c81570b..a66d6395 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt @@ -21,10 +21,10 @@ import android.graphics.Bitmap import androidx.core.app.NotificationCompat import androidx.core.app.Person import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.api.util.SecretStoringUtils import im.vector.riotredesign.BuildConfig import im.vector.riotredesign.R import im.vector.riotredesign.core.di.ActiveSessionHolder -import im.vector.riotredesign.core.utils.SecretStoringUtils import im.vector.riotredesign.features.settings.PreferencesManager import me.gujun.android.span.span import timber.log.Timber diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsGeneralFragment.kt index f059ef62..aa7ce304 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsGeneralFragment.kt @@ -32,6 +32,7 @@ import androidx.core.view.isVisible import androidx.preference.EditTextPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory +import com.bumptech.glide.Glide import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout 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.themes.ThemeUtils 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.util.* @@ -197,6 +202,18 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { 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 displayLoadingView() diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt index a77f0f73..0a82ef9f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt @@ -119,7 +119,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() { context?.let { context: Context -> AlertDialog.Builder(context) .setSingleChoiceItems(R.array.media_saving_choice, - PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n -> + PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n -> PreferencesManager.setSelectedMediasSavingPeriod(activity, n) d.cancel() diff --git a/vector/src/main/java/im/vector/riotredesign/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/workers/signout/SignOutBottomSheetDialogFragment.kt index ff66800a..d9c6d7f9 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -18,6 +18,7 @@ package im.vector.riotredesign.features.workers.signout import android.app.Activity import android.app.Dialog +import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater @@ -41,16 +42,17 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.riotredesign.R +import im.vector.riotredesign.core.di.DaggerScreenComponent +import im.vector.riotredesign.core.platform.VectorBaseActivity import im.vector.riotredesign.core.utils.toast import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity -import javax.inject.Inject class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() { - @Inject lateinit var session: Session - @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + lateinit var session: Session + lateinit var viewModelFactory: ViewModelProvider.Factory @BindView(R.id.bottom_sheet_signout_warning_text) @@ -99,6 +101,14 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() { private lateinit var viewModel: SignOutViewModel + override fun onAttach(context: Context) { + super.onAttach(context) + val vectorBaseActivity = activity as VectorBaseActivity + val screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) + viewModelFactory = screenComponent.viewModelFactory() + session = screenComponent.session() + } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) diff --git a/vector/src/main/res/layout/activity_image_media_viewer.xml b/vector/src/main/res/layout/activity_image_media_viewer.xml index 194b9d60..61d5d286 100644 --- a/vector/src/main/res/layout/activity_image_media_viewer.xml +++ b/vector/src/main/res/layout/activity_image_media_viewer.xml @@ -1,6 +1,7 @@ @@ -18,4 +19,11 @@ app:failureImageInitScaleType="center" app:optimizeDisplay="true" /> + + \ No newline at end of file