VoiceBroadcast - Listening view

This commit is contained in:
Florian Renaud 2022-10-20 19:07:34 +02:00
parent f1b4ebbc37
commit f711a0ea74
5 changed files with 98 additions and 62 deletions

View File

@ -3082,6 +3082,9 @@
<string name="a11y_resume_voice_broadcast_record">Resume voice broadcast record</string>
<string name="a11y_pause_voice_broadcast_record">Pause voice broadcast record</string>
<string name="a11y_stop_voice_broadcast_record">Stop voice broadcast record</string>
<string name="a11y_play_voice_broadcast">Play or resume voice broadcast</string>
<string name="a11y_pause_voice_broadcast">Pause voice broadcast</string>
<string name="a11y_voice_broadcast_buffering">Buffering</string>
<string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>
<string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>

View File

@ -27,6 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadca
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
@ -42,6 +43,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
private val colorProvider: ColorProvider,
private val drawableProvider: DrawableProvider,
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
) {
fun create(
@ -53,7 +55,8 @@ class VoiceBroadcastItemFactory @Inject constructor(
): VectorEpoxyModel<out VectorEpoxyHolder>? {
// Only display item of the initial event with updated data
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
val eventsGroup = params.eventsGroup ?: return null
val voiceBroadcastEventsGroup = VoiceBroadcastEventsGroup(eventsGroup)
val mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent()
val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent()
val mostRecentMessageContent = mostRecentEvent?.content ?: return null
@ -61,7 +64,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
return if (isRecording) {
createRecordingItem(params.event.roomId, highlight, callback, attributes)
} else {
createListeningItem(params.event.roomId, highlight, callback, attributes)
createListeningItem(params.event.roomId, eventsGroup.groupId, highlight, callback, attributes)
}
}
@ -85,6 +88,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
private fun createListeningItem(
roomId: String,
voiceBroadcastId: String,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
@ -96,7 +100,8 @@ class VoiceBroadcastItemFactory @Inject constructor(
.roomItem(roomSummary?.toMatrixItem())
.colorProvider(colorProvider)
.drawableProvider(drawableProvider)
.voiceBroadcastRecorder(voiceBroadcastRecorder)
.voiceBroadcastPlayer(voiceBroadcastPlayer)
.voiceBroadcastId(voiceBroadcastId)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}

View File

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
@ -23,12 +24,11 @@ import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.extensions.tintBackground
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass
@ -38,7 +38,10 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
var callback: TimelineEventController.Callback? = null
@EpoxyAttribute
var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null
var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null
@EpoxyAttribute
lateinit var voiceBroadcastId: String
@EpoxyAttribute
lateinit var colorProvider: ColorProvider
@ -52,7 +55,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
@EpoxyAttribute
var title: String? = null
private lateinit var recorderListener: VoiceBroadcastRecorder.Listener
private lateinit var playerListener: VoiceBroadcastPlayer.Listener
override fun isCacheable(): Boolean = false
@ -62,12 +65,10 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
}
private fun bindVoiceBroadcastItem(holder: Holder) {
recorderListener = object : VoiceBroadcastRecorder.Listener {
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
renderState(holder, state)
}
playerListener = VoiceBroadcastPlayer.Listener { state ->
renderState(holder, state)
}
voiceBroadcastRecorder?.addListener(recorderListener)
voiceBroadcastPlayer?.addListener(playerListener)
renderHeader(holder)
}
@ -80,45 +81,59 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
}
}
private fun renderState(holder: Holder, state: VoiceBroadcastRecorder.State) {
private fun renderState(holder: Holder, state: VoiceBroadcastPlayer.State) {
if (isCurrentMediaActive()) {
renderActiveMedia(holder, state)
} else {
renderInactiveMedia(holder)
}
}
@Suppress("UNUSED_PARAMETER")
private fun renderActiveMedia(holder: Holder, state: VoiceBroadcastPlayer.State) {
with(holder) {
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
liveIndicator.isVisible = false
// liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorOnError))
when (state) {
VoiceBroadcastRecorder.State.Recording -> {
stopRecordButton.isEnabled = true
liveIndicator.isVisible = true
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorOnError))
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
recordButton.setImageDrawable(drawable)
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record)
recordButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
stopRecordButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
VoiceBroadcastPlayer.State.PLAYING -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
playPauseButton.setOnClickListener { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
}
VoiceBroadcastRecorder.State.Paused -> {
stopRecordButton.isEnabled = true
liveIndicator.isVisible = true
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
recordButton.setImageResource(R.drawable.ic_recording_dot)
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
recordButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
stopRecordButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
}
VoiceBroadcastRecorder.State.Idle -> {
recordButton.isEnabled = false
stopRecordButton.isEnabled = false
liveIndicator.isVisible = false
VoiceBroadcastPlayer.State.IDLE,
VoiceBroadcastPlayer.State.PAUSED -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
playPauseButton.setOnClickListener {
attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
}
}
VoiceBroadcastPlayer.State.BUFFERING -> Unit
}
}
}
private fun renderInactiveMedia(holder: Holder) {
with(holder) {
liveIndicator.isVisible = false
bufferingView.isVisible = false
playPauseButton.isVisible = true
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
playPauseButton.setOnClickListener {
attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
}
}
}
private fun isCurrentMediaActive() = voiceBroadcastPlayer?.currentVoiceBroadcastId == voiceBroadcastId
override fun unbind(holder: Holder) {
super.unbind(holder)
voiceBroadcastRecorder?.removeListener(recorderListener)
voiceBroadcastPlayer?.removeListener(playerListener)
}
override fun getViewStubId() = STUB_ID
@ -127,8 +142,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
val liveIndicator by bind<TextView>(R.id.liveIndicator)
val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val titleText by bind<TextView>(R.id.titleText)
val recordButton by bind<ImageButton>(R.id.recordButton)
val stopRecordButton by bind<ImageButton>(R.id.stopRecordButton)
val playPauseButton by bind<ImageButton>(R.id.playPauseButton)
val bufferingView by bind<View>(R.id.bufferingView)
}
companion object {

View File

@ -73,15 +73,17 @@ class VoiceBroadcastPlayer @Inject constructor(
private var currentSequence: Int? = null
private var playlist = emptyList<MessageAudioEvent>()
private val currentVoiceBroadcastId
val currentVoiceBroadcastId
get() = playlist.firstOrNull()?.root?.getRelationContent()?.eventId
private var state: State = State.IDLE
set(value) {
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
field = value
listeners.forEach { it.onStateChanged(value) }
}
private var currentRoomId: String? = null
private var listeners = mutableListOf<Listener>()
fun playOrResume(roomId: String, eventId: String) {
val hasChanged = currentVoiceBroadcastId != eventId
@ -128,6 +130,15 @@ class VoiceBroadcastPlayer @Inject constructor(
currentRoomId = null
}
fun addListener(listener: Listener) {
listeners.add(listener)
listener.onStateChanged(state)
}
fun removeListener(listener: Listener) {
listeners.remove(listener)
}
private fun startPlayback(roomId: String, eventId: String) {
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
currentRoomId = roomId
@ -316,4 +327,8 @@ class VoiceBroadcastPlayer @Inject constructor(
BUFFERING,
IDLE
}
fun interface Listener {
fun onStateChanged(state: State)
}
}

View File

@ -65,29 +65,27 @@
app:constraint_referenced_ids="roomAvatarImageView,titleText" />
<ImageButton
android:id="@+id/recordButton"
android:id="@+id/playPauseButton"
android:layout_width="@dimen/voice_broadcast_controller_button_size"
android:layout_height="@dimen/voice_broadcast_controller_button_size"
android:background="@drawable/bg_rounded_button"
android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_resume_voice_broadcast_record"
android:src="@drawable/ic_recording_dot"
android:contentDescription="@string/a11y_play_voice_broadcast"
android:src="@drawable/ic_play_pause_play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/stopRecordButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier"
app:tint="?vctr_content_secondary" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/bufferingView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/a11y_voice_broadcast_buffering"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/playPauseButton"
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
<ImageButton
android:id="@+id/stopRecordButton"
android:layout_width="@dimen/voice_broadcast_controller_button_size"
android:layout_height="@dimen/voice_broadcast_controller_button_size"
android:background="@drawable/bg_rounded_button"
android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_stop_voice_broadcast_record"
android:src="@drawable/ic_stop"
app:layout_constraintBottom_toBottomOf="@id/recordButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/recordButton"
app:layout_constraintTop_toTopOf="@id/recordButton" />
</androidx.constraintlayout.widget.ConstraintLayout>