forked from GitHub-Mirror/riotX-android
Merge branch 'develop' into feature/Perf
This commit is contained in:
commit
11bf00030d
@ -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.Content
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.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.ImportRoomKeysResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||||
@ -107,4 +108,8 @@ interface CryptoService {
|
|||||||
|
|
||||||
fun clearCryptoCache(callback: MatrixCallback<Unit>)
|
fun clearCryptoCache(callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
|
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||||
|
|
||||||
|
fun removeSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
}
|
}
|
@ -228,4 +228,8 @@ data class Event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if the event is redacted
|
||||||
|
*/
|
||||||
|
fun isRedacted() = unsignedData?.redactedEvent != null
|
||||||
}
|
}
|
@ -128,6 +128,7 @@ class CreateRoomParams {
|
|||||||
contentMap["algorithm"] = algorithm
|
contentMap["algorithm"] = algorithm
|
||||||
|
|
||||||
val algoEvent = Event(type = EventType.ENCRYPTION,
|
val algoEvent = Event(type = EventType.ENCRYPTION,
|
||||||
|
stateKey = "",
|
||||||
content = contentMap.toContent()
|
content = contentMap.toContent()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -161,6 +162,7 @@ class CreateRoomParams {
|
|||||||
contentMap["history_visibility"] = historyVisibility
|
contentMap["history_visibility"] = historyVisibility
|
||||||
|
|
||||||
val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
|
val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
|
||||||
|
stateKey = "",
|
||||||
content = contentMap.toContent())
|
content = contentMap.toContent())
|
||||||
|
|
||||||
if (null == initialStates) {
|
if (null == initialStates) {
|
||||||
|
@ -21,7 +21,18 @@ import com.squareup.moshi.JsonClass
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class AudioInfo(
|
data class AudioInfo(
|
||||||
|
/**
|
||||||
|
* The mimetype of the audio e.g. "audio/aac".
|
||||||
|
*/
|
||||||
@Json(name = "mimetype") val mimeType: String,
|
@Json(name = "mimetype") val mimeType: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the audio clip in bytes.
|
||||||
|
*/
|
||||||
@Json(name = "size") val size: Long = 0,
|
@Json(name = "size") val size: Long = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The duration of the audio in milliseconds.
|
||||||
|
*/
|
||||||
@Json(name = "duration") val duration: Int = 0
|
@Json(name = "duration") val duration: Int = 0
|
||||||
)
|
)
|
@ -18,11 +18,32 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class FileInfo(
|
data class FileInfo(
|
||||||
|
/**
|
||||||
|
* The mimetype of the file e.g. application/msword.
|
||||||
|
*/
|
||||||
@Json(name = "mimetype") val mimeType: String?,
|
@Json(name = "mimetype") val mimeType: String?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the file in bytes.
|
||||||
|
*/
|
||||||
@Json(name = "size") val size: Long = 0,
|
@Json(name = "size") val size: Long = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about the image referred to in thumbnail_url.
|
||||||
|
*/
|
||||||
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
||||||
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null
|
|
||||||
|
/**
|
||||||
|
* The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted.
|
||||||
|
*/
|
||||||
|
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
|
||||||
|
*/
|
||||||
|
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
|
||||||
)
|
)
|
@ -18,15 +18,52 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ImageInfo(
|
data class ImageInfo(
|
||||||
|
/**
|
||||||
|
* The mimetype of the image, e.g. "image/jpeg".
|
||||||
|
*/
|
||||||
@Json(name = "mimetype") val mimeType: String?,
|
@Json(name = "mimetype") val mimeType: String?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The intended display width of the image in pixels. This may differ from the intrinsic dimensions of the image file.
|
||||||
|
*/
|
||||||
@Json(name = "w") val width: Int = 0,
|
@Json(name = "w") val width: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The intended display height of the image in pixels. This may differ from the intrinsic dimensions of the image file.
|
||||||
|
*/
|
||||||
@Json(name = "h") val height: Int = 0,
|
@Json(name = "h") val height: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of the image in bytes.
|
||||||
|
*/
|
||||||
@Json(name = "size") val size: Int = 0,
|
@Json(name = "size") val size: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not documented
|
||||||
|
*/
|
||||||
@Json(name = "rotation") val rotation: Int = 0,
|
@Json(name = "rotation") val rotation: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not documented
|
||||||
|
*/
|
||||||
@Json(name = "orientation") val orientation: Int = 0,
|
@Json(name = "orientation") val orientation: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about the image referred to in thumbnail_url.
|
||||||
|
*/
|
||||||
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
||||||
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null
|
|
||||||
|
/**
|
||||||
|
* The URL (typically MXC URI) to a thumbnail of the image. Only present if the thumbnail is unencrypted.
|
||||||
|
*/
|
||||||
|
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
|
||||||
|
*/
|
||||||
|
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
|
||||||
)
|
)
|
@ -18,9 +18,22 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LocationInfo(
|
data class LocationInfo(
|
||||||
|
/**
|
||||||
|
* The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted.
|
||||||
|
*/
|
||||||
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null,
|
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null,
|
||||||
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null
|
|
||||||
|
/**
|
||||||
|
* Metadata about the image referred to in thumbnail_url.
|
||||||
|
*/
|
||||||
|
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
|
||||||
|
*/
|
||||||
|
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
|
||||||
)
|
)
|
@ -20,13 +20,35 @@ import com.squareup.moshi.Json
|
|||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageAudioContent(
|
data class MessageAudioContent(
|
||||||
|
/**
|
||||||
|
* Not documented
|
||||||
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A description of the audio e.g. 'Bee Gees - Stayin' Alive', or some kind of content description for accessibility e.g. 'audio attachment'.
|
||||||
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "info") val info: AudioInfo? = null,
|
|
||||||
|
/**
|
||||||
|
* Metadata for the audio clip referred to in url.
|
||||||
|
*/
|
||||||
|
@Json(name = "info") val audioInfo: AudioInfo? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip.
|
||||||
|
*/
|
||||||
@Json(name = "url") val url: String? = null,
|
@Json(name = "url") val url: String? = null,
|
||||||
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||||
) : MessageContent
|
|
||||||
|
/**
|
||||||
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
|
*/
|
||||||
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
) : MessageEncyptedContent
|
@ -14,21 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.core.glide;
|
package im.vector.matrix.android.api.session.room.model.message
|
||||||
|
|
||||||
import android.content.Context;
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.bumptech.glide.GlideBuilder;
|
|
||||||
import com.bumptech.glide.annotation.GlideModule;
|
|
||||||
import com.bumptech.glide.module.AppGlideModule;
|
|
||||||
|
|
||||||
@GlideModule
|
|
||||||
public final class MyAppGlideModule extends AppGlideModule {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void applyOptions(Context context, GlideBuilder builder) {
|
|
||||||
builder.setLogLevel(Log.ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for message which can contains encrypted data
|
||||||
|
*/
|
||||||
|
interface MessageEncyptedContent : MessageContent {
|
||||||
|
val encryptedFileInfo: EncryptedFileInfo?
|
||||||
}
|
}
|
@ -20,14 +20,37 @@ import com.squareup.moshi.Json
|
|||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageFileContent(
|
data class MessageFileContent(
|
||||||
|
/**
|
||||||
|
* Not documented
|
||||||
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A human-readable description of the file. This is recommended to be the filename of the original upload.
|
||||||
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original filename of the uploaded file.
|
||||||
|
*/
|
||||||
@Json(name = "filename") val filename: String? = null,
|
@Json(name = "filename") val filename: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the file referred to in url.
|
||||||
|
*/
|
||||||
@Json(name = "info") val info: FileInfo? = null,
|
@Json(name = "info") val info: FileInfo? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the file.
|
||||||
|
*/
|
||||||
@Json(name = "url") val url: String? = null,
|
@Json(name = "url") val url: String? = null,
|
||||||
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||||
) : MessageContent
|
|
||||||
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
) : MessageEncyptedContent
|
@ -20,13 +20,36 @@ import com.squareup.moshi.Json
|
|||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageImageContent(
|
data class MessageImageContent(
|
||||||
|
/**
|
||||||
|
* Required. Must be 'm.image'.
|
||||||
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
|
||||||
|
* or some kind of content description for accessibility e.g. 'image attachment'.
|
||||||
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about the image referred to in url.
|
||||||
|
*/
|
||||||
@Json(name = "info") val info: ImageInfo? = null,
|
@Json(name = "info") val info: ImageInfo? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
||||||
|
*/
|
||||||
@Json(name = "url") val url: String? = null,
|
@Json(name = "url") val url: String? = null,
|
||||||
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||||
) : MessageContent
|
|
||||||
|
/**
|
||||||
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
|
*/
|
||||||
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
) : MessageEncyptedContent
|
@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageLocationContent(
|
data class MessageLocationContent(
|
||||||
|
/**
|
||||||
|
* Not documented
|
||||||
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'.
|
||||||
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A geo URI representing this location.
|
||||||
|
*/
|
||||||
@Json(name = "geo_uri") val geoUri: String,
|
@Json(name = "geo_uri") val geoUri: String,
|
||||||
@Json(name = "info") val info: LocationInfo? = null,
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Json(name = "info") val locationInfo: LocationInfo? = null,
|
||||||
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -20,13 +20,35 @@ import com.squareup.moshi.Json
|
|||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageVideoContent(
|
data class MessageVideoContent(
|
||||||
|
/**
|
||||||
|
* Required. Must be 'm.video'.
|
||||||
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A description of the video e.g. 'Gangnam style', or some kind of content description for accessibility e.g. 'video attachment'.
|
||||||
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "info") val info: VideoInfo? = null,
|
|
||||||
|
/**
|
||||||
|
* Metadata about the video clip referred to in url.
|
||||||
|
*/
|
||||||
|
@Json(name = "info") val videoInfo: VideoInfo? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the video clip.
|
||||||
|
*/
|
||||||
@Json(name = "url") val url: String? = null,
|
@Json(name = "url") val url: String? = null,
|
||||||
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||||
) : MessageContent
|
|
||||||
|
/**
|
||||||
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
|
*/
|
||||||
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
) : MessageEncyptedContent
|
@ -21,8 +21,23 @@ import com.squareup.moshi.JsonClass
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ThumbnailInfo(
|
data class ThumbnailInfo(
|
||||||
|
/**
|
||||||
|
* The intended display width of the image in pixels. This may differ from the intrinsic dimensions of the image file.
|
||||||
|
*/
|
||||||
@Json(name = "w") val width: Int = 0,
|
@Json(name = "w") val width: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The intended display height of the image in pixels. This may differ from the intrinsic dimensions of the image file.
|
||||||
|
*/
|
||||||
@Json(name = "h") val height: Int = 0,
|
@Json(name = "h") val height: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of the image in bytes.
|
||||||
|
*/
|
||||||
@Json(name = "size") val size: Long = 0,
|
@Json(name = "size") val size: Long = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mimetype of the image, e.g. "image/jpeg".
|
||||||
|
*/
|
||||||
@Json(name = "mimetype") val mimeType: String
|
@Json(name = "mimetype") val mimeType: String
|
||||||
)
|
)
|
@ -18,14 +18,47 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class VideoInfo(
|
data class VideoInfo(
|
||||||
|
/**
|
||||||
|
* The mimetype of the video e.g. "video/mp4".
|
||||||
|
*/
|
||||||
@Json(name = "mimetype") val mimeType: String,
|
@Json(name = "mimetype") val mimeType: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The width of the video in pixels.
|
||||||
|
*/
|
||||||
@Json(name = "w") val width: Int = 0,
|
@Json(name = "w") val width: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The height of the video in pixels.
|
||||||
|
*/
|
||||||
@Json(name = "h") val height: Int = 0,
|
@Json(name = "h") val height: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the video in bytes.
|
||||||
|
*/
|
||||||
@Json(name = "size") val size: Long = 0,
|
@Json(name = "size") val size: Long = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The duration of the video in milliseconds.
|
||||||
|
*/
|
||||||
@Json(name = "duration") val duration: Int = 0,
|
@Json(name = "duration") val duration: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about the image referred to in thumbnail_url.
|
||||||
|
*/
|
||||||
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
||||||
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null
|
|
||||||
|
/**
|
||||||
|
* The URL (typically MXC URI) to an image thumbnail of the video clip. Only present if the thumbnail is unencrypted.
|
||||||
|
*/
|
||||||
|
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted.
|
||||||
|
*/
|
||||||
|
@Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
|
||||||
)
|
)
|
@ -34,6 +34,7 @@ data class TimelineEvent(
|
|||||||
val isUniqueDisplayName: Boolean,
|
val isUniqueDisplayName: Boolean,
|
||||||
val senderAvatar: String?,
|
val senderAvatar: String?,
|
||||||
val sendState: SendState,
|
val sendState: SendState,
|
||||||
|
val hasClearEventFlag: Boolean = false,
|
||||||
val annotations: EventAnnotationsSummary? = null
|
val annotations: EventAnnotationsSummary? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* 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
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotredesign.core.utils
|
package im.vector.matrix.android.api.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
@ -23,8 +23,8 @@ import dagger.Provides
|
|||||||
import im.vector.matrix.android.api.auth.Authenticator
|
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.AuthRealmModule
|
||||||
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
|
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.AuthDatabase
|
||||||
import im.vector.matrix.android.internal.di.MatrixScope
|
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ internal abstract class AuthModule {
|
|||||||
old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm"))
|
old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm"))
|
||||||
}
|
}
|
||||||
return RealmConfiguration.Builder()
|
return RealmConfiguration.Builder()
|
||||||
|
.configureEncryption("matrix-sdk-auth", context)
|
||||||
.name("matrix-sdk-auth.realm")
|
.name("matrix-sdk-auth.realm")
|
||||||
.modules(AuthRealmModule())
|
.modules(AuthRealmModule())
|
||||||
.deleteRealmIfMigrationNeeded()
|
.deleteRealmIfMigrationNeeded()
|
||||||
@ -50,7 +51,6 @@ internal abstract class AuthModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
|
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
|
||||||
|
|
||||||
|
@ -1064,6 +1064,13 @@ internal class CryptoManager @Inject constructor(
|
|||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addNewSessionListener(newSessionListener: NewSessionListener) {
|
||||||
|
roomDecryptorProvider.addNewSessionListener(newSessionListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeSessionListener(listener: NewSessionListener) {
|
||||||
|
roomDecryptorProvider.removeSessionListener(listener)
|
||||||
|
}
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* DEBUG INFO
|
* DEBUG INFO
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
@ -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.RealmCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration
|
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.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.crypto.tasks.*
|
||||||
|
import im.vector.matrix.android.internal.database.configureEncryption
|
||||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
||||||
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
|
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
|
||||||
|
import im.vector.matrix.android.internal.util.md5
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -45,14 +46,16 @@ internal abstract class CryptoModule {
|
|||||||
@Module
|
@Module
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
// Realm configuration, named to avoid clash with main cache realm configuration
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@CryptoDatabase
|
@CryptoDatabase
|
||||||
@SessionScope
|
@SessionScope
|
||||||
fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration {
|
fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration {
|
||||||
|
val userIDHash = credentials.userId.md5()
|
||||||
|
|
||||||
return RealmConfiguration.Builder()
|
return RealmConfiguration.Builder()
|
||||||
.directory(File(context.filesDir, credentials.userId.hash()))
|
.directory(File(context.filesDir, userIDHash))
|
||||||
|
.configureEncryption("crypto_module_$userIDHash", context)
|
||||||
.name("crypto_store.realm")
|
.name("crypto_store.realm")
|
||||||
.modules(RealmCryptoStoreModule())
|
.modules(RealmCryptoStoreModule())
|
||||||
.schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
|
.schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
|
||||||
@ -169,6 +172,4 @@ internal abstract class CryptoModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask
|
abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -34,6 +34,16 @@ internal class RoomDecryptorProvider @Inject constructor(
|
|||||||
// A map from algorithm to MXDecrypting instance, for each room
|
// A map from algorithm to MXDecrypting instance, for each room
|
||||||
private val roomDecryptors: MutableMap<String /* room id */, MutableMap<String /* algorithm */, IMXDecrypting>> = HashMap()
|
private val roomDecryptors: MutableMap<String /* room id */, MutableMap<String /* algorithm */, IMXDecrypting>> = HashMap()
|
||||||
|
|
||||||
|
private val newSessionListeners = ArrayList<NewSessionListener>()
|
||||||
|
|
||||||
|
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.
|
* Get a decryptor for a given room and algorithm.
|
||||||
* If we already have a decryptor for the given room and algorithm, return
|
* 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)
|
val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm)
|
||||||
if (decryptingClass) {
|
if (decryptingClass) {
|
||||||
val alg = when (algorithm) {
|
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()
|
else -> olmDecryptionFactory.create()
|
||||||
}
|
}
|
||||||
if (roomId != null && !TextUtils.isEmpty(roomId)) {
|
if (roomId != null && !TextUtils.isEmpty(roomId)) {
|
||||||
|
@ -55,6 +55,8 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
|||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers)
|
private val coroutineDispatchers: MatrixCoroutineDispatchers)
|
||||||
: IMXDecrypting {
|
: IMXDecrypting {
|
||||||
|
|
||||||
|
var newSessionListener: NewSessionListener? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||||
* senderKey|sessionId to timelines to list of MatrixEvents.
|
* 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) {
|
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||||
Timber.v("## onRoomKeyEvent(), forward adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId
|
Timber.v("## onRoomKeyEvent(), forward adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId
|
||||||
+ " sessionKey " + roomKeyContent.sessionKey) // from " + event);
|
+ " sessionKey " + roomKeyContent.sessionKey) // from " + event);
|
||||||
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>() ?: return
|
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||||
|
?: return
|
||||||
forwardingCurve25519KeyChain = if (forwardedRoomKeyContent.forwardingCurve25519KeyChain == null) {
|
forwardingCurve25519KeyChain = if (forwardedRoomKeyContent.forwardingCurve25519KeyChain == null) {
|
||||||
ArrayList()
|
ArrayList()
|
||||||
} else {
|
} else {
|
||||||
@ -275,43 +278,8 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
|||||||
* @param sessionId the session id
|
* @param sessionId the session id
|
||||||
*/
|
*/
|
||||||
override fun onNewSession(senderKey: String, sessionId: String) {
|
override fun onNewSession(senderKey: String, sessionId: String) {
|
||||||
//TODO see how to handle this
|
|
||||||
Timber.v("ON NEW SESSION $sessionId - $senderKey")
|
Timber.v("ON NEW SESSION $sessionId - $senderKey")
|
||||||
/*val k = "$senderKey|$sessionId"
|
newSessionListener?.onNewSession(null, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean {
|
override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean {
|
||||||
|
@ -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
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
package im.vector.matrix.android.internal.crypto.attachments
|
||||||
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
@ -42,7 +42,7 @@ object MXEncryptedAttachments {
|
|||||||
*/
|
*/
|
||||||
data class EncryptionResult(
|
data class EncryptionResult(
|
||||||
var encryptedFileInfo: EncryptedFileInfo,
|
var encryptedFileInfo: EncryptedFileInfo,
|
||||||
var encryptedStream: InputStream
|
var encryptedByteArray: ByteArray
|
||||||
)
|
)
|
||||||
|
|
||||||
/***
|
/***
|
||||||
@ -112,7 +112,7 @@ object MXEncryptedAttachments {
|
|||||||
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))!!),
|
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))!!),
|
||||||
v = "v2"
|
v = "v2"
|
||||||
),
|
),
|
||||||
encryptedStream = ByteArrayInputStream(outStream.toByteArray())
|
encryptedByteArray = outStream.toByteArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
outStream.close()
|
outStream.close()
|
||||||
@ -142,43 +142,37 @@ object MXEncryptedAttachments {
|
|||||||
* @return the decrypted attachment stream
|
* @return the decrypted attachment stream
|
||||||
*/
|
*/
|
||||||
fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? {
|
fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? {
|
||||||
|
if (encryptedFileInfo?.isValid() != true) {
|
||||||
|
Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val elementToDecrypt = encryptedFileInfo.toElementToDecrypt()
|
||||||
|
|
||||||
|
return decryptAttachment(attachmentStream, elementToDecrypt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt an attachment
|
||||||
|
*
|
||||||
|
* @param attachmentStream the attachment stream
|
||||||
|
* @param elementToDecrypt the elementToDecrypt info
|
||||||
|
* @return the decrypted attachment stream
|
||||||
|
*/
|
||||||
|
fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? {
|
||||||
// sanity checks
|
// sanity checks
|
||||||
if (null == attachmentStream || null == encryptedFileInfo) {
|
if (null == attachmentStream || elementToDecrypt == null) {
|
||||||
Timber.e("## decryptAttachment() : null parameters")
|
Timber.e("## decryptAttachment() : null stream")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(encryptedFileInfo.iv)
|
|
||||||
|| null == encryptedFileInfo.key
|
|
||||||
|| null == encryptedFileInfo.hashes
|
|
||||||
|| !encryptedFileInfo.hashes.containsKey("sha256")) {
|
|
||||||
Timber.e("## decryptAttachment() : some fields are not defined")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TextUtils.equals(encryptedFileInfo.key!!.alg, "A256CTR")
|
|
||||||
|| !TextUtils.equals(encryptedFileInfo.key!!.kty, "oct")
|
|
||||||
|| TextUtils.isEmpty(encryptedFileInfo.key!!.k)) {
|
|
||||||
Timber.e("## decryptAttachment() : invalid key fields")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// detect if there is no data to decrypt
|
|
||||||
try {
|
|
||||||
if (0 == attachmentStream.available()) {
|
|
||||||
return ByteArrayInputStream(ByteArray(0))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Fail to retrieve the file size")
|
|
||||||
}
|
|
||||||
|
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
|
|
||||||
val outStream = ByteArrayOutputStream()
|
val outStream = ByteArrayOutputStream()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val key = Base64.decode(base64UrlToBase64(encryptedFileInfo.key!!.k), Base64.DEFAULT)
|
val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
|
||||||
val initVectorBytes = Base64.decode(encryptedFileInfo.iv, Base64.DEFAULT)
|
val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)
|
||||||
|
|
||||||
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
|
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
|
||||||
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
|
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
|
||||||
@ -205,7 +199,7 @@ object MXEncryptedAttachments {
|
|||||||
|
|
||||||
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
|
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
|
||||||
|
|
||||||
if (!TextUtils.equals(encryptedFileInfo.hashes["sha256"], currentDigestValue)) {
|
if (!TextUtils.equals(elementToDecrypt.sha256, currentDigestValue)) {
|
||||||
Timber.e("## decryptAttachment() : Digest value mismatch")
|
Timber.e("## decryptAttachment() : Digest value mismatch")
|
||||||
outStream.close()
|
outStream.close()
|
||||||
return null
|
return null
|
@ -15,14 +15,75 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.crypto.model.rest
|
package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In Matrix specs: EncryptedFile
|
||||||
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class EncryptedFileInfo(
|
data class EncryptedFileInfo(
|
||||||
|
/**
|
||||||
|
* Required. The URL to the file.
|
||||||
|
*/
|
||||||
|
@Json(name = "url")
|
||||||
var url: String? = null,
|
var url: String? = null,
|
||||||
var mimetype: String,
|
|
||||||
|
/**
|
||||||
|
* Not documented
|
||||||
|
*/
|
||||||
|
@Json(name = "mimetype")
|
||||||
|
var mimetype: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A JSON Web Key object.
|
||||||
|
*/
|
||||||
|
@Json(name = "key")
|
||||||
var key: EncryptedFileKey? = null,
|
var key: EncryptedFileKey? = null,
|
||||||
var iv: String,
|
|
||||||
var hashes: Map<String, String>,
|
/**
|
||||||
|
* Required. The Initialisation Vector used by AES-CTR, encoded as unpadded base64.
|
||||||
|
*/
|
||||||
|
@Json(name = "iv")
|
||||||
|
var iv: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64.
|
||||||
|
* Clients should support the SHA-256 hash, which uses the key "sha256".
|
||||||
|
*/
|
||||||
|
@Json(name = "hashes")
|
||||||
|
var hashes: Map<String, String>? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. Version of the encrypted attachments protocol. Must be "v2".
|
||||||
|
*/
|
||||||
|
@Json(name = "v")
|
||||||
var v: String? = null
|
var v: String? = null
|
||||||
)
|
) {
|
||||||
|
/**
|
||||||
|
* Check what the spec tells us
|
||||||
|
*/
|
||||||
|
fun isValid(): Boolean {
|
||||||
|
if (url.isNullOrBlank()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key?.isValid() != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iv.isNullOrBlank()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashes?.containsKey("sha256") != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v != "v2") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,14 +15,66 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.crypto.model.rest
|
package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class EncryptedFileKey(
|
data class EncryptedFileKey(
|
||||||
var alg: String,
|
/**
|
||||||
var ext: Boolean? = null,
|
* Required. Algorithm. Must be "A256CTR".
|
||||||
var key_ops: List<String>,
|
*/
|
||||||
var kty: String,
|
@Json(name = "alg")
|
||||||
var k: String
|
var alg: String? = null,
|
||||||
)
|
|
||||||
|
/**
|
||||||
|
* Required. Extractable. Must be true. This is a W3C extension.
|
||||||
|
*/
|
||||||
|
@Json(name = "ext")
|
||||||
|
var ext: Boolean? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. Key operations. Must at least contain "encrypt" and "decrypt".
|
||||||
|
*/
|
||||||
|
@Json(name = "key_ops")
|
||||||
|
var key_ops: List<String>? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. Key type. Must be "oct".
|
||||||
|
*/
|
||||||
|
@Json(name = "kty")
|
||||||
|
var kty: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The key, encoded as urlsafe unpadded base64.
|
||||||
|
*/
|
||||||
|
@Json(name = "k")
|
||||||
|
var k: String? = null
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Check what the spec tells us
|
||||||
|
*/
|
||||||
|
fun isValid(): Boolean {
|
||||||
|
if (alg != "A256CTR") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ext != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_ops?.contains("encrypt") != true || key_ops?.contains("decrypt") != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kty != "oct") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (k.isNullOrBlank()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
package im.vector.matrix.android.internal.crypto.store
|
package im.vector.matrix.android.internal.crypto.store
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
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.OutgoingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||||
@ -376,4 +377,8 @@ internal interface IMXCryptoStore {
|
|||||||
* @return an IncomingRoomKeyRequest if it exists, else null
|
* @return an IncomingRoomKeyRequest if it exists, else null
|
||||||
*/
|
*/
|
||||||
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
|
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
|
||||||
|
|
||||||
|
fun addNewSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
|
fun removeSessionListener(listener: NewSessionListener)
|
||||||
}
|
}
|
||||||
|
@ -25,27 +25,9 @@ import java.io.ByteArrayInputStream
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.ObjectInputStream
|
import java.io.ObjectInputStream
|
||||||
import java.io.ObjectOutputStream
|
import java.io.ObjectOutputStream
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.zip.GZIPInputStream
|
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
|
* Get realm, invoke the action, close realm, and return the result of the action
|
||||||
*/
|
*/
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.store.db
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
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.OutgoingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
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
|
// Cache for InboundGroupSession, to release them properly
|
||||||
private val inboundGroupSessionToRelease = HashMap<String, OlmInboundGroupSessionWrapper>()
|
private val inboundGroupSessionToRelease = HashMap<String, OlmInboundGroupSessionWrapper>()
|
||||||
|
|
||||||
|
|
||||||
|
private val newSessionListeners = ArrayList<NewSessionListener>()
|
||||||
|
|
||||||
|
override fun addNewSessionListener(listener: NewSessionListener) {
|
||||||
|
if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeSessionListener(listener: NewSessionListener) {
|
||||||
|
newSessionListeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* Other data
|
* Other data
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
@ -718,4 +730,5 @@ internal class RealmCryptoStore(private val enableFileEncryption: Boolean = fals
|
|||||||
}
|
}
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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 <M, the KeyStore cannot create AES keys, so a public/private key pair is generated,
|
||||||
|
* then we generate a random secret key. The database key is encrypted with the secret key; The secret
|
||||||
|
* key is encrypted with the public RSA key and stored with the encrypted key in the shared pref
|
||||||
|
*/
|
||||||
|
private object RealmKeysUtils {
|
||||||
|
|
||||||
|
private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY"
|
||||||
|
|
||||||
|
private val rng = SecureRandom()
|
||||||
|
|
||||||
|
private fun generateKeyForRealm(): ByteArray {
|
||||||
|
val keyForRealm = ByteArray(RealmConfiguration.KEY_LENGTH)
|
||||||
|
rng.nextBytes(keyForRealm)
|
||||||
|
return keyForRealm
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there is already a key for this alias
|
||||||
|
*/
|
||||||
|
fun hasKeyForDatabase(alias: String, context: Context): Boolean {
|
||||||
|
val sharedPreferences = getSharedPreferences(context)
|
||||||
|
return sharedPreferences.contains("${ENCRYPTED_KEY_PREFIX}_$alias")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new secure random key for this database.
|
||||||
|
* The random key is then encrypted by the keystore, and the encrypted key is stored
|
||||||
|
* in shared preferences.
|
||||||
|
*
|
||||||
|
* @return the generate key (can be passed to Realm Configuration)
|
||||||
|
*/
|
||||||
|
fun createAndSaveKeyForDatabase(alias: String, context: Context): ByteArray {
|
||||||
|
val key = generateKeyForRealm()
|
||||||
|
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
|
||||||
|
val toStore = SecretStoringUtils.securelyStoreString(encodedKey, alias, context)
|
||||||
|
val sharedPreferences = getSharedPreferences(context)
|
||||||
|
sharedPreferences
|
||||||
|
.edit()
|
||||||
|
.putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
|
||||||
|
.apply()
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the key for this database
|
||||||
|
* throws if something goes wrong
|
||||||
|
*/
|
||||||
|
fun extractKeyForDatabase(alias: String, context: Context): ByteArray {
|
||||||
|
val sharedPreferences = getSharedPreferences(context)
|
||||||
|
val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
|
||||||
|
val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
|
||||||
|
val b64 = SecretStoringUtils.loadSecureSecret(encryptedKey, alias, context)
|
||||||
|
return Base64.decode(b64!!, Base64.NO_PADDING)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSharedPreferences(context: Context) =
|
||||||
|
context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun RealmConfiguration.Builder.configureEncryption(alias: String, context: Context): RealmConfiguration.Builder {
|
||||||
|
if (RealmKeysUtils.hasKeyForDatabase(alias, context)) {
|
||||||
|
Timber.i("Found key for alias:$alias")
|
||||||
|
RealmKeysUtils.extractKeyForDatabase(alias, context).also {
|
||||||
|
this.encryptionKey(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.i("Create key for DB alias:$alias")
|
||||||
|
RealmKeysUtils.createAndSaveKeyForDatabase(alias, context).also {
|
||||||
|
this.encryptionKey(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
@ -49,6 +49,7 @@ internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
|
|||||||
val eventEntity = event.toEntity(roomId).apply {
|
val eventEntity = event.toEntity(roomId).apply {
|
||||||
this.stateIndex = stateIndex
|
this.stateIndex = stateIndex
|
||||||
this.isUnlinked = isUnlinked
|
this.isUnlinked = isUnlinked
|
||||||
|
this.sendState = SendState.SYNCED
|
||||||
}
|
}
|
||||||
untimelinedStateEvents.add(0, eventEntity)
|
untimelinedStateEvents.add(0, eventEntity)
|
||||||
}
|
}
|
||||||
|
@ -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.auth.data.SessionParams
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.internal.database.LiveEntityObserver
|
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.database.model.SessionRealmModule
|
||||||
import im.vector.matrix.android.internal.di.Authenticated
|
import im.vector.matrix.android.internal.di.Authenticated
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
@ -74,6 +75,7 @@ internal abstract class SessionModule {
|
|||||||
return RealmConfiguration.Builder()
|
return RealmConfiguration.Builder()
|
||||||
.directory(directory)
|
.directory(directory)
|
||||||
.name("disk_store.realm")
|
.name("disk_store.realm")
|
||||||
|
.configureEncryption("session_db_$childPath", context)
|
||||||
.modules(SessionRealmModule())
|
.modules(SessionRealmModule())
|
||||||
.deleteRealmIfMigrationNeeded()
|
.deleteRealmIfMigrationNeeded()
|
||||||
.build()
|
.build()
|
||||||
|
@ -54,7 +54,6 @@ internal class FileUploader @Inject constructor(@Authenticated
|
|||||||
|
|
||||||
val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray)
|
val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray)
|
||||||
return upload(uploadBody, filename, progressListener)
|
return upload(uploadBody, filename, progressListener)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,13 +25,17 @@ import im.vector.matrix.android.api.session.events.model.Event
|
|||||||
import im.vector.matrix.android.api.session.events.model.toContent
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
|
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendEventWorker
|
import im.vector.matrix.android.internal.session.room.send.SendEventWorker
|
||||||
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@ -42,7 +46,9 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
override val userId: String,
|
override val userId: String,
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val event: Event,
|
val event: Event,
|
||||||
val attachment: ContentAttachmentData
|
val attachment: ContentAttachmentData,
|
||||||
|
val isRoomEncrypted: Boolean,
|
||||||
|
override var lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var fileUploader: FileUploader
|
@Inject lateinit var fileUploader: FileUploader
|
||||||
@ -52,19 +58,41 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success()
|
?: return Result.success()
|
||||||
|
|
||||||
|
if (params.lastFailureMessage != null) {
|
||||||
|
// Transmit the error
|
||||||
|
return Result.success(inputData)
|
||||||
|
}
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
val eventId = params.event.eventId ?: return Result.success()
|
val eventId = params.event.eventId ?: return Result.success()
|
||||||
val attachment = params.attachment
|
val attachment = params.attachment
|
||||||
|
|
||||||
|
val isRoomEncrypted = params.isRoomEncrypted
|
||||||
|
|
||||||
|
|
||||||
val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment)
|
val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment)
|
||||||
val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure()
|
val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure()
|
||||||
var uploadedThumbnailUrl: String? = null
|
var uploadedThumbnailUrl: String? = null
|
||||||
|
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
if (thumbnailData != null) {
|
if (thumbnailData != null) {
|
||||||
fileUploader
|
val contentUploadResponse = if (isRoomEncrypted) {
|
||||||
.uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType)
|
Timber.v("Encrypt thumbnail")
|
||||||
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
|
||||||
|
?: return Result.failure()
|
||||||
|
|
||||||
|
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
|
||||||
|
fileUploader
|
||||||
|
.uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${attachment.name}", thumbnailData.mimeType)
|
||||||
|
} else {
|
||||||
|
fileUploader
|
||||||
|
.uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentUploadResponse
|
||||||
.fold(
|
.fold(
|
||||||
{ Timber.e(it) },
|
{ Timber.e(it) },
|
||||||
{ uploadedThumbnailUrl = it.contentUri }
|
{ uploadedThumbnailUrl = it.contentUri }
|
||||||
@ -76,11 +104,28 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
contentUploadStateTracker.setProgress(eventId, current, total)
|
contentUploadStateTracker.setProgress(eventId, current, total)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fileUploader
|
|
||||||
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
|
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
|
||||||
|
|
||||||
|
val contentUploadResponse = if (isRoomEncrypted) {
|
||||||
|
Timber.v("Encrypt file")
|
||||||
|
|
||||||
|
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
|
||||||
|
?: return Result.failure()
|
||||||
|
|
||||||
|
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
|
||||||
|
|
||||||
|
fileUploader
|
||||||
|
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
|
||||||
|
} else {
|
||||||
|
fileUploader
|
||||||
|
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentUploadResponse
|
||||||
.fold(
|
.fold(
|
||||||
{ handleFailure(params) },
|
{ handleFailure(params, it) },
|
||||||
{ handleSuccess(params, it.contentUri, uploadedThumbnailUrl) }
|
{ handleSuccess(params, it.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,46 +138,79 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFailure(params: Params): Result {
|
private fun handleFailure(params: Params, failure: Throwable): Result {
|
||||||
contentUploadStateTracker.setFailure(params.event.eventId!!)
|
contentUploadStateTracker.setFailure(params.event.eventId!!)
|
||||||
return Result.success()
|
return Result.success(
|
||||||
|
WorkerParamsFactory.toData(
|
||||||
|
params.copy(
|
||||||
|
lastFailureMessage = failure.localizedMessage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess(params: Params,
|
private fun handleSuccess(params: Params,
|
||||||
attachmentUrl: String,
|
attachmentUrl: String,
|
||||||
thumbnailUrl: String?): Result {
|
encryptedFileInfo: EncryptedFileInfo?,
|
||||||
|
thumbnailUrl: String?,
|
||||||
|
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result {
|
||||||
contentUploadStateTracker.setSuccess(params.event.eventId!!)
|
contentUploadStateTracker.setSuccess(params.event.eventId!!)
|
||||||
val event = updateEvent(params.event, attachmentUrl, thumbnailUrl)
|
val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
||||||
val sendParams = SendEventWorker.Params(params.userId, params.roomId, event)
|
val sendParams = SendEventWorker.Params(params.userId, params.roomId, event)
|
||||||
return Result.success(WorkerParamsFactory.toData(sendParams))
|
return Result.success(WorkerParamsFactory.toData(sendParams))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateEvent(event: Event, url: String, thumbnailUrl: String? = null): Event {
|
private fun updateEvent(event: Event,
|
||||||
|
url: String,
|
||||||
|
encryptedFileInfo: EncryptedFileInfo?,
|
||||||
|
thumbnailUrl: String? = null,
|
||||||
|
thumbnailEncryptedFileInfo: EncryptedFileInfo?): Event {
|
||||||
val messageContent: MessageContent = event.content.toModel() ?: return event
|
val messageContent: MessageContent = event.content.toModel() ?: return event
|
||||||
val updatedContent = when (messageContent) {
|
val updatedContent = when (messageContent) {
|
||||||
is MessageImageContent -> messageContent.update(url)
|
is MessageImageContent -> messageContent.update(url, encryptedFileInfo)
|
||||||
is MessageVideoContent -> messageContent.update(url, thumbnailUrl)
|
is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
|
||||||
is MessageFileContent -> messageContent.update(url)
|
is MessageFileContent -> messageContent.update(url, encryptedFileInfo)
|
||||||
is MessageAudioContent -> messageContent.update(url)
|
is MessageAudioContent -> messageContent.update(url, encryptedFileInfo)
|
||||||
else -> messageContent
|
else -> messageContent
|
||||||
}
|
}
|
||||||
return event.copy(content = updatedContent.toContent())
|
return event.copy(content = updatedContent.toContent())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageImageContent.update(url: String): MessageImageContent {
|
private fun MessageImageContent.update(url: String,
|
||||||
return copy(url = url)
|
encryptedFileInfo: EncryptedFileInfo?): MessageImageContent {
|
||||||
|
return copy(
|
||||||
|
url = if (encryptedFileInfo == null) url else null,
|
||||||
|
encryptedFileInfo = encryptedFileInfo?.copy(url = url)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageVideoContent.update(url: String, thumbnailUrl: String?): MessageVideoContent {
|
private fun MessageVideoContent.update(url: String,
|
||||||
return copy(url = url, info = info?.copy(thumbnailUrl = thumbnailUrl))
|
encryptedFileInfo: EncryptedFileInfo?,
|
||||||
|
thumbnailUrl: String?,
|
||||||
|
thumbnailEncryptedFileInfo: EncryptedFileInfo?): MessageVideoContent {
|
||||||
|
return copy(
|
||||||
|
url = if (encryptedFileInfo == null) url else null,
|
||||||
|
videoInfo = videoInfo?.copy(
|
||||||
|
thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null,
|
||||||
|
thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = url)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageFileContent.update(url: String): MessageFileContent {
|
private fun MessageFileContent.update(url: String,
|
||||||
return copy(url = url)
|
encryptedFileInfo: EncryptedFileInfo?): MessageFileContent {
|
||||||
|
return copy(
|
||||||
|
url = if (encryptedFileInfo == null) url else null,
|
||||||
|
encryptedFileInfo = encryptedFileInfo?.copy(url = url)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageAudioContent.update(url: String): MessageAudioContent {
|
private fun MessageAudioContent.update(url: String,
|
||||||
return copy(url = url)
|
encryptedFileInfo: EncryptedFileInfo?): MessageAudioContent {
|
||||||
|
return copy(
|
||||||
|
url = if (encryptedFileInfo == null) url else null,
|
||||||
|
encryptedFileInfo = encryptedFileInfo?.copy(url = url)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,28 +20,26 @@ import android.content.Context
|
|||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.squareup.inject.assisted.Assisted
|
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.internal.worker.DelegateWorkerFactory
|
|
||||||
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class GetGroupDataWorker (context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
override val userId: String,
|
override val userId: String,
|
||||||
val groupIds: List<String>
|
val groupIds: List<String>,
|
||||||
): SessionWorkerParams
|
override var lastFailureMessage: String? = null
|
||||||
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var getGroupDataTask: GetGroupDataTask
|
@Inject lateinit var getGroupDataTask: GetGroupDataTask
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
@ -64,7 +64,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
|
|||||||
|
|
||||||
fun create(roomId: String): Room {
|
fun create(roomId: String): Room {
|
||||||
val timelineEventFactory = InMemoryTimelineEventFactory(SenderRoomMemberExtractor(), EventRelationExtractor(), cryptoService)
|
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 sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
|
||||||
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
|
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
|
||||||
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
||||||
|
@ -39,6 +39,7 @@ internal class DefaultReadService @Inject constructor(private val roomId: String
|
|||||||
private val credentials: Credentials) : ReadService {
|
private val credentials: Credentials) : ReadService {
|
||||||
|
|
||||||
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
|
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
|
||||||
|
//TODO shouldn't it be latest synced event?
|
||||||
val latestEvent = getLatestEvent()
|
val latestEvent = getLatestEvent()
|
||||||
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId)
|
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId)
|
||||||
setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor)
|
setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor)
|
||||||
|
@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.read
|
|||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
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.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
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.latestEvent
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
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.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.task.Task
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {
|
internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {
|
||||||
@ -48,21 +48,28 @@ private const val READ_MARKER = "m.fully_read"
|
|||||||
private const val READ_RECEIPT = "m.read"
|
private const val READ_RECEIPT = "m.read"
|
||||||
|
|
||||||
internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI,
|
internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI,
|
||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val monarchy: Monarchy
|
private val monarchy: Monarchy
|
||||||
) : SetReadMarkersTask {
|
) : SetReadMarkersTask {
|
||||||
|
|
||||||
override suspend fun execute(params: SetReadMarkersTask.Params): Try<Unit> {
|
override suspend fun execute(params: SetReadMarkersTask.Params): Try<Unit> {
|
||||||
val markers = HashMap<String, String>()
|
val markers = HashMap<String, String>()
|
||||||
if (params.fullyReadEventId != null && MatrixPatterns.isEventId(params.fullyReadEventId)) {
|
if (params.fullyReadEventId != null) {
|
||||||
markers[READ_MARKER] = params.fullyReadEventId
|
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
|
if (params.readReceiptEventId != null
|
||||||
&& MatrixPatterns.isEventId(params.readReceiptEventId)
|
|
||||||
&& !isEventRead(params.roomId, params.readReceiptEventId)) {
|
&& !isEventRead(params.roomId, params.readReceiptEventId)) {
|
||||||
|
|
||||||
updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId)
|
if (LocalEchoEventFactory.isLocalEchoId(params.readReceiptEventId)) {
|
||||||
markers[READ_RECEIPT] = 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()) {
|
return if (markers.isEmpty()) {
|
||||||
Try.just(Unit)
|
Try.just(Unit)
|
||||||
|
@ -31,10 +31,8 @@ import im.vector.matrix.android.api.util.Cancelable
|
|||||||
import im.vector.matrix.android.internal.database.RealmLiveData
|
import im.vector.matrix.android.internal.database.RealmLiveData
|
||||||
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
|
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
|
||||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||||
@ -107,9 +105,12 @@ internal class DefaultRelationService @Inject constructor(private val context: C
|
|||||||
|
|
||||||
//TODO duplicate with send service?
|
//TODO duplicate with send service?
|
||||||
private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest {
|
private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest {
|
||||||
|
val sendContentWorkerParams = RedactEventWorker.Params(
|
||||||
val sendContentWorkerParams = RedactEventWorker.Params(credentials.userId, localEvent.eventId!!,
|
credentials.userId,
|
||||||
roomId, eventId, reason)
|
localEvent.eventId!!,
|
||||||
|
roomId,
|
||||||
|
eventId,
|
||||||
|
reason)
|
||||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,8 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
|
|||||||
override val userId: String,
|
override val userId: String,
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val event: Event,
|
val event: Event,
|
||||||
val relationType: String? = null
|
val relationType: String? = null,
|
||||||
|
override var lastFailureMessage: String?
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var roomAPI: RoomAPI
|
@Inject lateinit var roomAPI: RoomAPI
|
||||||
@ -48,6 +49,11 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
|
|||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
|
||||||
|
if (params.lastFailureMessage != null) {
|
||||||
|
// Transmit the error
|
||||||
|
return Result.success(inputData)
|
||||||
|
}
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
|
@ -101,13 +101,26 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
|
|||||||
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
val uploadWork = createUploadMediaWork(event, attachment)
|
|
||||||
|
val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)
|
||||||
|
|
||||||
|
val uploadWork = createUploadMediaWork(event, attachment, isRoomEncrypted)
|
||||||
val sendWork = createSendEventWork(event)
|
val sendWork = createSendEventWork(event)
|
||||||
|
|
||||||
WorkManager.getInstance(context)
|
if (isRoomEncrypted) {
|
||||||
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
val encryptWork = createEncryptEventWork(event)
|
||||||
.then(sendWork)
|
|
||||||
.enqueue()
|
WorkManager.getInstance(context)
|
||||||
|
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||||
|
.then(encryptWork)
|
||||||
|
.then(sendWork)
|
||||||
|
.enqueue()
|
||||||
|
} else {
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||||
|
.then(sendWork)
|
||||||
|
.enqueue()
|
||||||
|
}
|
||||||
|
|
||||||
return CancelableWork(context, sendWork.id)
|
return CancelableWork(context, sendWork.id)
|
||||||
}
|
}
|
||||||
@ -148,8 +161,8 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
|
|||||||
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {
|
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData, isRoomEncrypted: Boolean): OneTimeWorkRequest {
|
||||||
val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment)
|
val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment, isRoomEncrypted)
|
||||||
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
||||||
|
|
||||||
return OneTimeWorkRequestBuilder<UploadContentWorker>()
|
return OneTimeWorkRequestBuilder<UploadContentWorker>()
|
||||||
|
@ -41,7 +41,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||||||
val roomId: String,
|
val roomId: String,
|
||||||
val event: Event,
|
val event: Event,
|
||||||
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
|
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
|
||||||
val keepKeys: List<String>? = null
|
val keepKeys: List<String>? = null,
|
||||||
|
override var lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var crypto: CryptoService
|
@Inject lateinit var crypto: CryptoService
|
||||||
@ -52,6 +53,11 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success()
|
?: return Result.success()
|
||||||
|
|
||||||
|
if (params.lastFailureMessage != null) {
|
||||||
|
// Transmit the error
|
||||||
|
return Result.success(inputData)
|
||||||
|
}
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
@ -105,16 +111,15 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||||||
)
|
)
|
||||||
val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, encryptedEvent)
|
val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, encryptedEvent)
|
||||||
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||||
|
} else {
|
||||||
|
val sendState = when (error) {
|
||||||
|
is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
|
||||||
|
else -> SendState.UNDELIVERED
|
||||||
|
}
|
||||||
|
localEchoUpdater.updateSendState(localEvent.eventId, sendState)
|
||||||
|
//always return success, or the chain will be stuck for ever!
|
||||||
|
val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, localEvent, error?.localizedMessage ?: "Error")
|
||||||
|
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||||
}
|
}
|
||||||
|
|
||||||
val safeError = error
|
|
||||||
val sendState = when (safeError) {
|
|
||||||
is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
|
|
||||||
else -> SendState.UNDELIVERED
|
|
||||||
}
|
|
||||||
localEchoUpdater.updateSendState(localEvent.eventId, sendState)
|
|
||||||
//always return success, or the chain will be stuck for ever!
|
|
||||||
return Result.success()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.RelationDefaultContent
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
|
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.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.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
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?) =
|
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
|
||||||
text != htmlText && htmlText != "<p>$text</p>\n"
|
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
|
||||||
|
|
||||||
fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event {
|
fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event {
|
||||||
val content = MessageTextContent(
|
val content = MessageTextContent(
|
||||||
@ -179,7 +177,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
|||||||
val content = MessageVideoContent(
|
val content = MessageVideoContent(
|
||||||
type = MessageType.MSGTYPE_VIDEO,
|
type = MessageType.MSGTYPE_VIDEO,
|
||||||
body = attachment.name ?: "video",
|
body = attachment.name ?: "video",
|
||||||
info = VideoInfo(
|
videoInfo = VideoInfo(
|
||||||
mimeType = attachment.mimeType,
|
mimeType = attachment.mimeType,
|
||||||
width = width,
|
width = width,
|
||||||
height = height,
|
height = height,
|
||||||
@ -198,7 +196,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
|||||||
val content = MessageAudioContent(
|
val content = MessageAudioContent(
|
||||||
type = MessageType.MSGTYPE_AUDIO,
|
type = MessageType.MSGTYPE_AUDIO,
|
||||||
body = attachment.name ?: "audio",
|
body = attachment.name ?: "audio",
|
||||||
info = AudioInfo(
|
audioInfo = AudioInfo(
|
||||||
mimeType = attachment.mimeType ?: "audio/mpeg",
|
mimeType = attachment.mimeType ?: "audio/mpeg",
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
@ -238,7 +236,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun dummyEventId(roomId: String): String {
|
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? {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,8 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
|
|||||||
val txID: String,
|
val txID: String,
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val reason: String?
|
val reason: String?,
|
||||||
|
override var lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var roomAPI: RoomAPI
|
@Inject lateinit var roomAPI: RoomAPI
|
||||||
@ -44,6 +45,11 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
|
|||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
|
||||||
|
if (params.lastFailureMessage != null) {
|
||||||
|
// Transmit the error
|
||||||
|
return Result.success(inputData)
|
||||||
|
}
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.userId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
|
||||||
@ -62,7 +68,9 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
|
|||||||
else -> {
|
else -> {
|
||||||
//TODO mark as failed to send?
|
//TODO mark as failed to send?
|
||||||
//always return success, or the chain will be stuck for ever!
|
//always return success, or the chain will be stuck for ever!
|
||||||
Result.success()
|
Result.success(WorkerParamsFactory.toData(params.copy(
|
||||||
|
lastFailureMessage = it.localizedMessage
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
@ -38,14 +38,14 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
|
|||||||
internal data class Params(
|
internal data class Params(
|
||||||
override val userId: String,
|
override val userId: String,
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val event: Event
|
val event: Event,
|
||||||
|
override var lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@Inject lateinit var localEchoUpdater: LocalEchoUpdater
|
@Inject lateinit var localEchoUpdater: LocalEchoUpdater
|
||||||
@Inject lateinit var roomAPI: RoomAPI
|
@Inject lateinit var roomAPI: RoomAPI
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
|
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success()
|
?: return Result.success()
|
||||||
|
|
||||||
@ -57,6 +57,13 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
|
|||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.lastFailureMessage != null) {
|
||||||
|
localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED)
|
||||||
|
|
||||||
|
// Transmit the error
|
||||||
|
return Result.success(inputData)
|
||||||
|
}
|
||||||
|
|
||||||
localEchoUpdater.updateSendState(event.eventId, SendState.SENDING)
|
localEchoUpdater.updateSendState(event.eventId, SendState.SENDING)
|
||||||
val result = executeRequest<SendResponse> {
|
val result = executeRequest<SendResponse> {
|
||||||
apiCall = roomAPI.send(
|
apiCall = roomAPI.send(
|
||||||
|
@ -20,11 +20,15 @@ import android.os.Handler
|
|||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
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.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.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.util.CancelableBag
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
import im.vector.matrix.android.api.util.addTo
|
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.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.*
|
import im.vector.matrix.android.internal.database.model.*
|
||||||
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
||||||
@ -56,6 +60,7 @@ internal class DefaultTimeline(
|
|||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val timelineEventFactory: CacheableTimelineEventFactory,
|
private val timelineEventFactory: CacheableTimelineEventFactory,
|
||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask,
|
||||||
|
private val cryptoService: CryptoService,
|
||||||
private val allowedTypes: List<String>?
|
private val allowedTypes: List<String>?
|
||||||
) : Timeline {
|
) : Timeline {
|
||||||
|
|
||||||
@ -159,6 +164,33 @@ internal class DefaultTimeline(
|
|||||||
postSnapshot()
|
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<EncryptedEventContent>()
|
||||||
|
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 ******************************************************************************
|
// Public methods ******************************************************************************
|
||||||
|
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
@ -184,6 +216,7 @@ internal class DefaultTimeline(
|
|||||||
val handler = Handler(handlerThread.looper)
|
val handler = Handler(handlerThread.looper)
|
||||||
this.backgroundHandlerThread.set(handlerThread)
|
this.backgroundHandlerThread.set(handlerThread)
|
||||||
this.backgroundHandler.set(handler)
|
this.backgroundHandler.set(handler)
|
||||||
|
cryptoService.addNewSessionListener(newSessionListener)
|
||||||
handler.post {
|
handler.post {
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
val realm = Realm.getInstance(realmConfiguration)
|
||||||
backgroundRealm.set(realm)
|
backgroundRealm.set(realm)
|
||||||
@ -211,6 +244,7 @@ internal class DefaultTimeline(
|
|||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
if (isStarted.compareAndSet(true, false)) {
|
if (isStarted.compareAndSet(true, false)) {
|
||||||
|
cryptoService.removeSessionListener(newSessionListener)
|
||||||
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
||||||
backgroundHandler.get()?.post {
|
backgroundHandler.get()?.post {
|
||||||
cancelableBag.cancel()
|
cancelableBag.cancel()
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.timeline
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import com.zhuinden.monarchy.Monarchy
|
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.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
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 taskExecutor: TaskExecutor,
|
||||||
private val timelineEventFactory: CacheableTimelineEventFactory,
|
private val timelineEventFactory: CacheableTimelineEventFactory,
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
|
private val cryptoService: CryptoService,
|
||||||
private val paginationTask: PaginationTask
|
private val paginationTask: PaginationTask
|
||||||
) : TimelineService {
|
) : TimelineService {
|
||||||
|
|
||||||
override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {
|
override fun createTimeline(eventId: String?, allowedTypes: List<String>?): 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? {
|
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
|
||||||
|
@ -69,6 +69,7 @@ internal class SimpleTimelineEventFactory @Inject constructor(private val roomMe
|
|||||||
isUniqueDisplayName,
|
isUniqueDisplayName,
|
||||||
senderRoomMember?.avatarUrl,
|
senderRoomMember?.avatarUrl,
|
||||||
eventEntity.sendState,
|
eventEntity.sendState,
|
||||||
|
event.mClearEvent != null,
|
||||||
relations
|
relations
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -107,7 +108,7 @@ internal class InMemoryTimelineEventFactory @Inject constructor(private val room
|
|||||||
senderRoomMember?.avatarUrl)
|
senderRoomMember?.avatarUrl)
|
||||||
}
|
}
|
||||||
val event = eventEntity.asDomain()
|
val event = eventEntity.asDomain()
|
||||||
if (event.getClearType() == EventType.ENCRYPTED) {
|
if (event.getClearType() == EventType.ENCRYPTED && !event.isRedacted()) {
|
||||||
handleEncryptedEvent(event, eventEntity.localId)
|
handleEncryptedEvent(event, eventEntity.localId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +121,7 @@ internal class InMemoryTimelineEventFactory @Inject constructor(private val room
|
|||||||
senderData.isUniqueDisplayName,
|
senderData.isUniqueDisplayName,
|
||||||
senderData.senderAvatar,
|
senderData.senderAvatar,
|
||||||
eventEntity.sendState,
|
eventEntity.sendState,
|
||||||
|
event.mClearEvent != null,
|
||||||
relations
|
relations
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -138,9 +140,12 @@ internal class InMemoryTimelineEventFactory @Inject constructor(private val room
|
|||||||
}
|
}
|
||||||
event.setClearData(result)
|
event.setClearData(result)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "Encrypted event: decryption failed")
|
Timber.e("Encrypted event: decryption failed ${failure.localizedMessage}")
|
||||||
if (failure is MXDecryptionException) {
|
if (failure is MXDecryptionException) {
|
||||||
event.setCryptoError(failure.cryptoError)
|
event.setCryptoError(failure.cryptoError)
|
||||||
|
} else {
|
||||||
|
// Other error
|
||||||
|
Timber.e("Other error, should be handled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,9 @@ package im.vector.matrix.android.internal.util
|
|||||||
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a Hash of a String, using md5 algorithm
|
||||||
|
*/
|
||||||
fun String.md5() = try {
|
fun String.md5() = try {
|
||||||
val digest = MessageDigest.getInstance("md5")
|
val digest = MessageDigest.getInstance("md5")
|
||||||
digest.update(toByteArray())
|
digest.update(toByteArray())
|
||||||
|
@ -18,4 +18,7 @@ package im.vector.matrix.android.internal.worker
|
|||||||
|
|
||||||
interface SessionWorkerParams {
|
interface SessionWorkerParams {
|
||||||
val userId: String
|
val userId: String
|
||||||
|
|
||||||
|
// Null is no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers
|
||||||
|
var lastFailureMessage: String?
|
||||||
}
|
}
|
@ -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("", "", ""))
|
||||||
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotredesign.core.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.Priority
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.Options
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import com.bumptech.glide.load.model.ModelLoader
|
||||||
|
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||||
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||||
|
import com.bumptech.glide.signature.ObjectKey
|
||||||
|
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||||
|
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import com.bumptech.glide.load.engine.Resource as Resource1
|
||||||
|
|
||||||
|
|
||||||
|
class VectorGlideModelLoaderFactory(private val activeSessionHolder: ActiveSessionHolder)
|
||||||
|
: ModelLoaderFactory<ImageContentRenderer.Data, InputStream> {
|
||||||
|
|
||||||
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ImageContentRenderer.Data, InputStream> {
|
||||||
|
return VectorGlideModelLoader(activeSessionHolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun teardown() {
|
||||||
|
// Is there something to do here?
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class VectorGlideModelLoader(private val activeSessionHolder: ActiveSessionHolder)
|
||||||
|
: ModelLoader<ImageContentRenderer.Data, InputStream> {
|
||||||
|
override fun handles(model: ImageContentRenderer.Data): Boolean {
|
||||||
|
// Always handle
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildLoadData(model: ImageContentRenderer.Data, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
|
||||||
|
return ModelLoader.LoadData(ObjectKey(model), VectorGlideDataFetcher(activeSessionHolder, model, width, height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
private val data: ImageContentRenderer.Data,
|
||||||
|
private val width: Int,
|
||||||
|
private val height: Int)
|
||||||
|
: DataFetcher<InputStream> {
|
||||||
|
|
||||||
|
val client = OkHttpClient()
|
||||||
|
|
||||||
|
override fun getDataClass(): Class<InputStream> {
|
||||||
|
return InputStream::class.java
|
||||||
|
}
|
||||||
|
|
||||||
|
private var stream: InputStream? = null
|
||||||
|
|
||||||
|
override fun cleanup() {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDataSource(): DataSource {
|
||||||
|
// ?
|
||||||
|
return DataSource.REMOTE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
if (stream != null) {
|
||||||
|
try {
|
||||||
|
stream?.close() // interrupts decode if any
|
||||||
|
stream = null
|
||||||
|
} catch (ignore: IOException) {
|
||||||
|
Timber.e(ignore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
|
Timber.v("Load data: $data")
|
||||||
|
if (data.isLocalFile()) {
|
||||||
|
val initialFile = File(data.url)
|
||||||
|
callback.onDataReady(FileInputStream(initialFile))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||||
|
val url = contentUrlResolver.resolveFullSize(data.url)
|
||||||
|
?: return
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
val inputStream = response.body()?.byteStream()
|
||||||
|
Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}")
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
callback.onLoadFailed(IOException("Unexpected code $response"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream = if (data.elementToDecrypt != null && data.elementToDecrypt.k.isNotBlank()) {
|
||||||
|
MXEncryptedAttachments.decryptAttachment(inputStream, data.elementToDecrypt)
|
||||||
|
} else {
|
||||||
|
inputStream
|
||||||
|
}
|
||||||
|
callback.onDataReady(stream)
|
||||||
|
}
|
||||||
|
}
|
@ -21,9 +21,9 @@ import android.database.Cursor
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
|
||||||
fun getFilenameFromUri(context: Context, uri: Uri): String? {
|
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
||||||
var result: String? = null
|
var result: String? = null
|
||||||
if (uri.scheme == "content") {
|
if (context != null && uri.scheme == "content") {
|
||||||
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
|
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
|
||||||
try {
|
try {
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
@ -616,11 +616,11 @@ class RoomDetailFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
|
override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
|
||||||
vectorBaseActivity.notImplemented()
|
vectorBaseActivity.notImplemented("open file")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) {
|
override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) {
|
||||||
vectorBaseActivity.notImplemented()
|
vectorBaseActivity.notImplemented("open audio file")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {
|
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@ -36,6 +37,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
|
|||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.intent.getFilenameFromUri
|
||||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||||
import im.vector.riotredesign.core.resources.UserPreferencesProvider
|
import im.vector.riotredesign.core.resources.UserPreferencesProvider
|
||||||
import im.vector.riotredesign.core.utils.LiveEvent
|
import im.vector.riotredesign.core.utils.LiveEvent
|
||||||
@ -360,13 +362,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
|
|
||||||
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
||||||
val attachments = action.mediaFiles.map {
|
val attachments = action.mediaFiles.map {
|
||||||
|
val nameWithExtension = getFilenameFromUri(null, Uri.parse(it.path))
|
||||||
|
|
||||||
ContentAttachmentData(
|
ContentAttachmentData(
|
||||||
size = it.size,
|
size = it.size,
|
||||||
duration = it.duration,
|
duration = it.duration,
|
||||||
date = it.date,
|
date = it.date,
|
||||||
height = it.height,
|
height = it.height,
|
||||||
width = it.width,
|
width = it.width,
|
||||||
name = it.name,
|
name = nameWithExtension ?: it.name,
|
||||||
path = it.path,
|
path = it.path,
|
||||||
mimeType = it.mimeType,
|
mimeType = it.mimeType,
|
||||||
type = ContentAttachmentData.Type.values()[it.mediaType]
|
type = ContentAttachmentData.Type.values()[it.mediaType]
|
||||||
@ -376,7 +380,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
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)
|
//We need to update this with the related m.replace also (to move read receipt)
|
||||||
action.event.annotations?.editSummary?.sourceEvents?.forEach {
|
action.event.annotations?.editSummary?.sourceEvents?.forEach {
|
||||||
room.getTimeLineEvent(it)?.let { event ->
|
room.getTimeLineEvent(it)?.let { event ->
|
||||||
|
@ -299,9 +299,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
|||||||
collapsedEventIds.removeAll(mergedEventIds)
|
collapsedEventIds.removeAll(mergedEventIds)
|
||||||
}
|
}
|
||||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it }
|
val mergeId = mergedEventIds.joinToString(separator = "_") { it }
|
||||||
MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) {
|
(MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) {
|
||||||
mergeItemCollapseStates[event.localId] = it
|
mergeItemCollapseStates[event.localId] = it
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
|
}).also {
|
||||||
|
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.model.message.MessageVideoContent
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
import im.vector.riotredesign.EmojiCompatFontProvider
|
import im.vector.riotredesign.EmojiCompatFontProvider
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||||
@ -90,7 +91,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
|
|
||||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||||
|
|
||||||
if (event.root.unsignedData?.redactedEvent != null) {
|
if (event.root.isRedacted()) {
|
||||||
//message is redacted
|
//message is redacted
|
||||||
return buildRedactedItem(informationData, highlight, callback)
|
return buildRedactedItem(informationData, highlight, callback)
|
||||||
}
|
}
|
||||||
@ -198,7 +199,8 @@ class MessageItemFactory @Inject constructor(
|
|||||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||||
val data = ImageContentRenderer.Data(
|
val data = ImageContentRenderer.Data(
|
||||||
filename = messageContent.body,
|
filename = messageContent.body,
|
||||||
url = messageContent.url,
|
url = messageContent.encryptedFileInfo?.url ?: messageContent.url,
|
||||||
|
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||||
height = messageContent.info?.height,
|
height = messageContent.info?.height,
|
||||||
maxHeight = maxHeight,
|
maxHeight = maxHeight,
|
||||||
width = messageContent.info?.width,
|
width = messageContent.info?.width,
|
||||||
@ -239,10 +241,11 @@ class MessageItemFactory @Inject constructor(
|
|||||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||||
val thumbnailData = ImageContentRenderer.Data(
|
val thumbnailData = ImageContentRenderer.Data(
|
||||||
filename = messageContent.body,
|
filename = messageContent.body,
|
||||||
url = messageContent.info?.thumbnailUrl,
|
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
|
||||||
height = messageContent.info?.height,
|
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
|
||||||
|
height = messageContent.videoInfo?.height,
|
||||||
maxHeight = maxHeight,
|
maxHeight = maxHeight,
|
||||||
width = messageContent.info?.width,
|
width = messageContent.videoInfo?.width,
|
||||||
maxWidth = maxWidth
|
maxWidth = maxWidth
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -55,7 +55,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
|
|
||||||
// Crypto
|
// Crypto
|
||||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
||||||
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
EventType.ENCRYPTED -> {
|
||||||
|
if (event.root.isRedacted()) {
|
||||||
|
// Redacted event, let the MessageItemFactory handle it
|
||||||
|
messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||||
|
} else {
|
||||||
|
encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Unhandled event types (yet)
|
// Unhandled event types (yet)
|
||||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
|
@ -32,3 +32,18 @@ class TimelineEventVisibilityStateChangedListener(private val callback: Timeline
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MergedTimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
||||||
|
private val events: List<TimelineEvent>)
|
||||||
|
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||||
|
|
||||||
|
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||||
|
if (visibilityState == VisibilityState.VISIBLE) {
|
||||||
|
events.forEach {
|
||||||
|
callback?.onEventVisible(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -22,8 +22,8 @@ import android.widget.ImageView
|
|||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.github.piasy.biv.view.BigImageView
|
import com.github.piasy.biv.view.BigImageView
|
||||||
import im.vector.matrix.android.api.Matrix
|
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
|
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotredesign.core.glide.GlideApp
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
||||||
@ -37,6 +37,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
|||||||
data class Data(
|
data class Data(
|
||||||
val filename: String,
|
val filename: String,
|
||||||
val url: String?,
|
val url: String?,
|
||||||
|
val elementToDecrypt: ElementToDecrypt?,
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
val maxHeight: Int,
|
val maxHeight: Int,
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
@ -59,17 +60,28 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
|||||||
val (width, height) = processSize(data, mode)
|
val (width, height) = processSize(data, mode)
|
||||||
imageView.layoutParams.height = height
|
imageView.layoutParams.height = height
|
||||||
imageView.layoutParams.width = width
|
imageView.layoutParams.width = width
|
||||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
|
||||||
val resolvedUrl = when (mode) {
|
|
||||||
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
|
||||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
|
||||||
}
|
|
||||||
//Fallback to base url
|
|
||||||
?: data.url
|
|
||||||
|
|
||||||
GlideApp
|
val glideRequest = if (data.elementToDecrypt != null) {
|
||||||
.with(imageView)
|
// Encrypted image
|
||||||
.load(resolvedUrl)
|
GlideApp
|
||||||
|
.with(imageView)
|
||||||
|
.load(data)
|
||||||
|
} else {
|
||||||
|
// Clear image
|
||||||
|
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||||
|
val resolvedUrl = when (mode) {
|
||||||
|
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
|
||||||
|
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||||
|
}
|
||||||
|
//Fallback to base url
|
||||||
|
?: data.url
|
||||||
|
|
||||||
|
GlideApp
|
||||||
|
.with(imageView)
|
||||||
|
.load(resolvedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
glideRequest
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.transform(RoundedCorners(dpToPx(8, imageView.context)))
|
.transform(RoundedCorners(dpToPx(8, imageView.context)))
|
||||||
.thumbnail(0.3f)
|
.thumbnail(0.3f)
|
||||||
@ -81,6 +93,8 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
|||||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||||
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
||||||
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||||
|
|
||||||
|
// TODO DECRYPT_FILE Decrypt file
|
||||||
imageView.showImage(
|
imageView.showImage(
|
||||||
Uri.parse(thumbnail),
|
Uri.parse(thumbnail),
|
||||||
Uri.parse(fullSize)
|
Uri.parse(fullSize)
|
||||||
|
@ -20,9 +20,11 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
|
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
|
||||||
import com.github.piasy.biv.view.GlideImageViewFactory
|
import com.github.piasy.biv.view.GlideImageViewFactory
|
||||||
import im.vector.riotredesign.core.di.ScreenComponent
|
import im.vector.riotredesign.core.di.ScreenComponent
|
||||||
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||||
import kotlinx.android.synthetic.main.activity_image_media_viewer.*
|
import kotlinx.android.synthetic.main.activity_image_media_viewer.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -44,9 +46,26 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
|
|||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
configureToolbar(imageMediaViewerToolbar, mediaData)
|
configureToolbar(imageMediaViewerToolbar, mediaData)
|
||||||
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
|
||||||
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
if (mediaData.elementToDecrypt != null) {
|
||||||
imageContentRenderer.render(mediaData, imageMediaViewerImageView)
|
// Encrypted image
|
||||||
|
imageMediaViewerImageView.isVisible = false
|
||||||
|
encryptedImageView.isVisible = true
|
||||||
|
|
||||||
|
GlideApp
|
||||||
|
.with(this)
|
||||||
|
.load(mediaData)
|
||||||
|
.dontAnimate()
|
||||||
|
.into(encryptedImageView)
|
||||||
|
} else {
|
||||||
|
// Clear image
|
||||||
|
imageMediaViewerImageView.isVisible = true
|
||||||
|
encryptedImageView.isVisible = false
|
||||||
|
|
||||||
|
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
||||||
|
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
||||||
|
imageContentRenderer.render(mediaData, imageMediaViewerImageView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){
|
class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){
|
||||||
|
|
||||||
|
// TODO DECRYPT_FILE Encrypted data
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Data(
|
data class Data(
|
||||||
val filename: String,
|
val filename: String,
|
||||||
|
@ -21,10 +21,10 @@ import android.graphics.Bitmap
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.Person
|
import androidx.core.app.Person
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
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.BuildConfig
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
import im.vector.riotredesign.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotredesign.core.utils.SecretStoringUtils
|
|
||||||
import im.vector.riotredesign.features.settings.PreferencesManager
|
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -32,6 +32,7 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
@ -46,6 +47,10 @@ import im.vector.riotredesign.core.utils.toast
|
|||||||
import im.vector.riotredesign.features.MainActivity
|
import im.vector.riotredesign.features.MainActivity
|
||||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||||
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
|
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -197,6 +202,18 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||||||
|
|
||||||
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
notImplemented()
|
notImplemented()
|
||||||
|
|
||||||
|
// TODO DECRYPT_FILE Quick implementation of clear cache, finish this
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
// On UI Thread
|
||||||
|
Glide.get(requireContext()).clearMemory()
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// On BG thread
|
||||||
|
Glide.get(requireContext()).clearDiskCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* TODO
|
/* TODO
|
||||||
displayLoadingView()
|
displayLoadingView()
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() {
|
|||||||
context?.let { context: Context ->
|
context?.let { context: Context ->
|
||||||
AlertDialog.Builder(context)
|
AlertDialog.Builder(context)
|
||||||
.setSingleChoiceItems(R.array.media_saving_choice,
|
.setSingleChoiceItems(R.array.media_saving_choice,
|
||||||
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
|
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
|
||||||
PreferencesManager.setSelectedMediasSavingPeriod(activity, n)
|
PreferencesManager.setSelectedMediasSavingPeriod(activity, n)
|
||||||
d.cancel()
|
d.cancel()
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.riotredesign.features.workers.signout
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
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.Session
|
||||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||||
import im.vector.riotredesign.R
|
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.core.utils.toast
|
||||||
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
@Inject lateinit var session: Session
|
lateinit var session: Session
|
||||||
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
|
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||||
|
|
||||||
|
|
||||||
@BindView(R.id.bottom_sheet_signout_warning_text)
|
@BindView(R.id.bottom_sheet_signout_warning_text)
|
||||||
@ -99,6 +101,14 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
private lateinit var viewModel: SignOutViewModel
|
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?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
@ -18,4 +19,11 @@
|
|||||||
app:failureImageInitScaleType="center"
|
app:failureImageInitScaleType="center"
|
||||||
app:optimizeDisplay="true" />
|
app:optimizeDisplay="true" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/encryptedImageView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
Loading…
Reference in New Issue
Block a user