Skip to content

Commit

Permalink
Merge pull request #5404 from vector-im/feature/ons/voice_message_scr…
Browse files Browse the repository at this point in the history
…ubbing

Voice Message Playback Scrolling Support
  • Loading branch information
onurays authored Mar 23, 2022
2 parents 20b2af4 + 7ead3f9 commit 6d0b823
Show file tree
Hide file tree
Showing 19 changed files with 418 additions and 53 deletions.
1 change: 1 addition & 0 deletions changelog.d/5426.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow scrolling position of Voice Message playback
2 changes: 0 additions & 2 deletions library/ui-styles/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,4 @@ dependencies {
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
// dialpad dimen
implementation 'im.dlg:android-dialer:1.2.5'
// AudioRecordView attr
implementation 'com.github.Armen101:AudioRecordView:1.0.5'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="AudioWaveformView">

<attr name="alignment" format="enum">
<enum name="center" value="0" />
<enum name="bottom" value="1" />
<enum name="top" value="2" />
</attr>
<attr name="flow" format="enum">
<enum name="leftToRight" value="0" />
<enum name="rightToLeft" value="1" />
</attr>
<attr name="verticalPadding" format="dimension" />
<attr name="horizontalPadding" format="dimension" />

<attr name="barWidth" format="dimension" />
<attr name="barSpace" format="dimension" />
<attr name="barMinHeight" format="dimension" />
<attr name="isBarRounded" format="boolean" />
</declare-styleable>
</resources>
16 changes: 8 additions & 8 deletions library/ui-styles/src/main/res/values/styles_voice_message.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<resources>

<style name="VoicePlaybackWaveform">
<item name="chunkColor">?vctr_content_secondary</item>
<item name="chunkAlignTo">center</item>
<item name="chunkMinHeight">1dp</item>
<item name="chunkRoundedCorners">true</item>
<item name="chunkSoftTransition">true</item>
<item name="chunkSpace">2dp</item>
<item name="chunkWidth">2dp</item>
<item name="direction">rightToLeft</item>
<item name="alignment">center</item>
<item name="flow">leftToRight</item>
<item name="verticalPadding">4dp</item>
<item name="horizontalPadding">4dp</item>
<item name="barWidth">2dp</item>
<item name="barSpace">2dp</item>
<item name="barMinHeight">1dp</item>
<item name="isBarRounded">true</item>
</style>

</resources>
1 change: 0 additions & 1 deletion vector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,6 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
implementation 'com.github.hyuwah:DraggableView:1.0.0'
implementation 'com.github.Armen101:AudioRecordView:1.0.5'

// Custom Tab
implementation 'androidx.browser:browser:1.4.0'
Expand Down
5 changes: 0 additions & 5 deletions vector/src/main/assets/open_source_licenses.html
Original file line number Diff line number Diff line change
Expand Up @@ -437,11 +437,6 @@ <h3>
<br/>
Copyright (c) 2017-present, dialog LLC &lt;[email protected]&gt;
</li>
<li>
<b>Armen101 / AudioRecordView</b>
<br/>
Copyright 2019 Armen Gevorgyan
</li>
</ul>
<pre>
Apache License
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,18 @@ class TimelineFragment @Inject constructor(
updateRecordingUiState(RecordingUiState.Draft)
}

override fun onVoiceWaveformTouchedUp(percentage: Float, duration: Int) {
messageComposerViewModel.handle(
MessageComposerAction.VoiceWaveformTouchedUp(VoiceMessagePlaybackTracker.RECORDING_ID, duration, percentage)
)
}

override fun onVoiceWaveformMoved(percentage: Float, duration: Int) {
messageComposerViewModel.handle(
MessageComposerAction.VoiceWaveformTouchedUp(VoiceMessagePlaybackTracker.RECORDING_ID, duration, percentage)
)
}

private fun updateRecordingUiState(state: RecordingUiState) {
messageComposerViewModel.handle(
MessageComposerAction.OnVoiceRecordingUiStateChanged(state))
Expand Down Expand Up @@ -2051,6 +2063,14 @@ class TimelineFragment @Inject constructor(
messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
}

override fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float) {
messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformTouchedUp(eventId, duration, percentage))
}

override fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float) {
messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, duration, percentage))
}

private fun onShareActionClicked(action: EventSharedAction.Share) {
when (action.messageContent) {
is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ sealed class MessageComposerAction : VectorViewModelAction {
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : MessageComposerAction()
object PlayOrPauseRecordingPlayback : MessageComposerAction()
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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ class MessageComposerViewModel @AssistedInject constructor(
is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData)
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
}
}

Expand Down Expand Up @@ -868,12 +870,23 @@ class MessageComposerViewModel @AssistedInject constructor(
voiceMessageHelper.pauseRecording()
}

private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) {
voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
}

private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) {
voiceMessageHelper.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)
voiceMessageHelper.clearTracker()

