Adds proper handling of audio seek bar

This commit is contained in:
ericdecanini 2022-04-04 16:17:41 +01:00
parent 34dcd70a64
commit d0155c9890
9 changed files with 99 additions and 31 deletions

View File

@ -105,7 +105,6 @@ import im.vector.app.core.utils.createJSonViewerStyleProvider
import im.vector.app.core.utils.createUIHandler import im.vector.app.core.utils.createUIHandler
import im.vector.app.core.utils.isValidUrl import im.vector.app.core.utils.isValidUrl
import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.core.utils.openLocation import im.vector.app.core.utils.openLocation
import im.vector.app.core.utils.openUrlInExternalBrowser import im.vector.app.core.utils.openUrlInExternalBrowser
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
@ -2080,6 +2079,10 @@ class TimelineFragment @Inject constructor(
messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, duration, percentage)) messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, duration, percentage))
} }
override fun onAudioSeekBarMovedTo(eventId: String, duration: Int, percentage: Float) {
messageComposerViewModel.handle(MessageComposerAction.AudioSeekBarMovedTo(eventId, duration, percentage))
}
private fun onShareActionClicked(action: EventSharedAction.Share) { private fun onShareActionClicked(action: EventSharedAction.Share) {
when (action.messageContent) { when (action.messageContent) {
is MessageTextContent -> shareText(requireContext(), action.messageContent.body) is MessageTextContent -> shareText(requireContext(), action.messageContent.body)

View File

@ -40,12 +40,13 @@ import javax.inject.Inject
/** /**
* Helper class to record audio for voice messages. * Helper class to record audio for voice messages.
*/ */
class VoiceMessageHelper @Inject constructor( class AudioMessageHelper @Inject constructor(
private val context: Context, private val context: Context,
private val playbackTracker: AudioMessagePlaybackTracker, private val playbackTracker: AudioMessagePlaybackTracker,
voiceRecorderProvider: VoiceRecorderProvider voiceRecorderProvider: VoiceRecorderProvider
) { ) {
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private var currentPlayingId: String? = null
private var voiceRecorder: VoiceRecorder = voiceRecorderProvider.provideVoiceRecorder() private var voiceRecorder: VoiceRecorder = voiceRecorderProvider.provideVoiceRecorder()
private val amplitudeList = mutableListOf<Int>() private val amplitudeList = mutableListOf<Int>()
@ -136,6 +137,7 @@ class VoiceMessageHelper @Inject constructor(
mediaPlayer?.stop() mediaPlayer?.stop()
stopPlaybackTicker() stopPlaybackTicker()
stopRecordingAmplitudes() stopRecordingAmplitudes()
currentPlayingId = null
if (playbackState is AudioMessagePlaybackTracker.Listener.State.Playing) { if (playbackState is AudioMessagePlaybackTracker.Listener.State.Playing) {
playbackTracker.pausePlayback(id) playbackTracker.pausePlayback(id)
} else { } else {
@ -163,6 +165,7 @@ class VoiceMessageHelper @Inject constructor(
seekTo(currentPlaybackTime) seekTo(currentPlaybackTime)
} }
} }
currentPlayingId = id
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "Unable to start playback") Timber.e(failure, "Unable to start playback")
throw VoiceFailure.UnableToPlay(failure) throw VoiceFailure.UnableToPlay(failure)
@ -174,14 +177,21 @@ class VoiceMessageHelper @Inject constructor(
playbackTracker.pausePlayback(AudioMessagePlaybackTracker.RECORDING_ID) playbackTracker.pausePlayback(AudioMessagePlaybackTracker.RECORDING_ID)
mediaPlayer?.stop() mediaPlayer?.stop()
stopPlaybackTicker() stopPlaybackTicker()
currentPlayingId = null
} }
fun movePlaybackTo(id: String, percentage: Float, totalDuration: Int) { fun movePlaybackTo(id: String, percentage: Float, totalDuration: Int) {
val toMillisecond = (totalDuration * percentage).toInt() val toMillisecond = (totalDuration * percentage).toInt()
playbackTracker.updateCurrentPlaybackTime(id, toMillisecond, percentage) playbackTracker.pauseAllPlaybacks()
stopPlayback() if (currentPlayingId == id) {
playbackTracker.pausePlayback(id) mediaPlayer?.seekTo(toMillisecond)
playbackTracker.updatePlayingAtPlaybackTime(id, toMillisecond, percentage)
} else {
mediaPlayer?.pause()
playbackTracker.updatePausedAtPlaybackTime(id, toMillisecond, percentage)
stopPlaybackTicker()
}
} }
private fun startRecordingAmplitudes() { private fun startRecordingAmplitudes() {
@ -233,7 +243,7 @@ class VoiceMessageHelper @Inject constructor(
val currentPosition = mediaPlayer?.currentPosition ?: 0 val currentPosition = mediaPlayer?.currentPosition ?: 0
val totalDuration = mediaPlayer?.duration ?: 0 val totalDuration = mediaPlayer?.duration ?: 0
val percentage = currentPosition.toFloat() / totalDuration val percentage = currentPosition.toFloat() / totalDuration
playbackTracker.updateCurrentPlaybackTime(id, currentPosition, percentage) playbackTracker.updatePlayingAtPlaybackTime(id, currentPosition, percentage)
} else { } else {
playbackTracker.stopPlayback(id) playbackTracker.stopPlayback(id)
stopPlaybackTicker() stopPlaybackTicker()

View File

@ -42,4 +42,5 @@ sealed class MessageComposerAction : VectorViewModelAction {
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction() data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction()
data class VoiceWaveformTouchedUp(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction() data class VoiceWaveformTouchedUp(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
data class VoiceWaveformMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction() data class VoiceWaveformMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
data class AudioSeekBarMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
} }

View File

@ -73,7 +73,7 @@ class MessageComposerViewModel @AssistedInject constructor(
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val commandParser: CommandParser, private val commandParser: CommandParser,
private val rainbowGenerator: RainbowGenerator, private val rainbowGenerator: RainbowGenerator,
private val voiceMessageHelper: VoiceMessageHelper, private val audioMessageHelper: AudioMessageHelper,
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val voicePlayerHelper: VoicePlayerHelper private val voicePlayerHelper: VoicePlayerHelper
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) { ) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
@ -90,7 +90,6 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
override fun handle(action: MessageComposerAction) { override fun handle(action: MessageComposerAction) {
Timber.v("Handle action: $action")
when (action) { when (action) {
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action) is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
@ -110,6 +109,7 @@ class MessageComposerViewModel @AssistedInject constructor(
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText) is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action) is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
} }
} }
@ -811,18 +811,18 @@ class MessageComposerViewModel @AssistedInject constructor(
private fun handleStartRecordingVoiceMessage() { private fun handleStartRecordingVoiceMessage() {
try { try {
voiceMessageHelper.startRecording(room.roomId) audioMessageHelper.startRecording(room.roomId)
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure)) _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
} }
} }
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) { private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) {
voiceMessageHelper.stopPlayback() audioMessageHelper.stopPlayback()
if (isCancelled) { if (isCancelled) {
voiceMessageHelper.deleteRecording() audioMessageHelper.deleteRecording()
} else { } else {
voiceMessageHelper.stopRecording(convertForSending = true)?.let { audioType -> audioMessageHelper.stopRecording(convertForSending = true)?.let { audioType ->
if (audioType.duration > 1000) { if (audioType.duration > 1000) {
room.sendMedia( room.sendMedia(
attachment = audioType.toContentAttachmentData(isVoiceMessage = true), attachment = audioType.toContentAttachmentData(isVoiceMessage = true),
@ -830,7 +830,7 @@ class MessageComposerViewModel @AssistedInject constructor(
roomIds = emptySet(), roomIds = emptySet(),
rootThreadEventId = rootThreadEventId) rootThreadEventId = rootThreadEventId)
} else { } else {
voiceMessageHelper.deleteRecording() audioMessageHelper.deleteRecording()
} }
} }
} }
@ -845,7 +845,7 @@ class MessageComposerViewModel @AssistedInject constructor(
// Conversion can fail, fallback to the original file in this case and let the player fail for us // Conversion can fail, fallback to the original file in this case and let the player fail for us
val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile
// Play can fail // Play can fail
voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile) audioMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure)) _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
} }
@ -853,34 +853,38 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
private fun handlePlayOrPauseRecordingPlayback() { private fun handlePlayOrPauseRecordingPlayback() {
voiceMessageHelper.startOrPauseRecordingPlayback() audioMessageHelper.startOrPauseRecordingPlayback()
} }
private fun handleEndAllVoiceActions(deleteRecord: Boolean) { private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
voiceMessageHelper.clearTracker() audioMessageHelper.clearTracker()
voiceMessageHelper.stopAllVoiceActions(deleteRecord) audioMessageHelper.stopAllVoiceActions(deleteRecord)
} }
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) { private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
voiceMessageHelper.initializeRecorder(attachmentData) audioMessageHelper.initializeRecorder(attachmentData)
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) } setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
} }
private fun handlePauseRecordingVoiceMessage() { private fun handlePauseRecordingVoiceMessage() {
voiceMessageHelper.pauseRecording() audioMessageHelper.pauseRecording()
} }
private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) { private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) {
voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration) audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
} }
private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) { private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) {
voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration) audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
}
private fun handleAudioSeekBarMovedTo(action: MessageComposerAction.AudioSeekBarMovedTo) {
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
} }
private fun handleEntersBackground(composerText: String) { private fun handleEntersBackground(composerText: String) {
// Always stop all voice actions. It may be playing in timeline or active recording // Always stop all voice actions. It may be playing in timeline or active recording
val playingAudioContent = voiceMessageHelper.stopAllVoiceActions(deleteRecord = false) val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording } val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
if (isVoiceRecording) { if (isVoiceRecording) {

View File

@ -148,6 +148,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float) fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float)
fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float) fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float)
fun onAudioSeekBarMovedTo(eventId: String, duration: Int, percentage: Float)
fun onAddMoreReaction(event: TimelineEvent) fun onAddMoreReaction(event: TimelineEvent)
} }

