Basic Video Support

This commit is contained in:
Valere 2020-07-08 20:09:39 +02:00
parent cc5df1e1d5
commit a1db8653ab
23 changed files with 537 additions and 75 deletions

View File

@ -62,6 +62,9 @@ dependencies {
implementation 'com.github.chrisbanes:PhotoView:2.0.0' implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation "com.github.bumptech.glide:glide:4.10.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 fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.0' implementation 'androidx.core:core-ktx:1.3.0'

View File

@ -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)
}

View File

@ -22,7 +22,7 @@ import android.view.View
sealed class AttachmentInfo { sealed class AttachmentInfo {
data class Image(val url: String, val data: Any?) : AttachmentInfo() data class Image(val url: String, val data: Any?) : AttachmentInfo()
data class AnimatedImage(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 Audio(val url: String, val data: Any) : AttachmentInfo()
data class File(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 loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage)
fun loadVideo(holder: VideoViewHolder, info: AttachmentInfo.Video)
fun overlayViewAtPosition(context: Context, position: Int) : View? fun overlayViewAtPosition(context: Context, position: Int) : View?
} }

View File

@ -34,15 +34,17 @@ import androidx.core.view.updatePadding
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.activity_attachment_viewer.* import kotlinx.android.synthetic.main.activity_attachment_viewer.*
import java.lang.ref.WeakReference
import kotlin.math.abs import kotlin.math.abs
abstract class AttachmentViewerActivity : AppCompatActivity() { abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
lateinit var pager2: ViewPager2 lateinit var pager2: ViewPager2
lateinit var imageTransitionView: ImageView lateinit var imageTransitionView: ImageView
lateinit var transitionImageContainer: ViewGroup lateinit var transitionImageContainer: ViewGroup
var topInset = 0 var topInset = 0
var bottomInset = 0
var systemUiVisibility = true var systemUiVisibility = true
private var overlayView: View? = null private var overlayView: View? = null
@ -50,7 +52,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() {
if (value == overlayView) return if (value == overlayView) return
overlayView?.let { rootContainer.removeView(it) } overlayView?.let { rootContainer.removeView(it) }
rootContainer.addView(value) rootContainer.addView(value)
value?.updatePadding(top = topInset) value?.updatePadding(top = topInset, bottom = bottomInset)
field = value field = value
} }
@ -109,8 +111,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() {
} }
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
currentPosition = position onSelectedPositionChanged(position)
overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position)
} }
}) })
@ -121,12 +122,27 @@ abstract class AttachmentViewerActivity : AppCompatActivity() {
scaleDetector = createScaleGestureDetector() scaleDetector = createScaleGestureDetector()
ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets -> ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets ->
overlayView?.updatePadding(top = insets.systemWindowInsetTop) overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom)
topInset = insets.systemWindowInsetTop topInset = insets.systemWindowInsetTop
bottomInset = insets.systemWindowInsetBottom
insets 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 { override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// The zoomable view is configured to disallow interception when image is zoomed // 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 shouldAnimateDismiss(): Boolean = true
protected open fun animateClose() { protected open fun animateClose() {

View File

@ -24,7 +24,11 @@ import androidx.recyclerview.widget.RecyclerView
abstract class BaseViewHolder constructor(itemView: View) : abstract class BaseViewHolder constructor(itemView: View) :
RecyclerView.ViewHolder(itemView) { 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) : class AttachmentViewHolder constructor(itemView: View) :
@ -59,6 +63,7 @@ class AttachmentsAdapter() : RecyclerView.Adapter<BaseViewHolder>() {
return when (viewType) { return when (viewType) {
R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView)
R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView) R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView)
R.layout.item_video_attachment -> VideoViewHolder(itemView)
else -> AttachmentViewHolder(itemView) else -> AttachmentViewHolder(itemView)
} }
} }
@ -88,11 +93,26 @@ class AttachmentsAdapter() : RecyclerView.Adapter<BaseViewHolder>() {
is AttachmentInfo.AnimatedImage -> { is AttachmentInfo.AnimatedImage -> {
attachmentSourceProvider?.loadImage(holder as AnimatedImageViewHolder, it) attachmentSourceProvider?.loadImage(holder as AnimatedImageViewHolder, it)
} }
is AttachmentInfo.Video -> {
attachmentSourceProvider?.loadVideo(holder as VideoViewHolder, it)
}
else -> {} 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 { fun isScaled(position: Int): Boolean {
val holder = recyclerView?.findViewHolderForAdapterPosition(position) val holder = recyclerView?.findViewHolderForAdapterPosition(position)
if (holder is ZoomableImageViewHolder) { if (holder is ZoomableImageViewHolder) {

View File

@ -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<AttachmentEventListener>? = 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", "")
}
}

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<ImageView <ImageView
android:id="@+id/videoThumbnailImage" android:id="@+id/videoThumbnailImage"
@ -14,13 +15,35 @@
android:id="@+id/videoView" android:id="@+id/videoView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" /> android:layout_centerInParent="true" />
<ImageView <ImageView
android:id="@+id/videoControlIcon" android:id="@+id/videoControlIcon"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="44dp" android:layout_width="44dp"
android:layout_height="44dp" android:layout_height="44dp"
/> />
<ProgressBar
android:layout_centerInParent="true"
android:id="@+id/videoLoaderProgress"
style="?android:attr/progressBarStyle"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="invisible"
tools:visibility="visible" />
<TextView
android:id="@+id/videoMediaViewerErrorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="16dp"
android:textSize="16sp"
android:visibility="gone"
tools:text="Error"
tools:visibility="visible" />
</RelativeLayout> </RelativeLayout>

View File

@ -40,5 +40,5 @@ interface TimelineService {
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
fun getAttachementMessages() : List<TimelineEvent> fun getAttachmentMessages() : List<TimelineEvent>
} }

View File

@ -22,6 +22,7 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.isImageMessage 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.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
@ -94,14 +95,14 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
} }
} }
override fun getAttachementMessages(): List<TimelineEvent> { override fun getAttachmentMessages(): List<TimelineEvent> {
// TODO pretty bad query.. maybe we should denormalize clear type in base? // TODO pretty bad query.. maybe we should denormalize clear type in base?
return doWithRealm(monarchy.realmConfiguration) { realm -> return doWithRealm(monarchy.realmConfiguration) { realm ->
realm.where<TimelineEventEntity>() realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId) .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.findAll() .findAll()
?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() } } ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
?: emptyList() ?: emptyList()
} }
} }

