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_resume_voice_broadcast_record">Resume voice broadcast record</string>
<string name="a11y_pause_voice_broadcast_record">Pause 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_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">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> <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.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.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.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
@ -42,6 +43,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val drawableProvider: DrawableProvider, private val drawableProvider: DrawableProvider,
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
) { ) {
fun create( fun create(
@ -53,7 +55,8 @@ class VoiceBroadcastItemFactory @Inject constructor(
): VectorEpoxyModel<out VectorEpoxyHolder>? { ): VectorEpoxyModel<out VectorEpoxyHolder>? {
// Only display item of the initial event with updated data // Only display item of the initial event with updated data
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null 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 mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent()
val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent() val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent()
val mostRecentMessageContent = mostRecentEvent?.content ?: return null val mostRecentMessageContent = mostRecentEvent?.content ?: return null
@ -61,7 +64,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
return if (isRecording) { return if (isRecording) {
createRecordingItem(params.event.roomId, highlight, callback, attributes) createRecordingItem(params.event.roomId, highlight, callback, attributes)
} else { } 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( private fun createListeningItem(
roomId: String, roomId: String,
voiceBroadcastId: String,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
@ -96,7 +100,8 @@ class VoiceBroadcastItemFactory @Inject constructor(
.roomItem(roomSummary?.toMatrixItem()) .roomItem(roomSummary?.toMatrixItem())
.colorProvider(colorProvider) .colorProvider(colorProvider)
.drawableProvider(drawableProvider) .drawableProvider(drawableProvider)
.voiceBroadcastRecorder(voiceBroadcastRecorder) .voiceBroadcastPlayer(voiceBroadcastPlayer)
.voiceBroadcastId(voiceBroadcastId)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback) .callback(callback)
} }

View File

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

View File

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

View File

@ -65,29 +65,27 @@
app:constraint_referenced_ids="roomAvatarImageView,titleText" /> app:constraint_referenced_ids="roomAvatarImageView,titleText" />
<ImageButton <ImageButton
android:id="@+id/recordButton" android:id="@+id/playPauseButton"
android:layout_width="@dimen/voice_broadcast_controller_button_size" android:layout_width="@dimen/voice_broadcast_controller_button_size"
android:layout_height="@dimen/voice_broadcast_controller_button_size" android:layout_height="@dimen/voice_broadcast_controller_button_size"
android:background="@drawable/bg_rounded_button" android:background="@drawable/bg_rounded_button"
android:backgroundTint="?vctr_system" android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_resume_voice_broadcast_record" android:contentDescription="@string/a11y_play_voice_broadcast"
android:src="@drawable/ic_recording_dot" android:src="@drawable/ic_play_pause_play"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/stopRecordButton" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="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" /> 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> </androidx.constraintlayout.widget.ConstraintLayout>