From 1d400180bccfeec1798d8fb281e4bbc22779e553 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Jan 2019 18:43:15 +0100 Subject: [PATCH] Timeline : start to handle media images/gif. Still a lot to do, but it's a first step. --- .../features/home/AvatarRenderer.kt | 5 +- .../room/detail/timeline/AbsMessageItem.kt | 33 ++++++++ .../room/detail/timeline/MessageImageItem.kt | 25 ++++++ .../detail/timeline/MessageInformationData.kt | 8 ++ .../detail/timeline/MessageItemFactory.kt | 25 +++--- .../room/detail/timeline/MessageTextItem.kt | 29 ++----- .../features/media/MessageImageRenderer.kt | 79 +++++++++++++++++++ .../item_timeline_event_image_message.xml | 64 +++++++++++++++ ...l => item_timeline_event_text_message.xml} | 0 .../api/session/content/ContentUrlResolver.kt | 31 ++++++++ .../session/room/model/message/ImageInfo.kt | 6 +- .../parsing/RuntimeJsonAdapterFactory.java | 10 +-- 12 files changed, 266 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/AbsMessageItem.kt create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageImageItem.kt create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageInformationData.kt create mode 100644 app/src/main/java/im/vector/riotredesign/features/media/MessageImageRenderer.kt create mode 100644 app/src/main/res/layout/item_timeline_event_image_message.xml rename app/src/main/res/layout/{item_timeline_event_message.xml => item_timeline_event_text_message.xml} (100%) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUrlResolver.kt diff --git a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index 1e6b27d3..34091211 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -1,9 +1,10 @@ package im.vector.riotredesign.features.home -import androidx.core.content.ContextCompat import android.widget.ImageView +import androidx.core.content.ContextCompat import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.request.RequestOptions +import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotredesign.R @@ -27,7 +28,7 @@ object AvatarRenderer { if (name.isNullOrEmpty()) { return } - val resolvedUrl = avatarUrl?.replace(MXC_PREFIX, MEDIA_URL) + val resolvedUrl = ContentUrlResolver.resolve(avatarUrl) val avatarColor = ContextCompat.getColor(imageView.context, R.color.pale_teal) val fallbackDrawable = TextDrawable.builder().buildRound(name.firstCharAsString().toUpperCase(), avatarColor) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/AbsMessageItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/AbsMessageItem.kt new file mode 100644 index 00000000..90a26705 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/AbsMessageItem.kt @@ -0,0 +1,33 @@ +package im.vector.riotredesign.features.home.room.detail.timeline + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.LayoutRes +import im.vector.riotredesign.core.epoxy.KotlinModel +import im.vector.riotredesign.features.home.AvatarRenderer + +abstract class AbsMessageItem(private val informationData: MessageInformationData, + @LayoutRes layoutRes: Int +) : KotlinModel(layoutRes) { + + protected abstract val avatarImageView: ImageView + protected abstract val memberNameView: TextView + protected abstract val timeView: TextView + + override fun bind() { + if (informationData.showInformation) { + avatarImageView.visibility = View.VISIBLE + memberNameView.visibility = View.VISIBLE + timeView.visibility = View.VISIBLE + timeView.text = informationData.time + memberNameView.text = informationData.memberName + AvatarRenderer.render(informationData.avatarUrl, informationData.memberName?.toString(), avatarImageView) + } else { + avatarImageView.visibility = View.GONE + memberNameView.visibility = View.GONE + timeView.visibility = View.GONE + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageImageItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageImageItem.kt new file mode 100644 index 00000000..bc7368f6 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageImageItem.kt @@ -0,0 +1,25 @@ +package im.vector.riotredesign.features.home.room.detail.timeline + +import android.widget.ImageView +import android.widget.TextView +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.riotredesign.R +import im.vector.riotredesign.features.media.MessageImageRenderer + +class MessageImageItem( + private val messageContent: MessageImageContent, + informationData: MessageInformationData +) : AbsMessageItem(informationData, R.layout.item_timeline_event_image_message) { + + override val avatarImageView by bind(R.id.messageAvatarImageView) + override val memberNameView by bind(R.id.messageMemberNameView) + override val timeView by bind(R.id.messageTimeView) + private val imageView by bind(R.id.messageImageView) + + override fun bind() { + super.bind() + MessageImageRenderer.render(messageContent, imageView) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageInformationData.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageInformationData.kt new file mode 100644 index 00000000..09a1ede2 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageInformationData.kt @@ -0,0 +1,8 @@ +package im.vector.riotredesign.features.home.room.detail.timeline + +data class MessageInformationData( + val time: CharSequence? = null, + val avatarUrl: String?, + val memberName: CharSequence? = null, + val showInformation: Boolean = true +) \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt index e77b150c..8168be1c 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt @@ -8,6 +8,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.TimelineEvent import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.riotredesign.core.extensions.localDateTime @@ -18,7 +19,7 @@ class MessageItemFactory(private val timelineDateFormatter: TimelineDateFormatte fun create(event: TimelineEvent, nextEvent: TimelineEvent?, callback: TimelineEventController.Callback? - ): MessageTextItem? { + ): AbsMessageItem? { val roomMember = event.roomMember val nextRoomMember = nextEvent?.roomMember @@ -41,21 +42,24 @@ class MessageItemFactory(private val timelineDateFormatter: TimelineDateFormatte val time = timelineDateFormatter.formatMessageHour(date) val avatarUrl = roomMember?.avatarUrl val memberName = roomMember?.displayName ?: event.root.sender + val informationData = MessageInformationData(time, avatarUrl, memberName, showInformation) return when (messageContent) { - is MessageTextContent -> buildTextMessageItem(messageContent, memberName, avatarUrl, time, showInformation, callback) - else -> null + is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback) + is MessageImageContent -> buildImageMessageItem(messageContent, informationData) + else -> null } } + private fun buildImageMessageItem(messageContent: MessageImageContent, informationData: MessageInformationData): MessageImageItem? { + return MessageImageItem(messageContent, informationData) + } + private fun buildTextMessageItem(messageContent: MessageTextContent, - memberName: String?, - avatarUrl: String?, - time: String, - showInformation: Boolean, + informationData: MessageInformationData, callback: TimelineEventController.Callback?): MessageTextItem? { - val message = messageContent.body?.let { + val message = messageContent.body.let { val spannable = SpannableStringBuilder(it) MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { override fun onUrlClicked(url: String) { @@ -67,10 +71,7 @@ class MessageItemFactory(private val timelineDateFormatter: TimelineDateFormatte } return MessageTextItem( message = message, - avatarUrl = avatarUrl, - showInformation = showInformation, - time = time, - memberName = memberName + informationData = informationData ) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt index e75a96af..5cc0de50 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt @@ -1,40 +1,23 @@ package im.vector.riotredesign.features.home.room.detail.timeline -import android.view.View import android.widget.ImageView import android.widget.TextView import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.riotredesign.R -import im.vector.riotredesign.core.epoxy.KotlinModel -import im.vector.riotredesign.features.home.AvatarRenderer class MessageTextItem( val message: CharSequence? = null, - val time: CharSequence? = null, - val avatarUrl: String?, - val memberName: CharSequence? = null, - val showInformation: Boolean = true -) : KotlinModel(R.layout.item_timeline_event_message) { + informationData: MessageInformationData +) : AbsMessageItem(informationData, R.layout.item_timeline_event_text_message) { - private val avatarImageView by bind(R.id.messageAvatarImageView) - private val memberNameView by bind(R.id.messageMemberNameView) - private val timeView by bind(R.id.messageTimeView) + override val avatarImageView by bind(R.id.messageAvatarImageView) + override val memberNameView by bind(R.id.messageMemberNameView) + override val timeView by bind(R.id.messageTimeView) private val messageView by bind(R.id.messageTextView) override fun bind() { + super.bind() messageView.text = message MatrixLinkify.addLinkMovementMethod(messageView) - if (showInformation) { - avatarImageView.visibility = View.VISIBLE - memberNameView.visibility = View.VISIBLE - timeView.visibility = View.VISIBLE - timeView.text = time - memberNameView.text = memberName - AvatarRenderer.render(avatarUrl, memberName?.toString(), avatarImageView) - } else { - avatarImageView.visibility = View.GONE - memberNameView.visibility = View.GONE - timeView.visibility = View.GONE - } } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/media/MessageImageRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/media/MessageImageRenderer.kt new file mode 100644 index 00000000..87eb6abc --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/media/MessageImageRenderer.kt @@ -0,0 +1,79 @@ +package im.vector.riotredesign.features.media + +import android.content.Context +import android.graphics.Point +import android.media.ExifInterface +import android.view.WindowManager +import android.widget.ImageView +import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.riotredesign.core.glide.GlideApp + +object MessageImageRenderer { + + fun render(messageContent: MessageImageContent, imageView: ImageView) { + val (maxImageWidth, maxImageHeight) = computeMaxSize(imageView.context) + val imageInfo = messageContent.info + val rotationAngle = imageInfo.rotation ?: 0 + val orientation = imageInfo.orientation ?: ExifInterface.ORIENTATION_NORMAL + var width = imageInfo.width + var height = imageInfo.height + + var finalHeight = -1 + var finalWidth = -1 + + // if the image size is known + // compute the expected height + if (width > 0 && height > 0) { + // swap width and height if the image is side oriented + if (rotationAngle == 90 || rotationAngle == 270) { + val tmp = width + width = height + height = tmp + } else if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270) { + val tmp = width + width = height + height = tmp + } + finalHeight = Math.min(maxImageWidth * height / width, maxImageHeight) + finalWidth = finalHeight * width / height + } + // ensure that some values are properly initialized + if (finalHeight < 0) { + finalHeight = maxImageHeight + } + if (finalWidth < 0) { + finalWidth = maxImageWidth + } + imageView.layoutParams.height = finalHeight + imageView.layoutParams.width = finalWidth + + val resolvedUrl = ContentUrlResolver.resolve(messageContent.url) ?: return + GlideApp + .with(imageView) + .load(resolvedUrl) + .override(finalWidth, finalHeight) + .thumbnail(0.3f) + .into(imageView) + } + + private fun computeMaxSize(context: Context): Pair { + val size = Point(0, 0) + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + wm.defaultDisplay.getSize(size) + val screenWidth = size.x + val screenHeight = size.y + val maxImageWidth: Int + val maxImageHeight: Int + // landscape / portrait + if (screenWidth < screenHeight) { + maxImageWidth = Math.round(screenWidth * 0.6f) + maxImageHeight = Math.round(screenHeight * 0.4f) + } else { + maxImageWidth = Math.round(screenWidth * 0.4f) + maxImageHeight = Math.round(screenHeight * 0.6f) + } + return Pair(maxImageWidth, maxImageHeight) + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/item_timeline_event_image_message.xml b/app/src/main/res/layout/item_timeline_event_image_message.xml new file mode 100644 index 00000000..a8cca347 --- /dev/null +++ b/app/src/main/res/layout/item_timeline_event_image_message.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_timeline_event_message.xml b/app/src/main/res/layout/item_timeline_event_text_message.xml similarity index 100% rename from app/src/main/res/layout/item_timeline_event_message.xml rename to app/src/main/res/layout/item_timeline_event_text_message.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUrlResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUrlResolver.kt new file mode 100644 index 00000000..a8f116fb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUrlResolver.kt @@ -0,0 +1,31 @@ +package im.vector.matrix.android.api.session.content + +object ContentUrlResolver { + + private const val MATRIX_CONTENT_URI_SCHEME = "mxc://" + private const val MEDIA_URL = "https://matrix.org/_matrix/media/v1/download/" + + /** + * Get the actual URL for accessing the full-size image of a Matrix media content URI. + * + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @return the URL to access the described resource, or null if the url is invalid. + */ + fun resolve(contentUrl: String?): String? { + if (contentUrl.isValidMatrixContentUrl()) { + return contentUrl?.replace(MATRIX_CONTENT_URI_SCHEME, MEDIA_URL) + } + return null + } + + /** + * Check whether an url is a valid matrix content url. + * + * @param contentUrl the content URL (in the form of "mxc://..."). + * @return true if contentUrl is valid. + */ + private fun String?.isValidMatrixContentUrl(): Boolean { + return !this.isNullOrEmpty() && startsWith(MATRIX_CONTENT_URI_SCHEME) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ImageInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ImageInfo.kt index f636c82c..8c58f6bb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ImageInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ImageInfo.kt @@ -6,9 +6,9 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ImageInfo( @Json(name = "mimetype") val mimeType: String, - @Json(name = "w") val w: Int, - @Json(name = "h") val h: Int, - @Json(name = "size") val size: Long, + @Json(name = "w") val width: Int, + @Json(name = "h") val height: Int, + @Json(name = "size") val size: Int, @Json(name = "rotation") val rotation: Int? = null, @Json(name = "orientation") val orientation: Int? = null, @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/parsing/RuntimeJsonAdapterFactory.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/parsing/RuntimeJsonAdapterFactory.java index 369efdde..8ebad453 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/parsing/RuntimeJsonAdapterFactory.java +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/parsing/RuntimeJsonAdapterFactory.java @@ -129,16 +129,8 @@ public final class RuntimeJsonAdapterFactory implements JsonAdapter.Factory { Object jsonValue = reader.readJsonValue(); Map jsonObject = (Map) jsonValue; Object label = jsonObject.get(labelKey); - if (label == null) { - throw new JsonDataException("Missing label for " + labelKey); - } if (!(label instanceof String)) { - throw new JsonDataException("Label for '" - + labelKey - + "' must be a string but was " - + label - + ", a " - + label.getClass()); + return null; } JsonAdapter adapter = labelToAdapter.get(label); if (adapter == null) {