From d0155c9890aa5cebad9b196247f5739229b332d2 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Mon, 4 Apr 2022 16:17:41 +0100 Subject: [PATCH] Adds proper handling of audio seek bar --- .../home/room/detail/TimelineFragment.kt | 5 +- ...MessageHelper.kt => AudioMessageHelper.kt} | 20 ++++++-- .../detail/composer/MessageComposerAction.kt | 1 + .../composer/MessageComposerViewModel.kt | 36 ++++++++------- .../timeline/TimelineEventController.kt | 2 + .../timeline/factory/MessageItemFactory.kt | 3 ++ .../helper/AudioMessagePlaybackTracker.kt | 6 ++- .../detail/timeline/item/MessageAudioItem.kt | 46 +++++++++++++++++-- .../layout/item_timeline_event_audio_stub.xml | 11 +++-- 9 files changed, 99 insertions(+), 31 deletions(-) rename vector/src/main/java/im/vector/app/features/home/room/detail/composer/{VoiceMessageHelper.kt => AudioMessageHelper.kt} (92%) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 4907f99d25..57dbd07daf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -105,7 +105,6 @@ import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.core.utils.createUIHandler import im.vector.app.core.utils.isValidUrl 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.openUrlInExternalBrowser import im.vector.app.core.utils.registerForPermissionsResult @@ -2080,6 +2079,10 @@ class TimelineFragment @Inject constructor( 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) { when (action.messageContent) { is MessageTextContent -> shareText(requireContext(), action.messageContent.body) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt similarity index 92% rename from vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt rename to vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index 3d263a9a97..f4cab3305d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -40,12 +40,13 @@ import javax.inject.Inject /** * Helper class to record audio for voice messages. */ -class VoiceMessageHelper @Inject constructor( +class AudioMessageHelper @Inject constructor( private val context: Context, private val playbackTracker: AudioMessagePlaybackTracker, voiceRecorderProvider: VoiceRecorderProvider ) { private var mediaPlayer: MediaPlayer? = null + private var currentPlayingId: String? = null private var voiceRecorder: VoiceRecorder = voiceRecorderProvider.provideVoiceRecorder() private val amplitudeList = mutableListOf() @@ -136,6 +137,7 @@ class VoiceMessageHelper @Inject constructor( mediaPlayer?.stop() stopPlaybackTicker() stopRecordingAmplitudes() + currentPlayingId = null if (playbackState is AudioMessagePlaybackTracker.Listener.State.Playing) { playbackTracker.pausePlayback(id) } else { @@ -163,6 +165,7 @@ class VoiceMessageHelper @Inject constructor( seekTo(currentPlaybackTime) } } + currentPlayingId = id } catch (failure: Throwable) { Timber.e(failure, "Unable to start playback") throw VoiceFailure.UnableToPlay(failure) @@ -174,14 +177,21 @@ class VoiceMessageHelper @Inject constructor( playbackTracker.pausePlayback(AudioMessagePlaybackTracker.RECORDING_ID) mediaPlayer?.stop() stopPlaybackTicker() + currentPlayingId = null } fun movePlaybackTo(id: String, percentage: Float, totalDuration: Int) { val toMillisecond = (totalDuration * percentage).toInt() - playbackTracker.updateCurrentPlaybackTime(id, toMillisecond, percentage) + playbackTracker.pauseAllPlaybacks() - stopPlayback() - playbackTracker.pausePlayback(id) + if (currentPlayingId == id) { + mediaPlayer?.seekTo(toMillisecond) + playbackTracker.updatePlayingAtPlaybackTime(id, toMillisecond, percentage) + } else { + mediaPlayer?.pause() + playbackTracker.updatePausedAtPlaybackTime(id, toMillisecond, percentage) + stopPlaybackTicker() + } } private fun startRecordingAmplitudes() { @@ -233,7 +243,7 @@ class VoiceMessageHelper @Inject constructor( val currentPosition = mediaPlayer?.currentPosition ?: 0 val totalDuration = mediaPlayer?.duration ?: 0 val percentage = currentPosition.toFloat() / totalDuration - playbackTracker.updateCurrentPlaybackTime(id, currentPosition, percentage) + playbackTracker.updatePlayingAtPlaybackTime(id, currentPosition, percentage) } else { playbackTracker.stopPlayback(id) stopPlaybackTicker() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt index 091e9f7869..dca698ee52 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt @@ -42,4 +42,5 @@ sealed class MessageComposerAction : VectorViewModelAction { data class EndAllVoiceActions(val deleteRecord: Boolean = true) : 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 AudioSeekBarMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index eb7d87c371..aabc319ee2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -73,7 +73,7 @@ class MessageComposerViewModel @AssistedInject constructor( private val vectorPreferences: VectorPreferences, private val commandParser: CommandParser, private val rainbowGenerator: RainbowGenerator, - private val voiceMessageHelper: VoiceMessageHelper, + private val audioMessageHelper: AudioMessageHelper, private val analyticsTracker: AnalyticsTracker, private val voicePlayerHelper: VoicePlayerHelper ) : VectorViewModel(initialState) { @@ -90,7 +90,6 @@ class MessageComposerViewModel @AssistedInject constructor( } override fun handle(action: MessageComposerAction) { - Timber.v("Handle action: $action") when (action) { is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action) is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) @@ -110,6 +109,7 @@ class MessageComposerViewModel @AssistedInject constructor( is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText) is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action) + is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action) } } @@ -811,18 +811,18 @@ class MessageComposerViewModel @AssistedInject constructor( private fun handleStartRecordingVoiceMessage() { try { - voiceMessageHelper.startRecording(room.roomId) + audioMessageHelper.startRecording(room.roomId) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure)) } } private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) { - voiceMessageHelper.stopPlayback() + audioMessageHelper.stopPlayback() if (isCancelled) { - voiceMessageHelper.deleteRecording() + audioMessageHelper.deleteRecording() } else { - voiceMessageHelper.stopRecording(convertForSending = true)?.let { audioType -> + audioMessageHelper.stopRecording(convertForSending = true)?.let { audioType -> if (audioType.duration > 1000) { room.sendMedia( attachment = audioType.toContentAttachmentData(isVoiceMessage = true), @@ -830,7 +830,7 @@ class MessageComposerViewModel @AssistedInject constructor( roomIds = emptySet(), rootThreadEventId = rootThreadEventId) } 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 val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile // Play can fail - voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile) + audioMessageHelper.startOrPausePlayback(action.eventId, convertedFile) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure)) } @@ -853,34 +853,38 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handlePlayOrPauseRecordingPlayback() { - voiceMessageHelper.startOrPauseRecordingPlayback() + audioMessageHelper.startOrPauseRecordingPlayback() } private fun handleEndAllVoiceActions(deleteRecord: Boolean) { - voiceMessageHelper.clearTracker() - voiceMessageHelper.stopAllVoiceActions(deleteRecord) + audioMessageHelper.clearTracker() + audioMessageHelper.stopAllVoiceActions(deleteRecord) } private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) { - voiceMessageHelper.initializeRecorder(attachmentData) + audioMessageHelper.initializeRecorder(attachmentData) setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) } } private fun handlePauseRecordingVoiceMessage() { - voiceMessageHelper.pauseRecording() + audioMessageHelper.pauseRecording() } 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) { - 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) { // 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 } if (isVoiceRecording) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 023c28cdc7..981e5740d7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -148,6 +148,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onVoiceWaveformTouchedUp(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) } 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 adb5cfdda6..dc2266b154 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 @@ -341,6 +341,7 @@ class MessageItemFactory @Inject constructor( ): MessageAudioItem { val fileUrl = getAudioFileUrl(messageContent, informationData) val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params) + val duration = messageContent.audioInfo?.duration ?: 0 return MessageAudioItem_() .attributes(attributes) @@ -349,6 +350,8 @@ class MessageItemFactory @Inject constructor( .playbackControlButtonClickListener(playbackControlButtonClickListener) .audioMessagePlaybackTracker(audioMessagePlaybackTracker) .isLocalFile(localFilesHelper.isLocalFile(fileUrl)) + .fileSize(messageContent.audioInfo?.size ?: 0L) + .onSeek { params.callback?.onAudioSeekBarMovedTo(informationData.eventId, duration, it) } .mxcUrl(fileUrl) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index fb000d7b70..0312ac9e6f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -104,10 +104,14 @@ class AudioMessagePlaybackTracker @Inject constructor() { 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)) } + fun updatePausedAtPlaybackTime(id: String, time: Int, percentage: Float) { + setState(id, Listener.State.Paused(time, percentage)) + } + fun updateCurrentRecording(id: String, amplitudeList: List) { setState(id, Listener.State.Recording(amplitudeList)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt index 7748ccb03f..8d5810a60a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt @@ -22,6 +22,7 @@ import android.graphics.Paint import android.text.format.DateUtils import android.view.ViewGroup import android.widget.ImageButton +import android.widget.SeekBar import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute @@ -29,6 +30,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.ClickListener 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.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder @@ -47,10 +49,16 @@ abstract class MessageAudioItem : AbsMessageItem() { @EpoxyAttribute var duration: Int = 0 + @EpoxyAttribute + var fileSize: Long = 0 + @EpoxyAttribute @JvmField var isLocalFile = false + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var onSeek: ((percentage: Float) -> Unit)? = null + @EpoxyAttribute lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder @@ -63,12 +71,15 @@ abstract class MessageAudioItem : AbsMessageItem() { @EpoxyAttribute lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker + private var isUserSeeking = false + override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.rootLayout, null) - bindFilenameViewAttributes(holder) + bindViewAttributes(holder) bindUploadState(holder) applyLayoutTint(holder) + bindSeekBar(holder) holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } renderStateBasedOnAudioPlayback(holder) } @@ -93,10 +104,30 @@ abstract class MessageAudioItem : AbsMessageItem() { holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) } - private fun bindFilenameViewAttributes(holder: Holder) { + private fun bindViewAttributes(holder: Holder) { holder.filenameView.text = filename holder.filenameView.onClick(attributes.itemClickListener) 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) { @@ -117,13 +148,18 @@ abstract class MessageAudioItem : AbsMessageItem() { holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_audio_message, filename) holder.audioPlaybackTime.text = formatPlaybackTime(duration) + holder.audioSeekBar.progress = 0 } private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) { holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) holder.audioPlaybackControlButton.contentDescription = 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) { @@ -131,6 +167,7 @@ abstract class MessageAudioItem : AbsMessageItem() { holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_audio_message, filename) holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime) + holder.audioSeekBar.progress = (state.percentage * 100).toInt() } private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) @@ -151,6 +188,9 @@ abstract class MessageAudioItem : AbsMessageItem() { val audioPlaybackControlButton by bind(R.id.audioPlaybackControlButton) val audioPlaybackTime by bind(R.id.audioPlaybackTime) val progressLayout by bind(R.id.messageFileUploadProgressLayout) + val fileSize by bind(R.id.fileSize) + val audioPlaybackDuration by bind(R.id.audioPlaybackDuration) + val audioSeekBar by bind(R.id.audioSeekBar) } companion object { diff --git a/vector/src/main/res/layout/item_timeline_event_audio_stub.xml b/vector/src/main/res/layout/item_timeline_event_audio_stub.xml index ee57046bf4..092810e446 100644 --- a/vector/src/main/res/layout/item_timeline_event_audio_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_audio_stub.xml @@ -45,10 +45,11 @@ tools:text="Filename.mp3" />