View File

@ -341,6 +341,7 @@ class MessageItemFactory @Inject constructor(
): MessageAudioItem { ): MessageAudioItem {
val fileUrl = getAudioFileUrl(messageContent, informationData) val fileUrl = getAudioFileUrl(messageContent, informationData)
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params) val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
val duration = messageContent.audioInfo?.duration ?: 0
return MessageAudioItem_() return MessageAudioItem_()
.attributes(attributes) .attributes(attributes)
@ -349,6 +350,8 @@ class MessageItemFactory @Inject constructor(
.playbackControlButtonClickListener(playbackControlButtonClickListener) .playbackControlButtonClickListener(playbackControlButtonClickListener)
.audioMessagePlaybackTracker(audioMessagePlaybackTracker) .audioMessagePlaybackTracker(audioMessagePlaybackTracker)
.isLocalFile(localFilesHelper.isLocalFile(fileUrl)) .isLocalFile(localFilesHelper.isLocalFile(fileUrl))
.fileSize(messageContent.audioInfo?.size ?: 0L)
.onSeek { params.callback?.onAudioSeekBarMovedTo(informationData.eventId, duration, it) }
.mxcUrl(fileUrl) .mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)

View File

@ -104,10 +104,14 @@ class AudioMessagePlaybackTracker @Inject constructor() {
setState(id, Listener.State.Idle) setState(id, Listener.State.Idle)
} }
fun updateCurrentPlaybackTime(id: String, time: Int, percentage: Float) { fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) {
setState(id, Listener.State.Playing(time, percentage)) setState(id, Listener.State.Playing(time, percentage))
} }
fun updatePausedAtPlaybackTime(id: String, time: Int, percentage: Float) {
setState(id, Listener.State.Paused(time, percentage))
}
fun updateCurrentRecording(id: String, amplitudeList: List<Int>) { fun updateCurrentRecording(id: String, amplitudeList: List<Int>) {
setState(id, Listener.State.Recording(amplitudeList)) setState(id, Listener.State.Recording(amplitudeList))
} }

