mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
VoiceBroadcast - Add recording view
This commit is contained in:
parent
0a9f2bfa0a
commit
4c71209573
14 changed files with 337 additions and 166 deletions
|
@ -3078,6 +3078,11 @@
|
||||||
<string name="audio_message_reply_content">%1$s (%2$s)</string>
|
<string name="audio_message_reply_content">%1$s (%2$s)</string>
|
||||||
<string name="audio_message_file_size">(%1$s)</string>
|
<string name="audio_message_file_size">(%1$s)</string>
|
||||||
|
|
||||||
|
<string name="voice_broadcast_live">Live</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_stop_voice_broadcast_record">Stop voice broadcast record</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. You’ll 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. You’ll 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. You’ll 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. You’ll be able to change this in room settings anytime.</string>
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,9 @@
|
||||||
<dimen name="location_sharing_live_duration_choice_margin_horizontal">12dp</dimen>
|
<dimen name="location_sharing_live_duration_choice_margin_horizontal">12dp</dimen>
|
||||||
<dimen name="location_sharing_live_duration_choice_margin_vertical">22dp</dimen>
|
<dimen name="location_sharing_live_duration_choice_margin_vertical">22dp</dimen>
|
||||||
|
|
||||||
|
<!-- Voice Broadcast -->
|
||||||
|
<dimen name="voice_broadcast_controller_button_size">48dp</dimen>
|
||||||
|
|
||||||
<!-- Material 3 -->
|
<!-- Material 3 -->
|
||||||
<dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen>
|
<dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen>
|
||||||
|
|
||||||
|
|
|
@ -201,7 +201,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
|
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
|
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
|
||||||
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, 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)
|
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
return messageItem?.apply {
|
return messageItem?.apply {
|
||||||
|
|
|
@ -15,46 +15,66 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.home.room.detail.timeline.factory
|
package im.vector.app.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
|
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.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.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.helper.VoiceBroadcastEventsGroup
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
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.MessageVoiceBroadcastRecordingItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
|
||||||
|
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
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class VoiceBroadcastItemFactory @Inject constructor(
|
class VoiceBroadcastItemFactory @Inject constructor(
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val avatarSizeProvider: AvatarSizeProvider,
|
private val avatarSizeProvider: AvatarSizeProvider,
|
||||||
private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
|
private val colorProvider: ColorProvider,
|
||||||
|
private val drawableProvider: DrawableProvider,
|
||||||
|
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
|
params: TimelineItemFactoryParams,
|
||||||
messageContent: MessageVoiceBroadcastInfoContent,
|
messageContent: MessageVoiceBroadcastInfoContent,
|
||||||
eventsGroup: TimelineEventsGroup?,
|
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes,
|
attributes: AbsMessageItem.Attributes,
|
||||||
): MessageVoiceBroadcastItem? {
|
): MessageVoiceBroadcastRecordingItem? {
|
||||||
// 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 = eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
||||||
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
|
||||||
val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId
|
val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId
|
||||||
return MessageVoiceBroadcastItem_()
|
return if (isRecording) {
|
||||||
|
createRecordingItem(params.event.roomId, highlight, callback, attributes)
|
||||||
|
} else {
|
||||||
|
createRecordingItem(params.event.roomId, highlight, callback, attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRecordingItem(
|
||||||
|
roomId: String,
|
||||||
|
highlight: Boolean,
|
||||||
|
callback: TimelineEventController.Callback?,
|
||||||
|
attributes: AbsMessageItem.Attributes,
|
||||||
|
): MessageVoiceBroadcastRecordingItem? {
|
||||||
|
val roomSummary = session.getRoom(roomId)?.roomSummary()
|
||||||
|
return MessageVoiceBroadcastRecordingItem_()
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.voiceBroadcastState(mostRecentMessageContent.voiceBroadcastState)
|
.roomItem(roomSummary?.toMatrixItem())
|
||||||
.recording(isRecording)
|
.colorProvider(colorProvider)
|
||||||
.audioMessagePlaybackTracker(audioMessagePlaybackTracker)
|
.drawableProvider(drawableProvider)
|
||||||
|
.voiceBroadcastRecorder(voiceBroadcastRecorder)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<MessageVoiceBroadcastItem.Holder>() {
|
|
||||||
|
|
||||||
@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<TextView>(R.id.currentStateText)
|
|
||||||
val playButton by bind<ImageButton>(R.id.playButton)
|
|
||||||
val pauseButton by bind<ImageButton>(R.id.pauseButton)
|
|
||||||
val stopButton by bind<ImageButton>(R.id.stopButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val STUB_ID = R.id.messageVoiceBroadcastStub
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* 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.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<MessageVoiceBroadcastRecordingItem.Holder>() {
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<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)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val STUB_ID = R.id.messageVoiceBroadcastStub
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,12 +22,21 @@ import java.io.File
|
||||||
|
|
||||||
interface VoiceBroadcastRecorder : VoiceRecorder {
|
interface VoiceBroadcastRecorder : VoiceRecorder {
|
||||||
|
|
||||||
var listener: Listener?
|
val currentSequence: Int
|
||||||
var currentSequence: Int
|
val state: State
|
||||||
|
|
||||||
fun startRecord(roomId: String, chunkLength: Int)
|
fun startRecord(roomId: String, chunkLength: Int)
|
||||||
|
fun addListener(listener: Listener)
|
||||||
|
fun removeListener(listener: Listener)
|
||||||
|
|
||||||
fun interface Listener {
|
interface Listener {
|
||||||
fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int)
|
fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int) = Unit
|
||||||
|
fun onStateUpdated(state: State) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
Recording,
|
||||||
|
Paused,
|
||||||
|
Idle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,13 @@ class VoiceBroadcastRecorderQ(
|
||||||
private var maxFileSize = 0L // zero or negative for no limit
|
private var maxFileSize = 0L // zero or negative for no limit
|
||||||
private var currentRoomId: String? = null
|
private var currentRoomId: String? = null
|
||||||
override var currentSequence = 0
|
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 = mutableListOf<VoiceBroadcastRecorder.Listener>()
|
||||||
|
|
||||||
override val outputFormat = MediaRecorder.OutputFormat.MPEG_4
|
override val outputFormat = MediaRecorder.OutputFormat.MPEG_4
|
||||||
override val audioEncoder = MediaRecorder.AudioEncoder.HE_AAC
|
override val audioEncoder = MediaRecorder.AudioEncoder.HE_AAC
|
||||||
|
@ -57,24 +62,28 @@ class VoiceBroadcastRecorderQ(
|
||||||
maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong()
|
maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong()
|
||||||
currentSequence = 1
|
currentSequence = 1
|
||||||
startRecord(roomId)
|
startRecord(roomId)
|
||||||
|
state = VoiceBroadcastRecorder.State.Recording
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pauseRecord() {
|
override fun pauseRecord() {
|
||||||
tryOrNull { mediaRecorder?.stop() }
|
tryOrNull { mediaRecorder?.stop() }
|
||||||
mediaRecorder?.reset()
|
mediaRecorder?.reset()
|
||||||
notifyOutputFileCreated()
|
notifyOutputFileCreated()
|
||||||
|
state = VoiceBroadcastRecorder.State.Paused
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resumeRecord() {
|
override fun resumeRecord() {
|
||||||
currentSequence++
|
currentSequence++
|
||||||
currentRoomId?.let { startRecord(it) }
|
currentRoomId?.let { startRecord(it) }
|
||||||
|
state = VoiceBroadcastRecorder.State.Recording
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopRecord() {
|
override fun stopRecord() {
|
||||||
super.stopRecord()
|
super.stopRecord()
|
||||||
notifyOutputFileCreated()
|
notifyOutputFileCreated()
|
||||||
listener = null
|
listeners.clear()
|
||||||
currentSequence = 0
|
currentSequence = 0
|
||||||
|
state = VoiceBroadcastRecorder.State.Idle
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun release() {
|
override fun release() {
|
||||||
|
@ -82,6 +91,15 @@ class VoiceBroadcastRecorderQ(
|
||||||
super.release()
|
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) {
|
private fun onMaxFileSizeApproaching(roomId: String) {
|
||||||
setNextOutputFile(roomId)
|
setNextOutputFile(roomId)
|
||||||
}
|
}
|
||||||
|
@ -92,8 +110,8 @@ class VoiceBroadcastRecorderQ(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyOutputFileCreated() {
|
private fun notifyOutputFileCreated() {
|
||||||
outputFile?.let {
|
outputFile?.let { file ->
|
||||||
listener?.onVoiceMessageCreated(it, currentSequence)
|
listeners.forEach { it.onVoiceMessageCreated(file, currentSequence) }
|
||||||
outputFile = nextOutputFile
|
outputFile = nextOutputFile
|
||||||
nextOutputFile = null
|
nextOutputFile = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,9 +81,11 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRecording(room: Room, eventId: String, chunkLength: Int) {
|
private fun startRecording(room: Room, eventId: String, chunkLength: Int) {
|
||||||
voiceBroadcastRecorder?.listener = VoiceBroadcastRecorder.Listener { file, sequence ->
|
voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener {
|
||||||
|
override fun onVoiceMessageCreated(file: File, sequence: Int) {
|
||||||
sendVoiceFile(room, file, eventId, sequence)
|
sendVoiceFile(room, file, eventId, sequence)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength)
|
voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
vector/src/main/res/drawable/ic_live_broadcast_16.xml
Normal file
21
vector/src/main/res/drawable/ic_live_broadcast_16.xml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M13.459,2.791C13.233,2.5 12.814,2.448 12.523,2.674C12.233,2.9 12.181,3.318 12.406,3.609L12.406,3.609L12.407,3.61L12.416,3.622C12.425,3.634 12.439,3.654 12.458,3.68C12.496,3.733 12.552,3.815 12.62,3.923C12.757,4.138 12.943,4.456 13.128,4.854C13.502,5.654 13.866,6.756 13.866,8C13.866,9.245 13.502,10.347 13.128,11.147C12.943,11.545 12.757,11.863 12.62,12.078C12.552,12.186 12.496,12.267 12.458,12.321C12.439,12.347 12.425,12.367 12.416,12.378L12.407,12.391L12.406,12.391L12.406,12.392C12.181,12.683 12.233,13.101 12.523,13.327C12.814,13.553 13.233,13.5 13.459,13.21L12.962,12.823C13.459,13.21 13.459,13.21 13.459,13.21L13.46,13.208L13.462,13.205L13.468,13.198L13.485,13.175C13.5,13.155 13.52,13.128 13.545,13.093C13.595,13.023 13.664,12.922 13.745,12.794C13.908,12.538 14.123,12.17 14.337,11.711C14.763,10.797 15.199,9.499 15.199,8C15.199,6.502 14.763,5.204 14.337,4.29C14.123,3.831 13.908,3.463 13.745,3.207C13.664,3.079 13.595,2.978 13.545,2.908C13.52,2.873 13.5,2.846 13.485,2.826L13.468,2.803L13.462,2.795L13.46,2.793L13.46,2.792C13.46,2.792 13.459,2.791 12.933,3.2L13.459,2.791Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M11.726,5.191C11.5,4.901 11.081,4.848 10.791,5.074C10.501,5.3 10.448,5.717 10.672,6.008L10.674,6.011C10.677,6.015 10.683,6.022 10.691,6.033C10.707,6.056 10.731,6.092 10.762,6.141C10.825,6.238 10.91,6.384 10.996,6.568C11.169,6.94 11.333,7.442 11.333,8.001C11.333,8.559 11.169,9.061 10.996,9.433C10.91,9.617 10.825,9.763 10.762,9.86C10.731,9.909 10.707,9.945 10.691,9.968C10.683,9.979 10.677,9.986 10.674,9.99L10.672,9.994C10.448,10.284 10.501,10.701 10.791,10.927C11.081,11.153 11.5,11.101 11.726,10.81L11.2,10.401C11.726,10.81 11.726,10.81 11.726,10.81L11.727,10.808L11.729,10.806L11.733,10.801L11.744,10.787C11.752,10.775 11.764,10.759 11.778,10.74C11.806,10.7 11.843,10.646 11.887,10.576C11.975,10.438 12.09,10.241 12.204,9.997C12.431,9.511 12.667,8.813 12.667,8.001C12.667,7.188 12.431,6.49 12.204,6.004C12.09,5.76 11.975,5.563 11.887,5.425C11.843,5.356 11.806,5.301 11.778,5.261C11.764,5.242 11.752,5.226 11.744,5.214L11.733,5.2L11.729,5.195L11.727,5.193L11.727,5.192C11.727,5.192 11.726,5.191 11.2,5.601L11.726,5.191Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M2.407,13.21C2.633,13.5 3.052,13.553 3.343,13.327C3.633,13.101 3.686,12.683 3.461,12.392L3.46,12.391L3.459,12.391L3.45,12.378C3.441,12.366 3.427,12.347 3.408,12.321C3.371,12.267 3.314,12.186 3.246,12.078C3.109,11.863 2.924,11.545 2.738,11.147C2.364,10.347 2,9.245 2,8C2,6.756 2.364,5.654 2.738,4.854C2.924,4.456 3.109,4.138 3.246,3.923C3.314,3.815 3.371,3.733 3.408,3.68C3.427,3.654 3.441,3.634 3.45,3.622L3.459,3.61L3.46,3.609L3.461,3.609C3.686,3.318 3.633,2.9 3.343,2.674C3.052,2.448 2.633,2.5 2.407,2.791L2.904,3.177C2.407,2.791 2.407,2.791 2.407,2.791L2.406,2.793L2.404,2.795L2.399,2.802L2.381,2.826C2.366,2.846 2.346,2.873 2.321,2.908C2.272,2.978 2.203,3.079 2.121,3.207C1.958,3.463 1.744,3.831 1.529,4.29C1.103,5.204 0.667,6.502 0.667,8C0.667,9.499 1.103,10.797 1.529,11.711C1.744,12.17 1.958,12.538 2.121,12.794C2.203,12.922 2.272,13.023 2.321,13.093C2.346,13.128 2.366,13.155 2.381,13.175L2.399,13.198L2.404,13.205L2.406,13.208L2.407,13.209C2.407,13.209 2.407,13.21 2.934,12.8L2.407,13.21Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M4.14,10.809C4.366,11.1 4.785,11.153 5.076,10.926C5.365,10.701 5.418,10.284 5.194,9.993L5.192,9.99C5.189,9.986 5.183,9.978 5.175,9.967C5.16,9.945 5.135,9.909 5.104,9.86C5.042,9.762 4.956,9.616 4.87,9.433C4.697,9.061 4.533,8.559 4.533,8C4.533,7.442 4.697,6.94 4.87,6.568C4.956,6.384 5.042,6.238 5.104,6.14C5.135,6.092 5.16,6.055 5.175,6.033C5.183,6.022 5.189,6.014 5.192,6.01L5.194,6.007C5.418,5.717 5.365,5.299 5.076,5.074C4.785,4.848 4.366,4.9 4.14,5.191L4.666,5.6C4.14,5.191 4.14,5.191 4.14,5.191L4.139,5.192L4.137,5.194L4.134,5.199L4.123,5.214C4.114,5.226 4.102,5.241 4.088,5.261C4.061,5.3 4.023,5.355 3.979,5.424C3.891,5.562 3.776,5.759 3.662,6.004C3.436,6.489 3.2,7.187 3.2,8C3.2,8.813 3.436,9.511 3.662,9.996C3.776,10.241 3.891,10.438 3.979,10.576C4.023,10.645 4.061,10.7 4.088,10.739C4.102,10.759 4.114,10.775 4.123,10.786L4.134,10.801L4.137,10.806L4.139,10.808L4.14,10.809C4.14,10.809 4.14,10.809 4.666,10.4L4.14,10.809Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M8,8m-1.333,0a1.333,1.333 0,1 1,2.667 0a1.333,1.333 0,1 1,-2.667 0"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</vector>
|
9
vector/src/main/res/drawable/ic_recording_dot.xml
Normal file
9
vector/src/main/res/drawable/ic_recording_dot.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M8,8m-7.5,0a7.5,7.5 0,1 1,15 0a7.5,7.5 0,1 1,-15 0"
|
||||||
|
android:fillColor="#FF5B55"/>
|
||||||
|
</vector>
|
9
vector/src/main/res/drawable/ic_stop.xml
Normal file
9
vector/src/main/res/drawable/ic_stop.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="18dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="18"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M1.5,3L16.5,3A1.5,1.5 0,0 1,18 4.5L18,19.5A1.5,1.5 0,0 1,16.5 21L1.5,21A1.5,1.5 0,0 1,0 19.5L0,4.5A1.5,1.5 0,0 1,1.5 3z"
|
||||||
|
android:fillColor="#737D8C"/>
|
||||||
|
</vector>
|
11
vector/src/main/res/drawable/rounded_rect_shape_2.xml
Normal file
11
vector/src/main/res/drawable/rounded_rect_shape_2.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<size android:width="40dp" android:height="40dp"/>
|
||||||
|
|
||||||
|
<solid android:color="?vctr_system" />
|
||||||
|
|
||||||
|
<corners android:radius="2dp" />
|
||||||
|
|
||||||
|
</shape>
|
|
@ -5,58 +5,89 @@
|
||||||
android:id="@+id/messageRootLayout"
|
android:id="@+id/messageRootLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/rounded_rect_shape_8"
|
||||||
|
android:backgroundTint="?vctr_content_quinary"
|
||||||
android:padding="@dimen/layout_vertical_margin"
|
android:padding="@dimen/layout_vertical_margin"
|
||||||
tools:viewBindingIgnore="true">
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/currentStateText"
|
android:id="@+id/liveIndicator"
|
||||||
style="@style/Widget.Vector.TextView.HeadlineMedium"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="20dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/playButton"
|
android:background="@drawable/rounded_rect_shape_2"
|
||||||
|
android:backgroundTint="?colorError"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:maxWidth="100dp"
|
||||||
|
android:paddingHorizontal="4dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="@string/voice_broadcast_live"
|
||||||
|
android:textColor="?colorOnError"
|
||||||
|
app:drawableStartCompat="@drawable/ic_live_broadcast_16"
|
||||||
|
app:drawableTint="?colorOnError"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/roomAvatarImageView"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:contentDescription="@string/avatar"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="Voice Broadcast state: STARTED" />
|
tools:src="@sample/user_round_avatars" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/avatarRightBarrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="right"
|
||||||
|
app:barrierMargin="6dp"
|
||||||
|
app:constraint_referenced_ids="roomAvatarImageView" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleText"
|
||||||
|
style="@style/Widget.Vector.TextView.Body.Medium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/avatar"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@sample/users.json/data/displayName" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/headerBottomBarrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="12dp"
|
||||||
|
app:constraint_referenced_ids="roomAvatarImageView,titleText" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/playButton"
|
android:id="@+id/recordButton"
|
||||||
android:layout_width="@dimen/item_event_message_media_button_size"
|
android:layout_width="@dimen/voice_broadcast_controller_button_size"
|
||||||
android:layout_height="@dimen/item_event_message_media_button_size"
|
android:layout_height="@dimen/voice_broadcast_controller_button_size"
|
||||||
android:layout_marginTop="@dimen/layout_vertical_margin"
|
android:background="@drawable/bg_rounded_button"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:backgroundTint="?vctr_system"
|
||||||
android:contentDescription="@string/a11y_play_voice_message"
|
android:contentDescription="@string/a11y_resume_voice_broadcast_record"
|
||||||
android:src="@drawable/ic_play_pause_play"
|
android:src="@drawable/ic_recording_dot"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pauseButton"
|
app:layout_constraintEnd_toStartOf="@id/stopRecordButton"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/currentStateText"
|
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
|
||||||
app:tint="@color/vector_content_primary_tint_selector" />
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/pauseButton"
|
android:id="@+id/stopRecordButton"
|
||||||
android:layout_width="@dimen/item_event_message_media_button_size"
|
android:layout_width="@dimen/voice_broadcast_controller_button_size"
|
||||||
android:layout_height="@dimen/item_event_message_media_button_size"
|
android:layout_height="@dimen/voice_broadcast_controller_button_size"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="@drawable/bg_rounded_button"
|
||||||
android:contentDescription="@string/a11y_pause_voice_message"
|
android:backgroundTint="?vctr_system"
|
||||||
android:src="@drawable/ic_play_pause_pause"
|
android:contentDescription="@string/a11y_stop_voice_broadcast_record"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/playButton"
|
android:src="@drawable/ic_stop"
|
||||||
app:layout_constraintEnd_toStartOf="@id/stopButton"
|
app:layout_constraintBottom_toBottomOf="@id/recordButton"
|
||||||
app:layout_constraintStart_toEndOf="@id/playButton"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/playButton"
|
|
||||||
app:tint="@color/vector_content_primary_tint_selector" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/stopButton"
|
|
||||||
android:layout_width="@dimen/item_event_message_media_button_size"
|
|
||||||
android:layout_height="@dimen/item_event_message_media_button_size"
|
|
||||||
android:background="?android:attr/selectableItemBackground"
|
|
||||||
android:contentDescription="@string/a11y_stop_voice_message"
|
|
||||||
android:src="@drawable/ic_close_24dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pauseButton"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/pauseButton"
|
app:layout_constraintStart_toEndOf="@id/recordButton"
|
||||||
app:layout_constraintTop_toTopOf="@id/playButton"
|
app:layout_constraintTop_toTopOf="@id/recordButton" />
|
||||||
app:tint="@color/vector_content_primary_tint_selector" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
Loading…
Reference in a new issue