diff --git a/changelog.d/7421.wip b/changelog.d/7421.wip
new file mode 100644
index 0000000000..4a399eee04
--- /dev/null
+++ b/changelog.d/7421.wip
@@ -0,0 +1 @@
+[Voice Broadcast] Improve rendering in the timeline
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index e6714005a1..ea9b4b5999 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3078,6 +3078,14 @@
%1$s (%2$s)
(%1$s)
+ Live
+ Resume voice broadcast record
+ Pause voice broadcast record
+ Stop voice broadcast record
+ Play or resume voice broadcast
+ Pause voice broadcast
+ Buffering
+
Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.
Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.
diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml
index 52d16eae7d..50d5aaf014 100644
--- a/library/ui-styles/src/main/res/values/dimens.xml
+++ b/library/ui-styles/src/main/res/values/dimens.xml
@@ -73,6 +73,9 @@
12dp
22dp
+
+ 48dp
+
112dp
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index cb947a67ce..245d92f95b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -201,7 +201,7 @@ class MessageItemFactory @Inject constructor(
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
- is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(messageContent, params.eventsGroup, highlight, callback, attributes)
+ is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
return messageItem?.apply {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
index f2dfb020a1..5dc601a91a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
@@ -15,46 +15,116 @@
*/
package im.vector.app.features.home.room.detail.timeline.factory
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.DrawableProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
-import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
-import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
-import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastItem
-import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastItem_
+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.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
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.getUser
+import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class VoiceBroadcastItemFactory @Inject constructor(
private val session: Session,
private val avatarSizeProvider: AvatarSizeProvider,
- private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
+ private val colorProvider: ColorProvider,
+ private val drawableProvider: DrawableProvider,
+ private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
+ private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
) {
fun create(
+ params: TimelineItemFactoryParams,
messageContent: MessageVoiceBroadcastInfoContent,
- eventsGroup: TimelineEventsGroup?,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
- ): MessageVoiceBroadcastItem? {
+ ): VectorEpoxyModel? {
// Only display item of the initial event with updated data
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
- val voiceBroadcastEventsGroup = 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
val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId
- return MessageVoiceBroadcastItem_()
+ val recorderName = mostRecentTimelineEvent.root.stateKey?.let { session.getUser(it) }?.displayName ?: mostRecentTimelineEvent.root.stateKey
+ return if (isRecording) {
+ createRecordingItem(
+ params.event.roomId,
+ eventsGroup.groupId,
+ highlight,
+ callback,
+ attributes
+ )
+ } else {
+ createListeningItem(
+ params.event.roomId,
+ eventsGroup.groupId,
+ mostRecentMessageContent.voiceBroadcastState,
+ recorderName,
+ highlight,
+ callback,
+ attributes
+ )
+ }
+ }
+
+ private fun createRecordingItem(
+ roomId: String,
+ voiceBroadcastId: String,
+ highlight: Boolean,
+ callback: TimelineEventController.Callback?,
+ attributes: AbsMessageItem.Attributes,
+ ): MessageVoiceBroadcastRecordingItem {
+ val roomSummary = session.getRoom(roomId)?.roomSummary()
+ return MessageVoiceBroadcastRecordingItem_()
+ .id("voice_broadcast_$voiceBroadcastId")
.attributes(attributes)
.highlighted(highlight)
- .voiceBroadcastState(mostRecentMessageContent.voiceBroadcastState)
- .recording(isRecording)
- .audioMessagePlaybackTracker(audioMessagePlaybackTracker)
+ .roomItem(roomSummary?.toMatrixItem())
+ .colorProvider(colorProvider)
+ .drawableProvider(drawableProvider)
+ .voiceBroadcastRecorder(voiceBroadcastRecorder)
+ .leftGuideline(avatarSizeProvider.leftGuideline)
+ .callback(callback)
+ }
+
+ private fun createListeningItem(
+ roomId: String,
+ voiceBroadcastId: String,
+ voiceBroadcastState: VoiceBroadcastState?,
+ broadcasterName: String?,
+ highlight: Boolean,
+ callback: TimelineEventController.Callback?,
+ attributes: AbsMessageItem.Attributes,
+ ): MessageVoiceBroadcastListeningItem {
+ val roomSummary = session.getRoom(roomId)?.roomSummary()
+ return MessageVoiceBroadcastListeningItem_()
+ .id("voice_broadcast_$voiceBroadcastId")
+ .attributes(attributes)
+ .highlighted(highlight)
+ .roomItem(roomSummary?.toMatrixItem())
+ .colorProvider(colorProvider)
+ .drawableProvider(drawableProvider)
+ .voiceBroadcastPlayer(voiceBroadcastPlayer)
+ .voiceBroadcastId(voiceBroadcastId)
+ .voiceBroadcastState(voiceBroadcastState)
+ .broadcasterName(broadcasterName)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastItem.kt
deleted file mode 100644
index 1927024a36..0000000000
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastItem.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (c) 2022 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.app.features.home.room.detail.timeline.item
-
-import android.annotation.SuppressLint
-import android.widget.ImageButton
-import android.widget.TextView
-import com.airbnb.epoxy.EpoxyAttribute
-import com.airbnb.epoxy.EpoxyModelClass
-import im.vector.app.R
-import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
-import im.vector.app.features.home.room.detail.timeline.TimelineEventController
-import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
-import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
-import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
-
-@EpoxyModelClass
-abstract class MessageVoiceBroadcastItem : AbsMessageItem() {
-
- @EpoxyAttribute
- var callback: TimelineEventController.Callback? = null
-
- @EpoxyAttribute
- var voiceBroadcastState: VoiceBroadcastState? = null
-
- @EpoxyAttribute
- var recording: Boolean = false
-
- @EpoxyAttribute
- lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
-
- private val voiceBroadcastEventId
- get() = attributes.informationData.eventId
-
- override fun isCacheable(): Boolean = false
-
- override fun bind(holder: Holder) {
- super.bind(holder)
- bindVoiceBroadcastItem(holder)
- }
-
- @SuppressLint("SetTextI18n") // Temporary text
- private fun bindVoiceBroadcastItem(holder: Holder) {
- holder.currentStateText.text = "Voice Broadcast state: ${voiceBroadcastState?.value ?: "None"}"
- if (recording) {
- renderRecording(holder)
- } else {
- renderListening(holder)
- }
- }
-
- private fun renderListening(holder: Holder) {
- audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
- override fun onUpdate(state: State) {
- holder.playButton.isEnabled = state !is State.Playing
- holder.pauseButton.isEnabled = state is State.Playing
- holder.stopButton.isEnabled = state !is State.Idle
- }
- })
- holder.playButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastEventId)) }
- holder.pauseButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) }
- holder.stopButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Stop) }
- }
-
- private fun renderRecording(holder: Holder) {
- with(holder) {
- playButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.PAUSED
- pauseButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.STARTED || voiceBroadcastState == VoiceBroadcastState.RESUMED
- stopButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.STARTED ||
- voiceBroadcastState == VoiceBroadcastState.RESUMED ||
- voiceBroadcastState == VoiceBroadcastState.PAUSED
- playButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
- pauseButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
- stopButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
- }
- }
-
- override fun getViewStubId() = STUB_ID
-
- class Holder : AbsMessageLocationItem.Holder(STUB_ID) {
- val currentStateText by bind(R.id.currentStateText)
- val playButton by bind(R.id.playButton)
- val pauseButton by bind(R.id.pauseButton)
- val stopButton by bind(R.id.stopButton)
- }
-
- companion object {
- private val STUB_ID = R.id.messageVoiceBroadcastStub
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
new file mode 100644
index 0000000000..5b58dda4e6
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2022 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.app.features.home.room.detail.timeline.item
+
+import android.view.View
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+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.epoxy.onClick
+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
+import im.vector.app.features.home.room.detail.timeline.TimelineEventController
+import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import org.matrix.android.sdk.api.util.MatrixItem
+
+@EpoxyModelClass
+abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem() {
+
+ @EpoxyAttribute
+ var callback: TimelineEventController.Callback? = null
+
+ @EpoxyAttribute
+ var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null
+
+ @EpoxyAttribute
+ lateinit var voiceBroadcastId: String
+
+ @EpoxyAttribute
+ var voiceBroadcastState: VoiceBroadcastState? = null
+
+ @EpoxyAttribute
+ var broadcasterName: String? = null
+
+ @EpoxyAttribute
+ lateinit var colorProvider: ColorProvider
+
+ @EpoxyAttribute
+ lateinit var drawableProvider: DrawableProvider
+
+ @EpoxyAttribute
+ var roomItem: MatrixItem? = null
+
+ @EpoxyAttribute
+ var title: String? = null
+
+ private lateinit var playerListener: VoiceBroadcastPlayer.Listener
+
+ override fun isCacheable(): Boolean = false
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ bindVoiceBroadcastItem(holder)
+ }
+
+ private fun bindVoiceBroadcastItem(holder: Holder) {
+ playerListener = VoiceBroadcastPlayer.Listener { state ->
+ renderState(holder, state)
+ }
+ voiceBroadcastPlayer?.addListener(playerListener)
+ renderHeader(holder)
+ renderLiveIcon(holder)
+ }
+
+ private fun renderHeader(holder: Holder) {
+ with(holder) {
+ roomItem?.let {
+ attributes.avatarRenderer.render(it, roomAvatarImageView)
+ titleText.text = it.displayName
+ }
+ broadcasterNameText.text = broadcasterName
+ }
+ }
+
+ private fun renderLiveIcon(holder: Holder) {
+ with(holder) {
+ when (voiceBroadcastState) {
+ VoiceBroadcastState.STARTED,
+ VoiceBroadcastState.RESUMED -> {
+ liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
+ liveIndicator.isVisible = true
+ }
+ VoiceBroadcastState.PAUSED -> {
+ liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
+ liveIndicator.isVisible = true
+ }
+ VoiceBroadcastState.STOPPED, null -> {
+ liveIndicator.isVisible = false
+ }
+ }
+ }
+ }
+
+ private fun renderState(holder: Holder, state: VoiceBroadcastPlayer.State) {
+ if (isCurrentMediaActive()) {
+ renderActiveMedia(holder, state)
+ } else {
+ renderInactiveMedia(holder)
+ }
+ }
+
+ private fun renderActiveMedia(holder: Holder, state: VoiceBroadcastPlayer.State) {
+ with(holder) {
+ bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
+ playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
+
+ when (state) {
+ VoiceBroadcastPlayer.State.PLAYING -> {
+ playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
+ playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
+ playPauseButton.onClick { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
+ }
+ 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.onClick {
+ attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
+ }
+ }
+ VoiceBroadcastPlayer.State.BUFFERING -> Unit
+ }
+ }
+ }
+
+ private fun renderInactiveMedia(holder: Holder) {
+ with(holder) {
+ 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.onClick {
+ attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
+ }
+ }
+ }
+
+ private fun isCurrentMediaActive() = voiceBroadcastPlayer?.currentVoiceBroadcastId == voiceBroadcastId
+
+ override fun unbind(holder: Holder) {
+ super.unbind(holder)
+ voiceBroadcastPlayer?.removeListener(playerListener)
+ }
+
+ override fun getViewStubId() = STUB_ID
+
+ class Holder : AbsMessageItem.Holder(STUB_ID) {
+ val liveIndicator by bind(R.id.liveIndicator)
+ val roomAvatarImageView by bind(R.id.roomAvatarImageView)
+ val titleText by bind(R.id.titleText)
+ val playPauseButton by bind(R.id.playPauseButton)
+ val bufferingView by bind(R.id.bufferingView)
+ val broadcasterNameText by bind(R.id.broadcasterNameText)
+ }
+
+ companion object {
+ private val STUB_ID = R.id.messageVoiceBroadcastListeningStub
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
new file mode 100644
index 0000000000..c417053b2a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2022 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.app.features.home.room.detail.timeline.item
+
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+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.epoxy.onClick
+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.timeline.TimelineEventController
+import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
+import org.matrix.android.sdk.api.util.MatrixItem
+
+@EpoxyModelClass
+abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem() {
+
+ @EpoxyAttribute
+ var callback: TimelineEventController.Callback? = null
+
+ @EpoxyAttribute
+ var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null
+
+ @EpoxyAttribute
+ lateinit var colorProvider: ColorProvider
+
+ @EpoxyAttribute
+ lateinit var drawableProvider: DrawableProvider
+
+ @EpoxyAttribute
+ var roomItem: MatrixItem? = null
+
+ @EpoxyAttribute
+ var title: String? = null
+
+ private lateinit var recorderListener: VoiceBroadcastRecorder.Listener
+
+ override fun isCacheable(): Boolean = false
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ bindVoiceBroadcastItem(holder)
+ }
+
+ private fun bindVoiceBroadcastItem(holder: Holder) {
+ recorderListener = object : VoiceBroadcastRecorder.Listener {
+ override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
+ renderState(holder, state)
+ }
+ }
+ voiceBroadcastRecorder?.addListener(recorderListener)
+ renderHeader(holder)
+ }
+
+ private fun renderHeader(holder: Holder) {
+ with(holder) {
+ roomItem?.let {
+ attributes.avatarRenderer.render(it, roomAvatarImageView)
+ titleText.text = it.displayName
+ }
+ }
+ }
+
+ private fun renderState(holder: Holder, state: VoiceBroadcastRecorder.State) {
+ with(holder) {
+ when (state) {
+ VoiceBroadcastRecorder.State.Recording -> {
+ stopRecordButton.isEnabled = true
+ recordButton.isEnabled = true
+
+ liveIndicator.isVisible = true
+ liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
+
+ 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.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
+ stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
+ }
+ VoiceBroadcastRecorder.State.Paused -> {
+ stopRecordButton.isEnabled = true
+ recordButton.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.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
+ stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
+ }
+ VoiceBroadcastRecorder.State.Idle -> {
+ recordButton.isEnabled = false
+ stopRecordButton.isEnabled = false
+ liveIndicator.isVisible = false
+ }
+ }
+ }
+ }
+
+ override fun unbind(holder: Holder) {
+ super.unbind(holder)
+ voiceBroadcastRecorder?.removeListener(recorderListener)
+ }
+
+ override fun getViewStubId() = STUB_ID
+
+ class Holder : AbsMessageItem.Holder(STUB_ID) {
+ val liveIndicator by bind(R.id.liveIndicator)
+ val roomAvatarImageView by bind(R.id.roomAvatarImageView)
+ val titleText by bind(R.id.titleText)
+ val recordButton by bind(R.id.recordButton)
+ val stopRecordButton by bind(R.id.stopRecordButton)
+ }
+
+ companion object {
+ private val STUB_ID = R.id.messageVoiceBroadcastRecordingStub
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt
index 62252570c6..2c892c8306 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt
@@ -18,6 +18,7 @@ package im.vector.app.features.voicebroadcast
import android.media.AudioAttributes
import android.media.MediaPlayer
+import androidx.annotation.MainThread
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.voice.VoiceFailure
@@ -29,8 +30,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.events.model.RelationType
-import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
@@ -40,6 +41,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import timber.log.Timber
+import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
import javax.inject.Singleton
@@ -73,15 +75,17 @@ class VoiceBroadcastPlayer @Inject constructor(
private var currentSequence: Int? = null
private var playlist = emptyList()
- private val currentVoiceBroadcastId
- get() = playlist.firstOrNull()?.root?.getRelationContent()?.eventId
+ var currentVoiceBroadcastId: String? = null
private var state: State = State.IDLE
+ @MainThread
set(value) {
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
field = value
+ listeners.forEach { it.onStateChanged(value) }
}
private var currentRoomId: String? = null
+ private var listeners = CopyOnWriteArrayList()
fun playOrResume(roomId: String, eventId: String) {
val hasChanged = currentVoiceBroadcastId != eventId
@@ -126,15 +130,26 @@ class VoiceBroadcastPlayer @Inject constructor(
playlist = emptyList()
currentSequence = null
currentRoomId = null
+ currentVoiceBroadcastId = 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
-
// Stop listening previous voice broadcast if any
if (state != State.IDLE) stop()
+ currentRoomId = roomId
+ currentVoiceBroadcastId = eventId
+
state = State.BUFFERING
val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
@@ -157,7 +172,7 @@ class VoiceBroadcastPlayer @Inject constructor(
currentMediaPlayer?.start()
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
currentSequence = sequence
- state = State.PLAYING
+ withContext(Dispatchers.Main) { state = State.PLAYING }
nextMediaPlayer = prepareNextMediaPlayer()
} catch (failure: Throwable) {
Timber.e(failure, "Unable to start playback")
@@ -316,4 +331,8 @@ class VoiceBroadcastPlayer @Inject constructor(
BUFFERING,
IDLE
}
+
+ fun interface Listener {
+ fun onStateChanged(state: State)
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt
index c9bb0c5f54..8b69051823 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt
@@ -22,12 +22,21 @@ import java.io.File
interface VoiceBroadcastRecorder : VoiceRecorder {
- var listener: Listener?
- var currentSequence: Int
+ val currentSequence: Int
+ val state: State
fun startRecord(roomId: String, chunkLength: Int)
+ fun addListener(listener: Listener)
+ fun removeListener(listener: Listener)
- fun interface Listener {
- fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int)
+ interface Listener {
+ fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int) = Unit
+ fun onStateUpdated(state: State) = Unit
+ }
+
+ enum class State {
+ Recording,
+ Paused,
+ Idle,
}
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt
index a65aae6f8a..5285dc5e3b 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt
@@ -23,6 +23,7 @@ import androidx.annotation.RequiresApi
import im.vector.app.features.voice.AbstractVoiceRecorderQ
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
+import java.util.concurrent.CopyOnWriteArrayList
@RequiresApi(Build.VERSION_CODES.Q)
class VoiceBroadcastRecorderQ(
@@ -32,8 +33,13 @@ class VoiceBroadcastRecorderQ(
private var maxFileSize = 0L // zero or negative for no limit
private var currentRoomId: String? = null
override var currentSequence = 0
+ override var state = VoiceBroadcastRecorder.State.Idle
+ set(value) {
+ field = value
+ listeners.forEach { it.onStateUpdated(value) }
+ }
- override var listener: VoiceBroadcastRecorder.Listener? = null
+ private val listeners = CopyOnWriteArrayList()
override val outputFormat = MediaRecorder.OutputFormat.MPEG_4
override val audioEncoder = MediaRecorder.AudioEncoder.HE_AAC
@@ -57,24 +63,28 @@ class VoiceBroadcastRecorderQ(
maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong()
currentSequence = 1
startRecord(roomId)
+ state = VoiceBroadcastRecorder.State.Recording
}
override fun pauseRecord() {
tryOrNull { mediaRecorder?.stop() }
mediaRecorder?.reset()
notifyOutputFileCreated()
+ state = VoiceBroadcastRecorder.State.Paused
}
override fun resumeRecord() {
currentSequence++
currentRoomId?.let { startRecord(it) }
+ state = VoiceBroadcastRecorder.State.Recording
}
override fun stopRecord() {
super.stopRecord()
notifyOutputFileCreated()
- listener = null
+ listeners.clear()
currentSequence = 0
+ state = VoiceBroadcastRecorder.State.Idle
}
override fun release() {
@@ -82,6 +92,15 @@ class VoiceBroadcastRecorderQ(
super.release()
}
+ override fun addListener(listener: VoiceBroadcastRecorder.Listener) {
+ listeners.add(listener)
+ listener.onStateUpdated(state)
+ }
+
+ override fun removeListener(listener: VoiceBroadcastRecorder.Listener) {
+ listeners.remove(listener)
+ }
+
private fun onMaxFileSizeApproaching(roomId: String) {
setNextOutputFile(roomId)
}
@@ -92,8 +111,8 @@ class VoiceBroadcastRecorderQ(
}
private fun notifyOutputFileCreated() {
- outputFile?.let {
- listener?.onVoiceMessageCreated(it, currentSequence)
+ outputFile?.let { file ->
+ listeners.forEach { it.onVoiceMessageCreated(file, currentSequence) }
outputFile = nextOutputFile
nextOutputFile = null
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt
index d5d58f822e..7934d18e36 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt
@@ -81,9 +81,11 @@ class StartVoiceBroadcastUseCase @Inject constructor(
}
private fun startRecording(room: Room, eventId: String, chunkLength: Int) {
- voiceBroadcastRecorder?.listener = VoiceBroadcastRecorder.Listener { file, sequence ->
- sendVoiceFile(room, file, eventId, sequence)
- }
+ voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener {
+ override fun onVoiceMessageCreated(file: File, sequence: Int) {
+ sendVoiceFile(room, file, eventId, sequence)
+ }
+ })
voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength)
}
diff --git a/vector/src/main/res/drawable/ic_recording_dot.xml b/vector/src/main/res/drawable/ic_recording_dot.xml
new file mode 100644
index 0000000000..f5d92f9718
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_recording_dot.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_stop.xml b/vector/src/main/res/drawable/ic_stop.xml
new file mode 100644
index 0000000000..459a7cfce2
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_stop.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_voice_broadcast_16.xml b/vector/src/main/res/drawable/ic_voice_broadcast_16.xml
new file mode 100644
index 0000000000..7d427a56d0
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_voice_broadcast_16.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/rounded_rect_shape_2.xml b/vector/src/main/res/drawable/rounded_rect_shape_2.xml
new file mode 100644
index 0000000000..977de2fd09
--- /dev/null
+++ b/vector/src/main/res/drawable/rounded_rect_shape_2.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml
index 6fcf5711f7..643f4a89c8 100644
--- a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml
+++ b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml
@@ -48,10 +48,17 @@
tools:visibility="gone" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml
new file mode 100644
index 0000000000..e3bb85138d
--- /dev/null
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_stub.xml
deleted file mode 100644
index e35060f72a..0000000000
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_stub.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-