From a1db8653ab4fd9778412704b393ca9851b1e023a Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 20:09:39 +0200 Subject: [PATCH] Basic Video Support --- attachment-viewer/build.gradle | 3 + .../attachmentviewer/AttachmentEvents.kt | 26 ++++ .../AttachmentSourceProvider.kt | 4 +- .../AttachmentViewerActivity.kt | 32 +++- .../attachmentviewer/AttachmentsAdapter.kt | 22 ++- .../riotx/attachmentviewer/VideoViewHolder.kt | 133 ++++++++++++++++ .../main/res/layout/item_video_attachment.xml | 27 +++- .../session/room/timeline/TimelineService.kt | 2 +- .../room/timeline/DefaultTimelineService.kt | 5 +- .../home/room/detail/RoomDetailFragment.kt | 5 +- .../timeline/factory/MessageItemFactory.kt | 2 +- .../features/media/AttachmentOverlayView.kt | 31 +++- .../features/media/ImageContentRenderer.kt | 20 ++- .../features/media/RoomAttachmentProvider.kt | 143 ++++++++++++++---- .../media/VectorAttachmentViewerActivity.kt | 20 ++- .../features/media/VideoContentRenderer.kt | 13 +- .../features/navigation/DefaultNavigator.kt | 30 +++- .../riotx/features/navigation/Navigator.kt | 9 +- .../uploads/media/RoomUploadsMediaFragment.kt | 3 +- vector/src/main/res/drawable/ic_pause.xml | 10 ++ .../src/main/res/drawable/ic_play_arrow.xml | 10 ++ .../layout/merge_image_attachment_overlay.xml | 58 ++++++- vector/src/main/res/values/strings.xml | 4 + 23 files changed, 537 insertions(+), 75 deletions(-) create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt create mode 100644 vector/src/main/res/drawable/ic_pause.xml create mode 100644 vector/src/main/res/drawable/ic_play_arrow.xml diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index ac41c3ed75..6b64e661fa 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -62,6 +62,9 @@ dependencies { implementation 'com.github.chrisbanes:PhotoView:2.0.0' implementation "com.github.bumptech.glide:glide:4.10.0" + implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.0' diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt new file mode 100644 index 0000000000..997790a938 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.attachmentviewer + +sealed class AttachmentEvents { + data class VideoEvent(val isPlaying: Boolean, val progress: Int, val duration: Int) : AttachmentEvents() +} + +interface AttachmentEventListener { + + fun onEvent(event: AttachmentEvents) +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt index 9539bf5565..930fc62658 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt @@ -22,7 +22,7 @@ import android.view.View sealed class AttachmentInfo { data class Image(val url: String, val data: Any?) : AttachmentInfo() data class AnimatedImage(val url: String, val data: Any?) : AttachmentInfo() - data class Video(val url: String, val data: Any) : AttachmentInfo() + data class Video(val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo() data class Audio(val url: String, val data: Any) : AttachmentInfo() data class File(val url: String, val data: Any) : AttachmentInfo() @@ -40,5 +40,7 @@ interface AttachmentSourceProvider { fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) + fun loadVideo(holder: VideoViewHolder, info: AttachmentInfo.Video) + fun overlayViewAtPosition(context: Context, position: Int) : View? } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index ffd9175fc0..99a90eb033 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -34,15 +34,17 @@ import androidx.core.view.updatePadding import androidx.transition.TransitionManager import androidx.viewpager2.widget.ViewPager2 import kotlinx.android.synthetic.main.activity_attachment_viewer.* +import java.lang.ref.WeakReference import kotlin.math.abs -abstract class AttachmentViewerActivity : AppCompatActivity() { +abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener { lateinit var pager2: ViewPager2 lateinit var imageTransitionView: ImageView lateinit var transitionImageContainer: ViewGroup var topInset = 0 + var bottomInset = 0 var systemUiVisibility = true private var overlayView: View? = null @@ -50,7 +52,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { if (value == overlayView) return overlayView?.let { rootContainer.removeView(it) } rootContainer.addView(value) - value?.updatePadding(top = topInset) + value?.updatePadding(top = topInset, bottom = bottomInset) field = value } @@ -109,8 +111,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { } override fun onPageSelected(position: Int) { - currentPosition = position - overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position) + onSelectedPositionChanged(position) } }) @@ -121,12 +122,27 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { scaleDetector = createScaleGestureDetector() ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets -> - overlayView?.updatePadding(top = insets.systemWindowInsetTop) + overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom) topInset = insets.systemWindowInsetTop + bottomInset = insets.systemWindowInsetBottom insets } } + fun onSelectedPositionChanged(position: Int) { + attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let { + (it as? BaseViewHolder)?.onSelected(false) + } + attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(position)?.let { + (it as? BaseViewHolder)?.onSelected(true) + if (it is VideoViewHolder) { + it.eventListener = WeakReference(this) + } + } + currentPosition = position + overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position) + } + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { // The zoomable view is configured to disallow interception when image is zoomed @@ -264,6 +280,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { } }) + override fun onEvent(event: AttachmentEvents) { + if (overlayView is AttachmentEventListener) { + (overlayView as? AttachmentEventListener)?.onEvent(event) + } + } + protected open fun shouldAnimateDismiss(): Boolean = true protected open fun animateClose() { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index 26577aee32..333a1b3625 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -24,7 +24,11 @@ import androidx.recyclerview.widget.RecyclerView abstract class BaseViewHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView) { - abstract fun bind(attachmentInfo: AttachmentInfo) + open fun bind(attachmentInfo: AttachmentInfo) {} + open fun onRecycled() {} + open fun onAttached() {} + open fun onDetached() {} + open fun onSelected(selected: Boolean) {} } class AttachmentViewHolder constructor(itemView: View) : @@ -59,6 +63,7 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { return when (viewType) { R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView) + R.layout.item_video_attachment -> VideoViewHolder(itemView) else -> AttachmentViewHolder(itemView) } } @@ -88,11 +93,26 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { is AttachmentInfo.AnimatedImage -> { attachmentSourceProvider?.loadImage(holder as AnimatedImageViewHolder, it) } + is AttachmentInfo.Video -> { + attachmentSourceProvider?.loadVideo(holder as VideoViewHolder, it) + } else -> {} } } } + override fun onViewAttachedToWindow(holder: BaseViewHolder) { + holder.onAttached() + } + + override fun onViewRecycled(holder: BaseViewHolder) { + holder.onRecycled() + } + + override fun onViewDetachedFromWindow(holder: BaseViewHolder) { + holder.onDetached() + } + fun isScaled(position: Int): Boolean { val holder = recyclerView?.findViewHolderForAdapterPosition(position) if (holder is ZoomableImageViewHolder) { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt new file mode 100644 index 0000000000..38b656559e --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.attachmentviewer + +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.VideoView +import androidx.core.view.isVisible +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import java.io.File +import java.lang.ref.WeakReference +import java.util.concurrent.TimeUnit + +// TODO, it would be probably better to use a unique media player +// for better customization and control +// But for now VideoView is enough, it released player when detached, we use a timer to update progress +class VideoViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) { + + private var isSelected = false + private var mVideoPath: String? = null + private var progressDisposable: Disposable? = null + + var eventListener: WeakReference? = null + +// interface Target { +// fun onResourceLoading(progress: Int, total: Int) +// fun onLoadFailed() +// fun onResourceReady(file: File) +// fun onThumbnailReady(thumbnail: Drawable?) +// } + + init { + } + + val thumbnailImage: ImageView = itemView.findViewById(R.id.videoThumbnailImage) + val videoView: VideoView = itemView.findViewById(R.id.videoView) + val loaderProgressBar: ProgressBar = itemView.findViewById(R.id.videoLoaderProgress) + val videoControlIcon: ImageView = itemView.findViewById(R.id.videoControlIcon) + val errorTextView: TextView = itemView.findViewById(R.id.videoMediaViewerErrorView) + +// val videoTarget = object : Target { +// override fun onResourceLoading(progress: Int, total: Int) { +// videoView.isVisible = false +// loaderProgressBar.isVisible = true +// } +// +// override fun onLoadFailed() { +// loaderProgressBar.isVisible = false +// } +// +// override fun onResourceReady(file: File) { +// } +// +// override fun onThumbnailReady(thumbnail: Drawable?) { +// } +// } + + override fun onRecycled() { + super.onRecycled() + progressDisposable?.dispose() + progressDisposable = null + mVideoPath = null + } + + fun videoReady(file: File) { + mVideoPath = file.path + if (isSelected) { + startPlaying() + } + } + + override fun onSelected(selected: Boolean) { + if (!selected) { + if (videoView.isPlaying) { + videoView.stopPlayback() + progressDisposable?.dispose() + progressDisposable = null + } + } else { + if (mVideoPath != null) { + startPlaying() + } + } + isSelected = true + } + + private fun startPlaying() { + thumbnailImage.isVisible = false + loaderProgressBar.isVisible = false + videoView.isVisible = true + + videoView.setOnPreparedListener { + progressDisposable?.dispose() + progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS) + .timeInterval() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + val duration = videoView.duration + val progress = videoView.currentPosition + val isPlaying = videoView.isPlaying + Log.v("FOO", "isPlaying $isPlaying $progress/$duration") + eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) + } + } + + videoView.setVideoPath(mVideoPath) + videoView.start() + } + + override fun bind(attachmentInfo: AttachmentInfo) { + Log.v("FOO", "") + } +} diff --git a/attachment-viewer/src/main/res/layout/item_video_attachment.xml b/attachment-viewer/src/main/res/layout/item_video_attachment.xml index 9449ec2e9f..29f01650fd 100644 --- a/attachment-viewer/src/main/res/layout/item_video_attachment.xml +++ b/attachment-viewer/src/main/res/layout/item_video_attachment.xml @@ -1,7 +1,8 @@ + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools"> + android:layout_centerInParent="true" /> + + + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index bdbbbf11bd..2353fc1c30 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -40,5 +40,5 @@ interface TimelineService { fun getTimeLineEventLive(eventId: String): LiveData> - fun getAttachementMessages() : List + fun getAttachmentMessages() : List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index ebdb8dd24d..32160a96eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -22,6 +22,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.isImageMessage +import im.vector.matrix.android.api.session.events.model.isVideoMessage import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineService @@ -94,14 +95,14 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv } } - override fun getAttachementMessages(): List { + override fun getAttachmentMessages(): List { // TODO pretty bad query.. maybe we should denormalize clear type in base? return doWithRealm(monarchy.realmConfiguration) { realm -> realm.where() .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) .findAll() - ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() } } + ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } } ?: emptyList() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 581bc8b2b2..a457587aa8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -1178,7 +1178,10 @@ class RoomDetailFragment @Inject constructor( } override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { - navigator.openVideoViewer(requireActivity(), mediaData) + navigator.openVideoViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs -> + pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) + pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) + } } // override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 2174556098..4f5f34cbf0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -337,7 +337,7 @@ class MessageItemFactory @Inject constructor( .playable(true) .highlighted(highlight) .mediaData(thumbnailData) - .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } + .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) } } private fun buildItemForTextContent(messageContent: MessageTextContent, diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt index af6c4991fa..ebd54bcd0b 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt @@ -21,20 +21,28 @@ import android.graphics.Color import android.util.AttributeSet import android.view.View import android.widget.ImageView +import android.widget.SeekBar import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.Group import im.vector.riotx.R +import im.vector.riotx.attachmentviewer.AttachmentEventListener +import im.vector.riotx.attachmentviewer.AttachmentEvents class AttachmentOverlayView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr) { +) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener { - var onShareCallback: (() -> Unit) ? = null - var onBack: (() -> Unit) ? = null + var onShareCallback: (() -> Unit)? = null + var onBack: (() -> Unit)? = null private val counterTextView: TextView private val infoTextView: TextView private val shareImage: ImageView + private val overlayPlayPauseButton: ImageView + private val overlaySeekBar: SeekBar + + val videoControlsGroup: Group init { View.inflate(context, R.layout.merge_image_attachment_overlay, this) @@ -42,14 +50,29 @@ class AttachmentOverlayView @JvmOverloads constructor( counterTextView = findViewById(R.id.overlayCounterText) infoTextView = findViewById(R.id.overlayInfoText) shareImage = findViewById(R.id.overlayShareButton) + videoControlsGroup = findViewById(R.id.overlayVideoControlsGroup) + overlayPlayPauseButton = findViewById(R.id.overlayPlayPauseButton) + overlaySeekBar = findViewById(R.id.overlaySeekBar) + overlaySeekBar.isEnabled = false findViewById(R.id.overlayBackButton).setOnClickListener { onBack?.invoke() } } - fun updateWith(counter: String, senderInfo : String) { + fun updateWith(counter: String, senderInfo: String) { counterTextView.text = counter infoTextView.text = senderInfo } + + override fun onEvent(event: AttachmentEvents) { + when (event) { + is AttachmentEvents.VideoEvent -> { + overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause) + val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat() + val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100) + overlaySeekBar.progress = percent + } + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index bc9c64c801..f7613855c5 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -44,21 +44,29 @@ import java.io.File import javax.inject.Inject import kotlin.math.min +interface AttachmentData : Parcelable { + val eventId: String + val filename: String + val mimeType: String? + val url: String? + val elementToDecrypt: ElementToDecrypt? +} + class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val dimensionConverter: DimensionConverter) { @Parcelize data class Data( - val eventId: String, - val filename: String, - val mimeType: String?, - val url: String?, - val elementToDecrypt: ElementToDecrypt?, + override val eventId: String, + override val filename: String, + override val mimeType: String?, + override val url: String?, + override val elementToDecrypt: ElementToDecrypt?, val height: Int?, val maxHeight: Int, val width: Int?, val maxWidth: Int - ) : Parcelable { + ) : AttachmentData { fun isLocalFile() = url.isLocalFile() } diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 84311c997a..09459b20d1 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -19,9 +19,18 @@ package im.vector.riotx.features.media import android.content.Context import android.graphics.drawable.Drawable import android.view.View +import android.widget.ImageView +import androidx.core.view.isVisible import com.bumptech.glide.request.target.CustomViewTarget +import com.bumptech.glide.request.transition.Transition +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.isVideoMessage import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.file.FileService 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.MessageVideoContent import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -29,16 +38,20 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.attachmentviewer.AnimatedImageViewHolder import im.vector.riotx.attachmentviewer.AttachmentInfo import im.vector.riotx.attachmentviewer.AttachmentSourceProvider +import im.vector.riotx.attachmentviewer.VideoViewHolder import im.vector.riotx.attachmentviewer.ZoomableImageViewHolder import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime +import java.io.File import javax.inject.Inject class RoomAttachmentProvider( private val attachments: List, private val initialIndex: Int, private val imageContentRenderer: ImageContentRenderer, - private val dateFormatter: VectorDateFormatter + private val videoContentRenderer: VideoContentRenderer, + private val dateFormatter: VectorDateFormatter, + private val fileService: FileService ) : AttachmentSourceProvider { interface InteractionListener { @@ -57,26 +70,64 @@ class RoomAttachmentProvider( override fun getAttachmentInfoAt(position: Int): AttachmentInfo { return attachments[position].let { val content = it.root.getClearContent().toModel() as? MessageWithAttachmentContent - val data = ImageContentRenderer.Data( - eventId = it.eventId, - filename = content?.body ?: "", - mimeType = content?.mimeType, - url = content?.getFileUrl(), - elementToDecrypt = content?.encryptedFileInfo?.toElementToDecrypt(), - maxHeight = -1, - maxWidth = -1, - width = null, - height = null - ) - if (content?.mimeType == "image/gif") { - AttachmentInfo.AnimatedImage( - content.url ?: "", - data + if (content is MessageImageContent) { + val data = ImageContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.getFileUrl(), + elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), + maxHeight = -1, + maxWidth = -1, + width = null, + height = null + ) + if (content.mimeType == "image/gif") { + AttachmentInfo.AnimatedImage( + content.url ?: "", + data + ) + } else { + AttachmentInfo.Image( + content.url ?: "", + data + ) + } + } else if (content is MessageVideoContent) { + val thumbnailData = ImageContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.videoInfo?.thumbnailFile?.url + ?: content.videoInfo?.thumbnailUrl, + elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(), + height = content.videoInfo?.height, + maxHeight = -1, + width = content.videoInfo?.width, + maxWidth = -1 + ) + val data = VideoContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.getFileUrl(), + elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), + thumbnailMediaData = thumbnailData + ) + AttachmentInfo.Video( + content.getFileUrl() ?: "", + data, + AttachmentInfo.Image( + url = content.videoInfo?.thumbnailFile?.url + ?: content.videoInfo?.thumbnailUrl ?: "", + data = thumbnailData + + ) ) } else { AttachmentInfo.Image( - content?.url ?: "", - data + "", + null ) } } @@ -94,6 +145,49 @@ class RoomAttachmentProvider( } } + override fun loadVideo(holder: VideoViewHolder, info: AttachmentInfo.Video) { + val data = info.data as? VideoContentRenderer.Data ?: return +// videoContentRenderer.render(data, +// holder.thumbnailImage, +// holder.loaderProgressBar, +// holder.videoView, +// holder.errorTextView) + imageContentRenderer.render(data.thumbnailMediaData, holder.thumbnailImage, object : CustomViewTarget(holder.thumbnailImage) { + override fun onLoadFailed(errorDrawable: Drawable?) { + holder.thumbnailImage.setImageDrawable(errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + holder.thumbnailImage.setImageDrawable(resource) + } + }) + + holder.thumbnailImage.isVisible = false + holder.loaderProgressBar.isVisible = false + holder.videoView.isVisible = false + + fileService.downloadFile( + downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, + id = data.eventId, + mimeType = data.mimeType, + elementToDecrypt = data.elementToDecrypt, + fileName = data.filename, + url = data.url, + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + holder.videoReady(data) + } + + override fun onFailure(failure: Throwable) { + holder.videoView.isVisible = false + } + } + ) + } + override fun overlayViewAtPosition(context: Context, position: Int): View? { if (overlayView == null) { overlayView = AttachmentOverlayView(context) @@ -109,22 +203,19 @@ class RoomAttachmentProvider( "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} " } overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString") + overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage() return overlayView } - -// override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) { -// (info.data as? ImageContentRenderer.Data)?.let { -// imageContentRenderer.render(it, ImageContentRenderer.Mode.FULL_SIZE, holder.touchImageView) -// } -// } } class RoomAttachmentProviderFactory @Inject constructor( private val imageContentRenderer: ImageContentRenderer, - private val vectorDateFormatter: VectorDateFormatter + private val vectorDateFormatter: VectorDateFormatter, + private val videoContentRenderer: VideoContentRenderer, + private val session: Session ) { fun createProvider(attachments: List, initialIndex: Int): RoomAttachmentProvider { - return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter) + return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, videoContentRenderer, vectorDateFormatter, session.fileService()) } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 0db7d6db2a..4c310b9c47 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -80,7 +80,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() } val room = args.roomId?.let { session.getRoom(it) } - val events = room?.getAttachementMessages() ?: emptyList() + val events = room?.getAttachmentMessages() ?: emptyList() val index = events.indexOfFirst { it.eventId == args.eventId } initialIndex = index @@ -90,8 +90,8 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen transitionImageContainer.isVisible = true // Postpone transaction a bit until thumbnail is loaded - val mediaData: ImageContentRenderer.Data? = intent.getParcelableExtra(EXTRA_IMAGE_DATA) - if (mediaData != null) { + val mediaData: Parcelable? = intent.getParcelableExtra(EXTRA_IMAGE_DATA) + if (mediaData is ImageContentRenderer.Data) { // will be shown at end of transition pager2.isInvisible = true supportPostponeEnterTransition() @@ -99,6 +99,14 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen // Proceed with transaction scheduleStartPostponedTransition(imageTransitionView) } + } else if (mediaData is VideoContentRenderer.Data) { + // will be shown at end of transition + pager2.isInvisible = true + supportPostponeEnterTransition() + imageContentRenderer.renderThumbnailDontTransform(mediaData.thumbnailMediaData, imageTransitionView) { + // Proceed with transaction + scheduleStartPostponedTransition(imageTransitionView) + } } } } @@ -108,6 +116,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen setSourceProvider(sourceProvider) if (savedInstanceState == null) { pager2.setCurrentItem(index, false) + // The page change listener is not notified of the change... + pager2.post { + onSelectedPositionChanged(index) + } } window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha) @@ -202,7 +214,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA" fun newIntent(context: Context, - mediaData: ImageContentRenderer.Data, + mediaData: AttachmentData, roomId: String?, eventId: String, sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt index 760d3b12a0..e6dec88349 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.media -import android.os.Parcelable import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView @@ -38,13 +37,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: @Parcelize data class Data( - val eventId: String, - val filename: String, - val mimeType: String?, - val url: String?, - val elementToDecrypt: ElementToDecrypt?, + override val eventId: String, + override val filename: String, + override val mimeType: String?, + override val url: String?, + override val elementToDecrypt: ElementToDecrypt?, val thumbnailMediaData: ImageContentRenderer.Data - ) : Parcelable + ) : AttachmentData fun render(data: Data, thumbnailView: ImageView, diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index a640823d34..2b0b6175f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -49,11 +49,10 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity +import im.vector.riotx.features.media.AttachmentData import im.vector.riotx.features.media.BigImageViewerActivity -import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VectorAttachmentViewerActivity import im.vector.riotx.features.media.VideoContentRenderer -import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity @@ -248,7 +247,7 @@ class DefaultNavigator @Inject constructor( override fun openImageViewer(activity: Activity, roomId: String?, - mediaData: ImageContentRenderer.Data, + mediaData: AttachmentData, view: View, options: ((MutableList>) -> Unit)?) { VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent -> @@ -284,9 +283,28 @@ class DefaultNavigator @Inject constructor( // activity.startActivity(intent, bundle) } - override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) { - val intent = VideoMediaViewerActivity.newIntent(activity, mediaData) - activity.startActivity(intent) + override fun openVideoViewer(activity: Activity, + roomId: String?, mediaData: VideoContentRenderer.Data, + view: View, + options: ((MutableList>) -> Unit)?) { +// val intent = VideoMediaViewerActivity.newIntent(activity, mediaData) +// activity.startActivity(intent) + VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent -> + val pairs = ArrayList>() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { + pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) + } + activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { + pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) + } + } + pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) + options?.invoke(pairs) + + val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() + activity.startActivity(intent, bundle) + } } private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index ebb09f686d..f1be6e072b 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -26,7 +26,7 @@ import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes -import im.vector.riotx.features.media.ImageContentRenderer +import im.vector.riotx.features.media.AttachmentData import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.SharedData @@ -93,9 +93,12 @@ interface Navigator { fun openImageViewer(activity: Activity, roomId: String?, - mediaData: ImageContentRenderer.Data, + mediaData: AttachmentData, view: View, options: ((MutableList>) -> Unit)?) - fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) + fun openVideoViewer(activity: Activity, + roomId: String?, mediaData: VideoContentRenderer.Data, + view: View, + options: ((MutableList>) -> Unit)?) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index d373168d25..a5f126875a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -81,7 +81,8 @@ class RoomUploadsMediaFragment @Inject constructor( } override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) { - navigator.openVideoViewer(requireActivity(), mediaData) + // TODO + // navigator.openVideoViewer(requireActivity(), mediaData, null, ) } override fun loadMore() { diff --git a/vector/src/main/res/drawable/ic_pause.xml b/vector/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000000..13d6d2ec00 --- /dev/null +++ b/vector/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_play_arrow.xml b/vector/src/main/res/drawable/ic_play_arrow.xml new file mode 100644 index 0000000000..13c137a921 --- /dev/null +++ b/vector/src/main/res/drawable/ic_play_arrow.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml index 07d4baedc1..db22c0112c 100644 --- a/vector/src/main/res/layout/merge_image_attachment_overlay.xml +++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml @@ -10,7 +10,8 @@ android:id="@+id/overlayTopBackground" android:layout_width="match_parent" android:layout_height="60dp" - android:background="@color/black_alpha"> + android:background="@color/black_alpha" + app:layout_constraintTop_toTopOf="parent"> @@ -21,12 +22,12 @@ android:layout_height="44dp" android:layout_marginStart="8dp" android:layout_marginEnd="16dp" - android:scaleType="centerInside" - android:clickable="true" - android:focusable="true" android:background="?attr/selectableItemBackgroundBorderless" + android:clickable="true" android:contentDescription="@string/share" + android:focusable="true" android:padding="6dp" + android:scaleType="centerInside" android:tint="@color/white" app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground" app:layout_constraintStart_toStartOf="@id/overlayTopBackground" @@ -72,6 +73,8 @@ android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:background="?attr/selectableItemBackgroundBorderless" + android:clickable="true" + android:focusable="true" android:contentDescription="@string/share" android:padding="6dp" android:tint="@color/white" @@ -80,4 +83,51 @@ app:layout_constraintTop_toTopOf="@id/overlayTopBackground" app:srcCompat="@drawable/ic_share" /> + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index c3f0e9df41..b57f0932aa 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -76,6 +76,10 @@ Disconnect Report content Active call + Play + Pause + + Ongoing conference call.\nJoin as %1$s or %2$s Voice