val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
if (isVoiceRecording) {
voiceMessageHelper.clearTracker()
viewModelScope.launch {
voiceMessageHelper.stopAllVoiceActions(deleteRecord = false)?.toContentAttachmentData()?.let { voiceDraft ->
playingAudioContent?.toContentAttachmentData()?.let { voiceDraft ->
val content = voiceDraft.toJsonString()
room.saveDraft(UserDraft.Voice(content))
setState { copy(sendMode = SendMode.Voice(content)) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,11 @@ class VoiceMessageHelper @Inject constructor(
}

fun startOrPausePlayback(id: String, file: File) {
stopPlayback()
val playbackState = playbackTracker.getPlaybackState(id)
mediaPlayer?.stop()
stopPlaybackTicker()
stopRecordingAmplitudes()
if (playbackTracker.getPlaybackState(id) is VoiceMessagePlaybackTracker.Listener.State.Playing) {
if (playbackState is VoiceMessagePlaybackTracker.Listener.State.Playing) {
playbackTracker.pausePlayback(id)
} else {
startPlayback(id, file)
Expand Down Expand Up @@ -169,11 +171,19 @@ class VoiceMessageHelper @Inject constructor(
}

fun stopPlayback() {
playbackTracker.stopPlayback(VoiceMessagePlaybackTracker.RECORDING_ID)
playbackTracker.pausePlayback(VoiceMessagePlaybackTracker.RECORDING_ID)
mediaPlayer?.stop()
stopPlaybackTicker()
}

fun movePlaybackTo(id: String, percentage: Float, totalDuration: Int) {
val toMillisecond = (totalDuration * percentage).toInt()
playbackTracker.updateCurrentPlaybackTime(id, toMillisecond, percentage)

stopPlayback()
playbackTracker.pausePlayback(id)
}

private fun startRecordingAmplitudes() {
amplitudeTicker?.stop()
amplitudeTicker = CountUpTimer(50).apply {
Expand Down Expand Up @@ -221,7 +231,9 @@ class VoiceMessageHelper @Inject constructor(
private fun onPlaybackTick(id: String) {
if (mediaPlayer?.isPlaying.orFalse()) {
val currentPosition = mediaPlayer?.currentPosition ?: 0
playbackTracker.updateCurrentPlaybackTime(id, currentPosition)
val totalDuration = mediaPlayer?.duration ?: 0
val percentage = currentPosition.toFloat() / totalDuration
playbackTracker.updateCurrentPlaybackTime(id, currentPosition, percentage)
} else {
playbackTracker.stopPlayback(id)
stopPlaybackTicker()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
fun onDeleteVoiceMessage()
fun onRecordingLimitReached()
fun onRecordingWaveformClicked()
fun onVoiceWaveformTouchedUp(percentage: Float, duration: Int)
fun onVoiceWaveformMoved(percentage: Float, duration: Int)
}

@Inject lateinit var clock: Clock
Expand All @@ -64,6 +66,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
private var recordingTicker: CountUpTimer? = null
private var lastKnownState: RecordingUiState? = null
private var dragState: DraggingState = DraggingState.Ignored
private var recordingDuration: Long = 0

init {
inflate(this.context, R.layout.view_voice_message_recorder, this)
Expand Down Expand Up @@ -94,7 +97,6 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
override fun onDeleteVoiceMessage() = callback.onDeleteVoiceMessage()
override fun onWaveformClicked() {
when (lastKnownState) {
RecordingUiState.Draft -> callback.onVoicePlaybackButtonClicked()
is RecordingUiState.Recording,
is RecordingUiState.Locked -> callback.onRecordingWaveformClicked()
else -> Unit
Expand All @@ -105,6 +107,18 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
override fun onMicButtonDrag(nextDragStateCreator: (DraggingState) -> DraggingState) {
onDrag(dragState, newDragState = nextDragStateCreator(dragState))
}

override fun onVoiceWaveformTouchedUp(percentage: Float) {
if (lastKnownState == RecordingUiState.Draft) {
callback.onVoiceWaveformTouchedUp(percentage, recordingDuration.toInt())
}
}

override fun onVoiceWaveformMoved(percentage: Float) {
if (lastKnownState == RecordingUiState.Draft) {
callback.onVoiceWaveformMoved(percentage, recordingDuration.toInt())
}
}
})
}

Expand Down Expand Up @@ -203,6 +217,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
}

private fun stopRecordingTicker() {
recordingDuration = recordingTicker?.elapsedTime() ?: 0
recordingTicker?.stop()
recordingTicker = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import androidx.core.view.doOnLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.visualizer.amplitude.AudioRecordView
import im.vector.app.R
import im.vector.app.core.extensions.setAttributeBackground
import im.vector.app.core.extensions.setAttributeTintedBackground
Expand All @@ -37,6 +36,8 @@ import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.voice.AudioWaveformView

class VoiceMessageViews(
private val resources: Resources,
Expand All @@ -59,8 +60,21 @@ class VoiceMessageViews(
actions.onDeleteVoiceMessage()
}

views.voicePlaybackWaveform.setOnClickListener {
actions.onWaveformClicked()
views.voicePlaybackWaveform.setOnTouchListener { view, motionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
actions.onWaveformClicked()
}
MotionEvent.ACTION_UP -> {
val percentage = getTouchedPositionPercentage(motionEvent, view)
actions.onVoiceWaveformTouchedUp(percentage)
}
MotionEvent.ACTION_MOVE -> {
val percentage = getTouchedPositionPercentage(motionEvent, view)
actions.onVoiceWaveformMoved(percentage)
}
}
true
}

views.voicePlaybackControlButton.setOnClickListener {
Expand All @@ -69,6 +83,8 @@ class VoiceMessageViews(
observeMicButton(actions)
}

private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)

@SuppressLint("ClickableViewAccessibility")
private fun observeMicButton(actions: Actions) {
val draggableStateProcessor = DraggableStateProcessor(resources, dimensionConverter)
Expand Down Expand Up @@ -284,19 +300,23 @@ class VoiceMessageViews(
hideRecordingViews(RecordingUiState.Idle)
views.voiceMessageMicButton.isVisible = true
views.voiceMessageSendButton.isVisible = false
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.clear() }
}

fun renderPlaying(state: VoiceMessagePlaybackTracker.Listener.State.Playing) {
views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_pause_voice_message)
val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong())
views.voicePlaybackTime.text = formattedTimerText
val waveformColorIdle = ThemeUtils.getColor(views.voicePlaybackWaveform.context, R.attr.vctr_content_quaternary)
val waveformColorPlayed = ThemeUtils.getColor(views.voicePlaybackWaveform.context, R.attr.vctr_content_secondary)
views.voicePlaybackWaveform.updateColors(state.percentage, waveformColorPlayed, waveformColorIdle)
}

fun renderIdle() {
views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_play_voice_message)
views.voicePlaybackWaveform.summarize()
}

fun renderToast(message: String) {
Expand Down Expand Up @@ -327,8 +347,9 @@ class VoiceMessageViews(

fun renderRecordingWaveform(amplitudeList: Array<Int>) {
views.voicePlaybackWaveform.doOnLayout { waveFormView ->
val waveformColor = ThemeUtils.getColor(waveFormView.context, R.attr.vctr_content_quaternary)
amplitudeList.iterator().forEach {
(waveFormView as AudioRecordView).update(it)
(waveFormView as AudioWaveformView).add(AudioWaveformView.FFT(it.toFloat(), waveformColor))
}
}
}
Expand All @@ -349,5 +370,7 @@ class VoiceMessageViews(
fun onDeleteVoiceMessage()
fun onWaveformClicked()
fun onVoicePlaybackButtonClicked()
fun onVoiceWaveformTouchedUp(percentage: Float)
fun onVoiceWaveformMoved(percentage: Float)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun getPreviewUrlRetriever(): PreviewUrlRetriever

fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent)
fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float)
fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float)

fun onAddMoreReaction(event: TimelineEvent)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import im.vector.app.features.location.toLocationData
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voice.AudioWaveformView
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.span
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
Expand Down Expand Up @@ -362,11 +363,24 @@ class MessageItemFactory @Inject constructor(
}
}

val waveformTouchListener: MessageVoiceItem.WaveformTouchListener = object : MessageVoiceItem.WaveformTouchListener {
override fun onWaveformTouchedUp(percentage: Float) {
val duration = messageContent.audioInfo?.duration ?: 0
params.callback?.onVoiceWaveformTouchedUp(informationData.eventId, duration, percentage)
}

override fun onWaveformMovedTo(percentage: Float) {
val duration = messageContent.audioInfo?.duration ?: 0
params.callback?.onVoiceWaveformMovedTo(informationData.eventId, duration, percentage)
}
}

return MessageVoiceItem_()
.attributes(attributes)
.duration(messageContent.audioWaveformInfo?.duration ?: 0)
.waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
.playbackControlButtonClickListener(playbackControlButtonClickListener)
.waveformTouchListener(waveformTouchListener)
.voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
.izLocalFile(localFilesHelper.isLocalFile(fileUrl))
.izDownloaded(session.fileService().isFileInCache(
Expand Down Expand Up @@ -699,8 +713,8 @@ class MessageItemFactory @Inject constructor(
return this
?.filterNotNull()
?.map {
// Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec
it * 22760 / 1024
// Value comes from AudioWaveformView.MAX_FFT, and 1024 is the max value in the Matrix spec
it * AudioWaveformView.MAX_FFT / 1024
}
}

Expand Down
Loading

0 comments on commit 6d0b823

Please sign in to comment.