Basic Video Support
This commit is contained in:
parent
cc5df1e1d5
commit
a1db8653ab
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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?
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<BaseViewHolder>() {
|
|||
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<BaseViewHolder>() {
|
|||
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) {
|
||||
|
|
|
@ -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", "")
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoThumbnailImage"
|
||||
|
@ -14,13 +15,35 @@
|
|||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center" />
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoControlIcon"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_width="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>
|
||||
|
|
|
@ -40,5 +40,5 @@ interface TimelineService {
|
|||
|
||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||
|
||||
fun getAttachementMessages() : List<TimelineEvent>
|
||||
fun getAttachmentMessages() : List<TimelineEvent>
|
||||
}
|
||||
|
|
|
@ -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<TimelineEvent> {
|
||||
override fun getAttachmentMessages(): List<TimelineEvent> {
|
||||
// TODO pretty bad query.. maybe we should denormalize clear type in base?
|
||||
return doWithRealm(monarchy.realmConfiguration) { realm ->
|
||||
realm.where<TimelineEventEntity>()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<ImageView>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<TimelineEvent>,
|
||||
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,28 +70,66 @@ class RoomAttachmentProvider(
|
|||
override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
|
||||
return attachments[position].let {
|
||||
val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent
|
||||
if (content is MessageImageContent) {
|
||||
val data = ImageContentRenderer.Data(
|
||||
eventId = it.eventId,
|
||||
filename = content?.body ?: "",
|
||||
mimeType = content?.mimeType,
|
||||
url = content?.getFileUrl(),
|
||||
elementToDecrypt = content?.encryptedFileInfo?.toElementToDecrypt(),
|
||||
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") {
|
||||
if (content.mimeType == "image/gif") {
|
||||
AttachmentInfo.AnimatedImage(
|
||||
content.url ?: "",
|
||||
data
|
||||
)
|
||||
} else {
|
||||
AttachmentInfo.Image(
|
||||
content?.url ?: "",
|
||||
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(
|
||||
"",
|
||||
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? {
|
||||
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<TimelineEvent>, initialIndex: Int): RoomAttachmentProvider {
|
||||
return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter)
|
||||
return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, videoContentRenderer, vectorDateFormatter, session.fileService())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Pair<View, String>>) -> 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<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) {
|
||||
|
|
|
@ -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<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)?)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">
|
||||
|
||||
</View>
|
||||
|
||||
|
@ -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" />
|
||||
|
||||
|
||||
<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>
|
||||
|
|
|
@ -76,6 +76,10 @@
|
|||
<string name="disconnect">Disconnect</string>
|
||||
<string name="report_content">Report content</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 -->
|
||||
<string name="ongoing_conference_call">Ongoing conference call.\nJoin as %1$s or %2$s</string>
|
||||
<string name="ongoing_conference_call_voice">Voice</string>
|
||||
|
|
Loading…
Reference in New Issue