View File

@ -1178,7 +1178,10 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { 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) { // override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {

View File

@ -337,7 +337,7 @@ class MessageItemFactory @Inject constructor(
.playable(true) .playable(true)
.highlighted(highlight) .highlighted(highlight)
.mediaData(thumbnailData) .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, private fun buildItemForTextContent(messageContent: MessageTextContent,

View File

@ -21,20 +21,28 @@ import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.Group
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.attachmentviewer.AttachmentEventListener
import im.vector.riotx.attachmentviewer.AttachmentEvents
class AttachmentOverlayView @JvmOverloads constructor( class AttachmentOverlayView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) { ) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
var onShareCallback: (() -> Unit) ? = null var onShareCallback: (() -> Unit)? = null
var onBack: (() -> Unit) ? = null var onBack: (() -> Unit)? = null
private val counterTextView: TextView private val counterTextView: TextView
private val infoTextView: TextView private val infoTextView: TextView
private val shareImage: ImageView private val shareImage: ImageView
private val overlayPlayPauseButton: ImageView
private val overlaySeekBar: SeekBar
val videoControlsGroup: Group
init { init {
View.inflate(context, R.layout.merge_image_attachment_overlay, this) View.inflate(context, R.layout.merge_image_attachment_overlay, this)
@ -42,14 +50,29 @@ class AttachmentOverlayView @JvmOverloads constructor(
counterTextView = findViewById(R.id.overlayCounterText) counterTextView = findViewById(R.id.overlayCounterText)
infoTextView = findViewById(R.id.overlayInfoText) infoTextView = findViewById(R.id.overlayInfoText)
shareImage = findViewById(R.id.overlayShareButton) shareImage = findViewById(R.id.overlayShareButton)
videoControlsGroup = findViewById(R.id.overlayVideoControlsGroup)
overlayPlayPauseButton = findViewById(R.id.overlayPlayPauseButton)
overlaySeekBar = findViewById(R.id.overlaySeekBar)
overlaySeekBar.isEnabled = false
findViewById<ImageView>(R.id.overlayBackButton).setOnClickListener { findViewById<ImageView>(R.id.overlayBackButton).setOnClickListener {
onBack?.invoke() onBack?.invoke()
} }
} }
fun updateWith(counter: String, senderInfo : String) { fun updateWith(counter: String, senderInfo: String) {
counterTextView.text = counter counterTextView.text = counter
infoTextView.text = senderInfo 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
}
}
}
} }

View File

@ -44,21 +44,29 @@ import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.min 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, class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val dimensionConverter: DimensionConverter) { private val dimensionConverter: DimensionConverter) {
@Parcelize @Parcelize
data class Data( data class Data(
val eventId: String, override val eventId: String,
val filename: String, override val filename: String,
val mimeType: String?, override val mimeType: String?,
val url: String?, override val url: String?,
val elementToDecrypt: ElementToDecrypt?, override val elementToDecrypt: ElementToDecrypt?,
val height: Int?, val height: Int?,
val maxHeight: Int, val maxHeight: Int,
val width: Int?, val width: Int?,
val maxWidth: Int val maxWidth: Int
) : Parcelable { ) : AttachmentData {
fun isLocalFile() = url.isLocalFile() fun isLocalFile() = url.isLocalFile()
} }

View File

@ -19,9 +19,18 @@ package im.vector.riotx.features.media
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.View 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.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.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.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.MessageWithAttachmentContent
import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.AnimatedImageViewHolder
import im.vector.riotx.attachmentviewer.AttachmentInfo import im.vector.riotx.attachmentviewer.AttachmentInfo
import im.vector.riotx.attachmentviewer.AttachmentSourceProvider import im.vector.riotx.attachmentviewer.AttachmentSourceProvider
import im.vector.riotx.attachmentviewer.VideoViewHolder
import im.vector.riotx.attachmentviewer.ZoomableImageViewHolder import im.vector.riotx.attachmentviewer.ZoomableImageViewHolder
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import java.io.File
import javax.inject.Inject import javax.inject.Inject
class RoomAttachmentProvider( class RoomAttachmentProvider(
private val attachments: List<TimelineEvent>, private val attachments: List<TimelineEvent>,
private val initialIndex: Int, private val initialIndex: Int,
private val imageContentRenderer: ImageContentRenderer, private val imageContentRenderer: ImageContentRenderer,
private val dateFormatter: VectorDateFormatter private val videoContentRenderer: VideoContentRenderer,
private val dateFormatter: VectorDateFormatter,
private val fileService: FileService
) : AttachmentSourceProvider { ) : AttachmentSourceProvider {
interface InteractionListener { interface InteractionListener {
@ -57,28 +70,66 @@ class RoomAttachmentProvider(
override fun getAttachmentInfoAt(position: Int): AttachmentInfo { override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
return attachments[position].let { return attachments[position].let {
val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent
if (content is MessageImageContent) {
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
eventId = it.eventId, eventId = it.eventId,
filename = content?.body ?: "", filename = content.body,
mimeType = content?.mimeType, mimeType = content.mimeType,
url = content?.getFileUrl(), url = content.getFileUrl(),
elementToDecrypt = content?.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
maxHeight = -1, maxHeight = -1,
maxWidth = -1, maxWidth = -1,
width = null, width = null,
height = null height = null
) )
if (content?.mimeType == "image/gif") { if (content.mimeType == "image/gif") {
AttachmentInfo.AnimatedImage( AttachmentInfo.AnimatedImage(
content.url ?: "", content.url ?: "",
data data
) )
} else { } else {
AttachmentInfo.Image( AttachmentInfo.Image(
content?.url ?: "", content.url ?: "",
data 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(
"",
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<ImageView, Drawable>(holder.thumbnailImage) {
override fun onLoadFailed(errorDrawable: Drawable?) {
holder.thumbnailImage.setImageDrawable(errorDrawable)
}
override fun onResourceCleared(placeholder: Drawable?) {
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
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<File> {
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? { override fun overlayViewAtPosition(context: Context, position: Int): View? {
if (overlayView == null) { if (overlayView == null) {
overlayView = AttachmentOverlayView(context) overlayView = AttachmentOverlayView(context)
@ -109,22 +203,19 @@ class RoomAttachmentProvider(
"${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} " "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
} }
overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString") overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString")
overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage()
return overlayView 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( class RoomAttachmentProviderFactory @Inject constructor(
private val imageContentRenderer: ImageContentRenderer, private val imageContentRenderer: ImageContentRenderer,
private val vectorDateFormatter: VectorDateFormatter private val vectorDateFormatter: VectorDateFormatter,
private val videoContentRenderer: VideoContentRenderer,
private val session: Session
) { ) {
fun createProvider(attachments: List<TimelineEvent>, initialIndex: Int): RoomAttachmentProvider { fun createProvider(attachments: List<TimelineEvent>, initialIndex: Int): RoomAttachmentProvider {
return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter) return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, videoContentRenderer, vectorDateFormatter, session.fileService())
} }
} }

View File

@ -80,7 +80,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen
val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() } val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() }
val room = args.roomId?.let { session.getRoom(it) } 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 } val index = events.indexOfFirst { it.eventId == args.eventId }
initialIndex = index initialIndex = index
@ -90,8 +90,8 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen
transitionImageContainer.isVisible = true transitionImageContainer.isVisible = true
// Postpone transaction a bit until thumbnail is loaded // Postpone transaction a bit until thumbnail is loaded
val mediaData: ImageContentRenderer.Data? = intent.getParcelableExtra(EXTRA_IMAGE_DATA) val mediaData: Parcelable? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
if (mediaData != null) { if (mediaData is ImageContentRenderer.Data) {
// will be shown at end of transition // will be shown at end of transition
pager2.isInvisible = true pager2.isInvisible = true
supportPostponeEnterTransition() supportPostponeEnterTransition()
@ -99,6 +99,14 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen
// Proceed with transaction // Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView) 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) setSourceProvider(sourceProvider)
if (savedInstanceState == null) { if (savedInstanceState == null) {
pager2.setCurrentItem(index, false) 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) 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" const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
fun newIntent(context: Context, fun newIntent(context: Context,
mediaData: ImageContentRenderer.Data, mediaData: AttachmentData,
roomId: String?, roomId: String?,
eventId: String, eventId: String,
sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also { sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also {

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.media package im.vector.riotx.features.media
import android.os.Parcelable
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
@ -38,13 +37,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
@Parcelize @Parcelize
data class Data( data class Data(
val eventId: String, override val eventId: String,
val filename: String, override val filename: String,
val mimeType: String?, override val mimeType: String?,
val url: String?, override val url: String?,
val elementToDecrypt: ElementToDecrypt?, override val elementToDecrypt: ElementToDecrypt?,
val thumbnailMediaData: ImageContentRenderer.Data val thumbnailMediaData: ImageContentRenderer.Data
) : Parcelable ) : AttachmentData
fun render(data: Data, fun render(data: Data,
thumbnailView: ImageView, thumbnailView: ImageView,

View File

@ -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.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity 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.BigImageViewerActivity
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VectorAttachmentViewerActivity import im.vector.riotx.features.media.VectorAttachmentViewerActivity
import im.vector.riotx.features.media.VideoContentRenderer 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.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@ -248,7 +247,7 @@ class DefaultNavigator @Inject constructor(
override fun openImageViewer(activity: Activity, override fun openImageViewer(activity: Activity,
roomId: String?, roomId: String?,
mediaData: ImageContentRenderer.Data, mediaData: AttachmentData,
view: View, view: View,
options: ((MutableList<Pair<View, String>>) -> Unit)?) { options: ((MutableList<Pair<View, String>>) -> Unit)?) {
VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent -> VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent ->
@ -284,9 +283,28 @@ class DefaultNavigator @Inject constructor(
// activity.startActivity(intent, bundle) // activity.startActivity(intent, bundle)
} }
override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) { override fun openVideoViewer(activity: Activity,
val intent = VideoMediaViewerActivity.newIntent(activity, mediaData) roomId: String?, mediaData: VideoContentRenderer.Data,
activity.startActivity(intent) view: View,
options: ((MutableList<Pair<View, String>>) -> 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<Pair<View, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
activity.window.decorView.findViewById<View>(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) { private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {

View File

@ -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.util.MatrixItem
import im.vector.matrix.android.api.session.widgets.model.Widget 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.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.media.VideoContentRenderer
import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
@ -93,9 +93,12 @@ interface Navigator {
fun openImageViewer(activity: Activity, fun openImageViewer(activity: Activity,
roomId: String?, roomId: String?,
mediaData: ImageContentRenderer.Data, mediaData: AttachmentData,
view: View, view: View,
options: ((MutableList<Pair<View, String>>) -> Unit)?) options: ((MutableList<Pair<View, String>>) -> Unit)?)
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) fun openVideoViewer(activity: Activity,
roomId: String?, mediaData: VideoContentRenderer.Data,
view: View,
options: ((MutableList<Pair<View, String>>) -> Unit)?)
} }

View File

@ -81,7 +81,8 @@ class RoomUploadsMediaFragment @Inject constructor(
} }
override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) { override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) {
navigator.openVideoViewer(requireActivity(), mediaData) // TODO
// navigator.openVideoViewer(requireActivity(), mediaData, null, )
} }
override fun loadMore() { override fun loadMore() {

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

@ -10,7 +10,8 @@
android:id="@+id/overlayTopBackground" android:id="@+id/overlayTopBackground"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="60dp" android:layout_height="60dp"
android:background="@color/black_alpha"> android:background="@color/black_alpha"
app:layout_constraintTop_toTopOf="parent">
</View> </View>
@ -21,12 +22,12 @@
android:layout_height="44dp" android:layout_height="44dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:scaleType="centerInside"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/share" android:contentDescription="@string/share"
android:focusable="true"
android:padding="6dp" android:padding="6dp"
android:scaleType="centerInside"
android:tint="@color/white" android:tint="@color/white"
app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground" app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground"
app:layout_constraintStart_toStartOf="@id/overlayTopBackground" app:layout_constraintStart_toStartOf="@id/overlayTopBackground"
@ -72,6 +73,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/share" android:contentDescription="@string/share"
android:padding="6dp" android:padding="6dp"
android:tint="@color/white" android:tint="@color/white"
@ -80,4 +83,51 @@
app:layout_constraintTop_toTopOf="@id/overlayTopBackground" app:layout_constraintTop_toTopOf="@id/overlayTopBackground"
app:srcCompat="@drawable/ic_share" /> app:srcCompat="@drawable/ic_share" />
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/overlayVideoControlsGroup"
android:visibility="gone"
tools:visibility="visible"
app:constraint_referenced_ids="overlayBottomBackground,overlayBackButton,overlayPlayPauseButton,overlaySeekBar" />
<View
android:id="@+id/overlayBottomBackground"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/black_alpha"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageView
android:id="@+id/overlayPlayPauseButton"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/play_video"
android:focusable="true"
android:padding="6dp"
android:scaleType="centerInside"
android:tint="@color/white"
app:layout_constraintBottom_toBottomOf="@id/overlayBottomBackground"
app:layout_constraintStart_toStartOf="@id/overlayBottomBackground"
app:layout_constraintTop_toTopOf="@id/overlayBottomBackground"
app:srcCompat="@drawable/ic_play_arrow" />
<SeekBar
android:id="@+id/overlaySeekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/white"
android:progressBackgroundTint="@color/white"
android:thumbTint="@color/white"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@id/overlayBottomBackground"
app:layout_constraintEnd_toEndOf="@id/overlayBottomBackground"
app:layout_constraintStart_toEndOf="@id/overlayPlayPauseButton"
app:layout_constraintTop_toTopOf="@id/overlayBottomBackground" />
</merge> </merge>

View File

@ -76,6 +76,10 @@
<string name="disconnect">Disconnect</string> <string name="disconnect">Disconnect</string>
<string name="report_content">Report content</string> <string name="report_content">Report content</string>
<string name="active_call">Active call</string> <string name="active_call">Active call</string>
<string name="play_video">Play</string>
<string name="pause_video">Pause</string>
<!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video --> <!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video -->
<string name="ongoing_conference_call">Ongoing conference call.\nJoin as %1$s or %2$s</string> <string name="ongoing_conference_call">Ongoing conference call.\nJoin as %1$s or %2$s</string>
<string name="ongoing_conference_call_voice">Voice</string> <string name="ongoing_conference_call_voice">Voice</string>