View File

@ -22,6 +22,7 @@ import android.graphics.Paint
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.SeekBar
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
@ -29,6 +30,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.utils.TextUtils
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
@ -47,10 +49,16 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var duration: Int = 0 var duration: Int = 0
@EpoxyAttribute
var fileSize: Long = 0
@EpoxyAttribute @EpoxyAttribute
@JvmField @JvmField
var isLocalFile = false var isLocalFile = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var onSeek: ((percentage: Float) -> Unit)? = null
@EpoxyAttribute @EpoxyAttribute
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
@ -63,12 +71,15 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
private var isUserSeeking = false
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
renderSendState(holder.rootLayout, null) renderSendState(holder.rootLayout, null)
bindFilenameViewAttributes(holder) bindViewAttributes(holder)
bindUploadState(holder) bindUploadState(holder)
applyLayoutTint(holder) applyLayoutTint(holder)
bindSeekBar(holder)
holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
renderStateBasedOnAudioPlayback(holder) renderStateBasedOnAudioPlayback(holder)
} }
@ -93,10 +104,30 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
} }
private fun bindFilenameViewAttributes(holder: Holder) { private fun bindViewAttributes(holder: Holder) {
holder.filenameView.text = filename holder.filenameView.text = filename
holder.filenameView.onClick(attributes.itemClickListener) holder.filenameView.onClick(attributes.itemClickListener)
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
holder.audioPlaybackDuration.text = formatPlaybackTime(duration)
holder.fileSize.text = TextUtils.formatFileSize(holder.rootLayout.context, fileSize, true)
}
private fun bindSeekBar(holder: Holder) {
holder.audioSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
holder.audioPlaybackTime.text = formatPlaybackTime(
(duration * (progress.toFloat() / 100)).toInt()
)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
isUserSeeking = true
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
isUserSeeking = false
val percentage = seekBar.progress.toFloat() / 100
onSeek?.invoke(percentage)
}
})
} }
private fun renderStateBasedOnAudioPlayback(holder: Holder) { private fun renderStateBasedOnAudioPlayback(holder: Holder) {
@ -117,13 +148,18 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
holder.audioPlaybackControlButton.contentDescription = holder.audioPlaybackControlButton.contentDescription =
holder.view.context.getString(R.string.a11y_play_audio_message, filename) holder.view.context.getString(R.string.a11y_play_audio_message, filename)
holder.audioPlaybackTime.text = formatPlaybackTime(duration) holder.audioPlaybackTime.text = formatPlaybackTime(duration)
holder.audioSeekBar.progress = 0
} }
private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) { private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) {
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
holder.audioPlaybackControlButton.contentDescription = holder.audioPlaybackControlButton.contentDescription =
holder.view.context.getString(R.string.a11y_pause_audio_message, filename) holder.view.context.getString(R.string.a11y_pause_audio_message, filename)
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
if (!isUserSeeking) {
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
holder.audioSeekBar.progress = (state.percentage * 100).toInt()
}
} }
private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused) { private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused) {
@ -131,6 +167,7 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
holder.audioPlaybackControlButton.contentDescription = holder.audioPlaybackControlButton.contentDescription =
holder.view.context.getString(R.string.a11y_play_audio_message, filename) holder.view.context.getString(R.string.a11y_play_audio_message, filename)
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime) holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
holder.audioSeekBar.progress = (state.percentage * 100).toInt()
} }
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
@ -151,6 +188,9 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
val audioPlaybackControlButton by bind<ImageButton>(R.id.audioPlaybackControlButton) val audioPlaybackControlButton by bind<ImageButton>(R.id.audioPlaybackControlButton)
val audioPlaybackTime by bind<TextView>(R.id.audioPlaybackTime) val audioPlaybackTime by bind<TextView>(R.id.audioPlaybackTime)
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout) val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
val fileSize by bind<TextView>(R.id.fileSize)
val audioPlaybackDuration by bind<TextView>(R.id.audioPlaybackDuration)
val audioSeekBar by bind<SeekBar>(R.id.audioSeekBar)
} }
companion object { companion object {

View File

@ -45,10 +45,11 @@
tools:text="Filename.mp3" /> tools:text="Filename.mp3" />
<TextView <TextView
android:id="@+id/audioPlaybackTime" android:id="@+id/audioPlaybackDuration"
style="@style/Widget.Vector.TextView.Body" style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="?vctr_content_secondary" android:textColor="?vctr_content_secondary"
app:layout_constraintStart_toStartOf="@id/messageFilenameView" app:layout_constraintStart_toStartOf="@id/messageFilenameView"
app:layout_constraintTop_toBottomOf="@id/messageFilenameView" app:layout_constraintTop_toBottomOf="@id/messageFilenameView"
@ -61,8 +62,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="?vctr_content_secondary" android:textColor="?vctr_content_secondary"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
app:layout_constraintStart_toEndOf="@id/audioPlaybackTime" app:layout_constraintStart_toEndOf="@id/audioPlaybackDuration"
app:layout_constraintBottom_toBottomOf="@id/audioPlaybackTime" app:layout_constraintBottom_toBottomOf="@id/audioPlaybackDuration"
tools:text="(2MB)" /> tools:text="(2MB)" />
<SeekBar <SeekBar
@ -74,13 +75,13 @@
android:progressDrawable="@drawable/bg_seek_bar" android:progressDrawable="@drawable/bg_seek_bar"
android:thumbTint="?vctr_content_tertiary" android:thumbTint="?vctr_content_tertiary"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/audioPlaybackDuration" app:layout_constraintEnd_toStartOf="@id/audioPlaybackTime"
app:layout_constraintTop_toBottomOf="@id/audioPlaybackControlButton" app:layout_constraintTop_toBottomOf="@id/audioPlaybackControlButton"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
tools:progress="40" /> tools:progress="40" />
<TextView <TextView
android:id="@+id/audioPlaybackDuration" android:id="@+id/audioPlaybackTime"
style="@style/Widget.Vector.TextView.Body" style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"