From ab2001cd7f8f97440a57a7f85e28723c32bc20a2 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 2 Mar 2022 17:45:27 +0300 Subject: [PATCH 01/13] Create a custom audio waveform view. --- .../main/res/values/styles_voice_message.xml | 16 +- .../detail/composer/VoiceMessageHelper.kt | 4 +- .../composer/voice/VoiceMessageViews.kt | 12 +- .../timeline/factory/MessageItemFactory.kt | 4 +- .../helper/VoiceMessagePlaybackTracker.kt | 23 +- .../detail/timeline/item/MessageVoiceItem.kt | 40 ++-- .../app/features/voice/AudioWaveformView.kt | 199 ++++++++++++++++++ .../layout/item_timeline_event_voice_stub.xml | 2 +- .../layout/view_voice_message_recorder.xml | 2 +- .../main/res/values/audio_waveform_attr.xml | 22 ++ 10 files changed, 287 insertions(+), 37 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt create mode 100644 vector/src/main/res/values/audio_waveform_attr.xml diff --git a/library/ui-styles/src/main/res/values/styles_voice_message.xml b/library/ui-styles/src/main/res/values/styles_voice_message.xml index 2e87353303d..81d2e7581d7 100644 --- a/library/ui-styles/src/main/res/values/styles_voice_message.xml +++ b/library/ui-styles/src/main/res/values/styles_voice_message.xml @@ -2,14 +2,14 @@ \ No newline at end of file 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/VoiceMessageHelper.kt index 735d356476b..f9dfecd1f53 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/VoiceMessageHelper.kt @@ -221,7 +221,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() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt index 09284ea5fcc..8adecaad6e7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt @@ -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 @@ -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, @@ -284,7 +285,7 @@ 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) { @@ -292,11 +293,15 @@ class VoiceMessageViews( 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) { @@ -327,8 +332,9 @@ class VoiceMessageViews( fun renderRecordingWaveform(amplitudeList: Array) { views.voicePlaybackWaveform.doOnLayout { waveFormView -> + val waveformColor = ThemeUtils.getColor(waveFormView.context, R.attr.vctr_content_secondary) amplitudeList.iterator().forEach { - (waveFormView as AudioRecordView).update(it) + (waveFormView as AudioWaveformView).add(AudioWaveformView.FFT(it.toFloat(), waveformColor)) } } } 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 0c836748c85..da97cf69841 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 @@ -73,6 +73,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 @@ -688,8 +689,7 @@ 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 + it * AudioWaveformView.MAX_FFT / 1024 } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt index c6204bff1c1..076c05b9c40 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt @@ -70,7 +70,8 @@ class VoiceMessagePlaybackTracker @Inject constructor() { fun startPlayback(id: String) { val currentPlaybackTime = getPlaybackTime(id) - val currentState = Listener.State.Playing(currentPlaybackTime) + val currentPercentage = getPercentage(id) + val currentState = Listener.State.Playing(currentPlaybackTime, currentPercentage) setState(id, currentState) // Pause any active playback states @@ -87,15 +88,16 @@ class VoiceMessagePlaybackTracker @Inject constructor() { fun pausePlayback(id: String) { val currentPlaybackTime = getPlaybackTime(id) - setState(id, Listener.State.Paused(currentPlaybackTime)) + val currentPercentage = getPercentage(id) + setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage)) } fun stopPlayback(id: String) { setState(id, Listener.State.Idle) } - fun updateCurrentPlaybackTime(id: String, time: Int) { - setState(id, Listener.State.Playing(time)) + fun updateCurrentPlaybackTime(id: String, time: Int, percentage: Float) { + setState(id, Listener.State.Playing(time, percentage)) } fun updateCurrentRecording(id: String, amplitudeList: List) { @@ -113,6 +115,15 @@ class VoiceMessagePlaybackTracker @Inject constructor() { } } + fun getPercentage(id: String): Float { + return when (val state = states[id]) { + is Listener.State.Playing -> state.percentage + is Listener.State.Paused -> state.percentage + /* Listener.State.Idle, */ + else -> 0f + } + } + fun clear() { listeners.forEach { it.value.onUpdate(Listener.State.Idle) @@ -131,8 +142,8 @@ class VoiceMessagePlaybackTracker @Inject constructor() { sealed class State { object Idle : State() - data class Playing(val playbackTime: Int) : State() - data class Paused(val playbackTime: Int) : State() + data class Playing(val playbackTime: Int, val percentage: Float) : State() + data class Paused(val playbackTime: Int, val percentage: Float) : State() data class Recording(val amplitudeList: List) : State() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index e9f728d976d..82400a431d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -26,7 +26,6 @@ import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.visualizer.amplitude.AudioRecordView import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder @@ -34,6 +33,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStat import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.themes.ThemeUtils +import im.vector.app.features.voice.AudioWaveformView @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageVoiceItem : AbsMessageItem() { @@ -78,11 +78,15 @@ abstract class MessageVoiceItem : AbsMessageItem() { holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener) + val waveformColorIdle = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quaternary) + val waveformColorPlayed = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_secondary) + holder.voicePlaybackWaveform.post { - holder.voicePlaybackWaveform.recreate() + holder.voicePlaybackWaveform.clear() waveform.forEach { amplitude -> - holder.voicePlaybackWaveform.update(amplitude) + holder.voicePlaybackWaveform.add(AudioWaveformView.FFT(amplitude.toFloat(), waveformColorIdle)) } + holder.voicePlaybackWaveform.summarize() } val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) { @@ -93,33 +97,39 @@ abstract class MessageVoiceItem : AbsMessageItem() { holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } - voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener { - override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { - when (state) { - is VoiceMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder) - is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state) - is VoiceMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state) + // Don't track and don't try to update UI before view is present + holder.view.post { + voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener { + override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { + when (state) { + is VoiceMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed) + is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed) + is VoiceMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed) + } } - } - }) + }) + } } - private fun renderIdleState(holder: Holder) { + private fun renderIdleState(holder: Holder, idleColor: Int, playedColor: Int) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message) holder.voicePlaybackTime.text = formatPlaybackTime(duration) + holder.voicePlaybackWaveform.updateColors(0f, playedColor, idleColor) } - private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing) { + private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing, idleColor: Int, playedColor: Int) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_pause_voice_message) holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + holder.voicePlaybackWaveform.updateColors(state.percentage, playedColor, idleColor) } - private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused) { + private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused, idleColor: Int, playedColor: Int) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message) holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + holder.voicePlaybackWaveform.updateColors(state.percentage, playedColor, idleColor) } private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) @@ -138,7 +148,7 @@ abstract class MessageVoiceItem : AbsMessageItem() { val voiceLayout by bind(R.id.voiceLayout) val voicePlaybackControlButton by bind(R.id.voicePlaybackControlButton) val voicePlaybackTime by bind(R.id.voicePlaybackTime) - val voicePlaybackWaveform by bind(R.id.voicePlaybackWaveform) + val voicePlaybackWaveform by bind(R.id.voicePlaybackWaveform) val progressLayout by bind(R.id.messageFileUploadProgressLayout) } diff --git a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt new file mode 100644 index 00000000000..9ba7597e609 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt @@ -0,0 +1,199 @@ +/* + * 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.voice + +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import im.vector.app.R +import kotlin.math.max +import kotlin.random.Random + +class AudioWaveformView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private enum class Alignment(var value: Int) { + CENTER(0), + BOTTOM(1), + TOP(2) + } + + private enum class Flow(var value: Int) { + LTR(0), + RTL(1) + } + + data class FFT(val value: Float, var color: Int) + + private fun Int.dp() = this * Resources.getSystem().displayMetrics.density + + // Configuration fields + private var alignment = Alignment.CENTER + private var flow = Flow.LTR + private var verticalPadding = 4.dp() + private var horizontalPadding = 4.dp() + private var barWidth = 2.dp() + private var barSpace = 1.dp() + private var barMinHeight = 1.dp() + private var isBarRounded = true + + private val rawFftList = mutableListOf() + private var visibleBarHeights = mutableListOf() + + private val barPaint = Paint() + + init { + attrs?.let { + context + .theme + .obtainStyledAttributes( + attrs, + R.styleable.AudioWaveformView, + 0, + 0 + ) + .apply { + alignment = Alignment.values().find { it.value == getInt(R.styleable.AudioWaveformView_alignment, alignment.value) }!! + flow = Flow.values().find { it.value == getInt(R.styleable.AudioWaveformView_flow, alignment.value) }!! + verticalPadding = getDimension(R.styleable.AudioWaveformView_verticalPadding, verticalPadding) + horizontalPadding = getDimension(R.styleable.AudioWaveformView_horizontalPadding, horizontalPadding) + barWidth = getDimension(R.styleable.AudioWaveformView_barWidth, barWidth) + barSpace = getDimension(R.styleable.AudioWaveformView_barSpace, barSpace) + barMinHeight = getDimension(R.styleable.AudioWaveformView_barMinHeight, barMinHeight) + isBarRounded = getBoolean(R.styleable.AudioWaveformView_isBarRounded, isBarRounded) + setWillNotDraw(false) + barPaint.isAntiAlias = true + } + .apply { recycle() } + .also { + barPaint.strokeWidth = barWidth + barPaint.strokeCap = if (isBarRounded) Paint.Cap.ROUND else Paint.Cap.BUTT + } + } + } + + fun initialize(fftList: List) { + handleNewFftList(fftList) + invalidate() + } + + fun add(fft: FFT) { + handleNewFftList(listOf(fft)) + invalidate() + } + + fun summarize() { + if (rawFftList.isEmpty()) return + + val maxVisibleBarCount = getMaxVisibleBarCount() + val summarizedFftList = rawFftList.summarize(maxVisibleBarCount) + clear() + handleNewFftList(summarizedFftList) + invalidate() + } + + fun updateColors(limitPercentage: Float, colorBefore: Int, colorAfter: Int) { + val size = visibleBarHeights.size + val limitIndex = (size * limitPercentage).toInt() + visibleBarHeights.forEachIndexed { index, fft -> + fft.color = if (index < limitIndex) { + colorBefore + } else { + colorAfter + } + } + invalidate() + } + + fun clear() { + rawFftList.clear() + visibleBarHeights.clear() + } + + private fun List.summarize(target: Int): List { + val result = mutableListOf() + if (size <= target) { + result.addAll(this) + val missingItemCount = target - size + repeat(missingItemCount) { + val index = Random.nextInt(result.size) + result.add(index, result[index]) + } + } else { + val step = (size.toDouble() - 1) / (target - 1) + var index = 0.0 + while (index < size) { + result.add(get(index.toInt())) + index += step + } + } + return result + } + + private fun handleNewFftList(fftList: List) { + val maxVisibleBarCount = getMaxVisibleBarCount() + fftList.forEach { fft -> + rawFftList.add(fft) + val barHeight = max(fft.value / MAX_FFT * (height - verticalPadding * 2), barMinHeight) + visibleBarHeights.add(FFT(barHeight, fft.color)) + if (visibleBarHeights.size > maxVisibleBarCount) { + visibleBarHeights = visibleBarHeights.subList(visibleBarHeights.size - maxVisibleBarCount, visibleBarHeights.size) + } + } + } + + private fun getMaxVisibleBarCount() = ((width - horizontalPadding * 2) / (barWidth + barSpace)).toInt() + + private fun drawBars(canvas: Canvas) { + var currentX = horizontalPadding + visibleBarHeights.forEach { + barPaint.color = it.color + // TODO. Support flow + when (alignment) { + Alignment.BOTTOM -> { + val startY = height - verticalPadding + val stopY = startY - it.value + canvas.drawLine(currentX, startY, currentX, stopY, barPaint) + } + Alignment.CENTER -> { + val startY = (height - it.value) / 2 + val stopY = startY + it.value + canvas.drawLine(currentX, startY, currentX, stopY, barPaint) + } + Alignment.TOP -> { + val startY = verticalPadding + val stopY = startY + it.value + canvas.drawLine(currentX, startY, currentX, stopY, barPaint) + } + } + currentX += barWidth + barSpace + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + drawBars(canvas) + } + + companion object { + private const val MAX_FFT = 32760f + } +} diff --git a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml index a180afbf8e0..0fad714bd46 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml @@ -40,7 +40,7 @@ app:layout_constraintTop_toTopOf="@id/voicePlaybackControlButton" tools:text="0:23" /> - - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 243a714586b13afeaa2432344687bddbf9c25658 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 2 Mar 2022 17:46:09 +0300 Subject: [PATCH 02/13] Remove 3rd party waveform library. --- library/ui-styles/build.gradle | 2 -- vector/build.gradle | 1 - vector/src/main/assets/open_source_licenses.html | 5 ----- .../java/im/vector/app/features/voice/AudioWaveformView.kt | 2 +- 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index cee58414c78..0ac513b2523 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -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' } \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index c6a2636acf0..d58118eb24a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -416,7 +416,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' diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 2c25606f574..0bead1f826c 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -437,11 +437,6 @@


Copyright (c) 2017-present, dialog LLC <info@dlg.im> -
  • - Armen101 / AudioRecordView -
    - Copyright 2019 Armen Gevorgyan -
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    index 9ba7597e609..768635b2f77 100644
    --- a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    +++ b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    @@ -194,6 +194,6 @@ class AudioWaveformView @JvmOverloads constructor(
         }
     
         companion object {
    -        private const val MAX_FFT = 32760f
    +        const val MAX_FFT = 32760
         }
     }
    
    From 4254f4606535f6899e0cc130683cfbbfc46fa7e8 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Thu, 3 Mar 2022 17:59:51 +0300
    Subject: [PATCH 03/13] Support scrolling playback on timeline.
    
    ---
     .../home/room/detail/TimelineFragment.kt      |  8 ++++++
     .../detail/composer/MessageComposerAction.kt  |  2 ++
     .../composer/MessageComposerViewModel.kt      | 16 +++++++++++-
     .../detail/composer/VoiceMessageHelper.kt     |  8 ++++++
     .../timeline/TimelineEventController.kt       |  2 ++
     .../timeline/factory/MessageItemFactory.kt    | 11 ++++++++
     .../detail/timeline/item/MessageVoiceItem.kt  | 25 +++++++++++++++++++
     7 files changed, 71 insertions(+), 1 deletion(-)
    
    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 2da69bbe6cd..d019cb17774 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
    @@ -2051,6 +2051,14 @@ class TimelineFragment @Inject constructor(
             messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
         }
     
    +    override fun onVoiceWaveformTouchedUp(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float) {
    +        messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformTouchedUp(eventId, messageAudioContent, percentage))
    +    }
    +
    +    override fun onVoiceWaveformMovedTo(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float) {
    +        messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, messageAudioContent, 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/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
    index 10cef399426..daa5631d841 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
    @@ -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 messageAudioContent: MessageAudioContent, val percentage: Float) : MessageComposerAction()
    +    data class VoiceWaveformMovedTo(val eventId: String, val messageAudioContent: MessageAudioContent, 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 0d902271684..ccb51d37965 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
    @@ -108,7 +108,9 @@ 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)
    +        }.exhaustive
         }
     
         private fun handleOnVoiceRecordingUiStateChanged(action: MessageComposerAction.OnVoiceRecordingUiStateChanged) = setState {
    @@ -861,6 +863,18 @@ class MessageComposerViewModel @AssistedInject constructor(
             voiceMessageHelper.pauseRecording()
         }
     
    +    private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) {
    +        val duration = (action.messageAudioContent.audioInfo?.duration ?: 0)
    +        val toMillisecond = (action.percentage * duration).toInt()
    +        voiceMessageHelper.movePlaybackTo(action.eventId, toMillisecond, duration)
    +    }
    +
    +    private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) {
    +        val duration = (action.messageAudioContent.audioInfo?.duration ?: 0)
    +        val toMillisecond = (action.percentage * duration).toInt()
    +        voiceMessageHelper.movePlaybackTo(action.eventId, toMillisecond, duration)
    +    }
    +
         private fun handleEntersBackground(composerText: String) {
             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/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt
    index f9dfecd1f53..b6a8dc2cd50 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/VoiceMessageHelper.kt
    @@ -174,6 +174,14 @@ class VoiceMessageHelper @Inject constructor(
             stopPlaybackTicker()
         }
     
    +    fun movePlaybackTo(id: String, toMillisecond: Int, totalDuration: Int) {
    +        val percentage = toMillisecond.toFloat() / totalDuration
    +        playbackTracker.updateCurrentPlaybackTime(id, toMillisecond, percentage)
    +
    +        stopPlayback()
    +        playbackTracker.pausePlayback(id)
    +    }
    +
         private fun startRecordingAmplitudes() {
             amplitudeTicker?.stop()
             amplitudeTicker = CountUpTimer(50).apply {
    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 2ac592797c4..3965afdbaa4 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
    @@ -138,6 +138,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             fun getPreviewUrlRetriever(): PreviewUrlRetriever
     
             fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent)
    +        fun onVoiceWaveformTouchedUp(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float)
    +        fun onVoiceWaveformMovedTo(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float)
         }
     
         interface ReactionPillCallback {
    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 da97cf69841..8b0b43009dc 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
    @@ -357,11 +357,22 @@ class MessageItemFactory @Inject constructor(
                 }
             }
     
    +        val waveformTouchListener: MessageVoiceItem.WaveformTouchListener = object : MessageVoiceItem.WaveformTouchListener {
    +            override fun onWaveformTouchedUp(percentage: Float) {
    +                params.callback?.onVoiceWaveformTouchedUp(informationData.eventId, messageContent, percentage)
    +            }
    +
    +            override fun onWaveformMovedTo(percentage: Float) {
    +                params.callback?.onVoiceWaveformMovedTo(informationData.eventId, messageContent, 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(
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    index 82400a431d3..d1c134a7430 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.item
     import android.content.res.ColorStateList
     import android.graphics.Color
     import android.text.format.DateUtils
    +import android.view.MotionEvent
     import android.view.View
     import android.view.ViewGroup
     import android.widget.ImageButton
    @@ -38,6 +39,11 @@ import im.vector.app.features.voice.AudioWaveformView
     @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
     abstract class MessageVoiceItem : AbsMessageItem() {
     
    +    interface WaveformTouchListener {
    +        fun onWaveformTouchedUp(percentage: Float)
    +        fun onWaveformMovedTo(percentage: Float)
    +    }
    +
         @EpoxyAttribute
         var mxcUrl: String = ""
     
    @@ -62,6 +68,9 @@ abstract class MessageVoiceItem : AbsMessageItem() {
         @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
         var playbackControlButtonClickListener: ClickListener? = null
     
    +    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
    +    var waveformTouchListener: WaveformTouchListener? = null
    +
         @EpoxyAttribute
         lateinit var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker
     
    @@ -87,6 +96,20 @@ abstract class MessageVoiceItem : AbsMessageItem() {
                     holder.voicePlaybackWaveform.add(AudioWaveformView.FFT(amplitude.toFloat(), waveformColorIdle))
                 }
                 holder.voicePlaybackWaveform.summarize()
    +
    +            holder.voicePlaybackWaveform.setOnTouchListener { view, motionEvent ->
    +                when (motionEvent.action) {
    +                    MotionEvent.ACTION_UP   -> {
    +                        val percentage = getTouchedPositionPercentage(motionEvent, view)
    +                        waveformTouchListener?.onWaveformTouchedUp(percentage)
    +                    }
    +                    MotionEvent.ACTION_MOVE -> {
    +                        val percentage = getTouchedPositionPercentage(motionEvent, view)
    +                        waveformTouchListener?.onWaveformMovedTo(percentage)
    +                    }
    +                }
    +                true
    +            }
             }
     
             val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
    @@ -111,6 +134,8 @@ abstract class MessageVoiceItem : AbsMessageItem() {
             }
         }
     
    +    private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = motionEvent.x / view.width
    +
         private fun renderIdleState(holder: Holder, idleColor: Int, playedColor: Int) {
             holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
             holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message)
    
    From 3bd4a4ccd3ece851ac3de5c4c4e1e11a406efc33 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Thu, 3 Mar 2022 19:54:13 +0300
    Subject: [PATCH 04/13] Fix voice recorder start/pause states.
    
    ---
     .../home/room/detail/composer/VoiceMessageHelper.kt         | 6 ++++--
     .../detail/timeline/helper/VoiceMessagePlaybackTracker.kt   | 2 +-
     2 files changed, 5 insertions(+), 3 deletions(-)
    
    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/VoiceMessageHelper.kt
    index b6a8dc2cd50..6bde4ada3d5 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/VoiceMessageHelper.kt
    @@ -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)
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt
    index 076c05b9c40..8167ad94aff 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt
    @@ -115,7 +115,7 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
             }
         }
     
    -    fun getPercentage(id: String): Float {
    +    private fun getPercentage(id: String): Float {
             return when (val state = states[id]) {
                 is Listener.State.Playing -> state.percentage
                 is Listener.State.Paused  -> state.percentage
    
    From 5168d715ceb89ec62a31ea0504232952dd6c7c57 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Fri, 4 Mar 2022 16:21:28 +0300
    Subject: [PATCH 05/13] Support scrolling playback on recorded audio before
     sending.
    
    ---
     .../home/room/detail/TimelineFragment.kt      | 20 ++++++++++++----
     .../detail/composer/MessageComposerAction.kt  |  4 ++--
     .../composer/MessageComposerViewModel.kt      |  8 ++-----
     .../detail/composer/VoiceMessageHelper.kt     |  6 ++---
     .../voice/VoiceMessageRecorderView.kt         | 17 +++++++++++++-
     .../composer/voice/VoiceMessageViews.kt       | 23 ++++++++++++++++---
     .../timeline/TimelineEventController.kt       |  4 ++--
     .../timeline/factory/MessageItemFactory.kt    |  6 +++--
     8 files changed, 65 insertions(+), 23 deletions(-)
    
    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 d019cb17774..a0e8ddce3d2 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
    @@ -786,6 +786,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))
    @@ -2051,12 +2063,12 @@ class TimelineFragment @Inject constructor(
             messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent))
         }
     
    -    override fun onVoiceWaveformTouchedUp(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float) {
    -        messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformTouchedUp(eventId, messageAudioContent, percentage))
    +    override fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float) {
    +        messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformTouchedUp(eventId, duration, percentage))
         }
     
    -    override fun onVoiceWaveformMovedTo(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float) {
    -        messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, messageAudioContent, percentage))
    +    override fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float) {
    +        messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, duration, percentage))
         }
     
         private fun onShareActionClicked(action: EventSharedAction.Share) {
    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 daa5631d841..091e9f7869c 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
    @@ -40,6 +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 messageAudioContent: MessageAudioContent, val percentage: Float) : MessageComposerAction()
    -    data class VoiceWaveformMovedTo(val eventId: String, val messageAudioContent: MessageAudioContent, 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()
     }
    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 ccb51d37965..fba3b8b5d3d 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
    @@ -864,15 +864,11 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     
         private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) {
    -        val duration = (action.messageAudioContent.audioInfo?.duration ?: 0)
    -        val toMillisecond = (action.percentage * duration).toInt()
    -        voiceMessageHelper.movePlaybackTo(action.eventId, toMillisecond, duration)
    +        voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
         }
     
         private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) {
    -        val duration = (action.messageAudioContent.audioInfo?.duration ?: 0)
    -        val toMillisecond = (action.percentage * duration).toInt()
    -        voiceMessageHelper.movePlaybackTo(action.eventId, toMillisecond, duration)
    +        voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
         }
     
         private fun handleEntersBackground(composerText: String) {
    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/VoiceMessageHelper.kt
    index 6bde4ada3d5..c5d8b7a5c10 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/VoiceMessageHelper.kt
    @@ -171,13 +171,13 @@ class VoiceMessageHelper @Inject constructor(
         }
     
         fun stopPlayback() {
    -        playbackTracker.stopPlayback(VoiceMessagePlaybackTracker.RECORDING_ID)
    +        playbackTracker.pausePlayback(VoiceMessagePlaybackTracker.RECORDING_ID)
             mediaPlayer?.stop()
             stopPlaybackTicker()
         }
     
    -    fun movePlaybackTo(id: String, toMillisecond: Int, totalDuration: Int) {
    -        val percentage = toMillisecond.toFloat() / totalDuration
    +    fun movePlaybackTo(id: String, percentage: Float, totalDuration: Int) {
    +        val toMillisecond = (totalDuration * percentage).toInt()
             playbackTracker.updateCurrentPlaybackTime(id, toMillisecond, percentage)
     
             stopPlayback()
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
    index 9a643796a95..87a2630f2ac 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
    @@ -53,6 +53,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
    @@ -65,6 +67,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)
    @@ -95,7 +98,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()
                     }
    @@ -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())
    +                }
    +            }
             })
         }
     
    @@ -203,6 +217,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
         }
     
         private fun stopRecordingTicker() {
    +        recordingDuration = recordingTicker?.elapsedTime() ?: 0
             recordingTicker?.stop()
             recordingTicker = null
         }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
    index 8adecaad6e7..f3b1fc918d1 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
    @@ -60,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 {
    @@ -70,6 +83,8 @@ class VoiceMessageViews(
             observeMicButton(actions)
         }
     
    +    private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = motionEvent.x / view.width
    +
         @SuppressLint("ClickableViewAccessibility")
         private fun observeMicButton(actions: Actions) {
             val draggableStateProcessor = DraggableStateProcessor(resources, dimensionConverter)
    @@ -332,7 +347,7 @@ class VoiceMessageViews(
     
         fun renderRecordingWaveform(amplitudeList: Array) {
             views.voicePlaybackWaveform.doOnLayout { waveFormView ->
    -            val waveformColor = ThemeUtils.getColor(waveFormView.context, R.attr.vctr_content_secondary)
    +            val waveformColor = ThemeUtils.getColor(waveFormView.context, R.attr.vctr_content_quaternary)
                 amplitudeList.iterator().forEach {
                     (waveFormView as AudioWaveformView).add(AudioWaveformView.FFT(it.toFloat(), waveformColor))
                 }
    @@ -355,5 +370,7 @@ class VoiceMessageViews(
             fun onDeleteVoiceMessage()
             fun onWaveformClicked()
             fun onVoicePlaybackButtonClicked()
    +        fun onVoiceWaveformTouchedUp(percentage: Float)
    +        fun onVoiceWaveformMoved(percentage: Float)
         }
     }
    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 3965afdbaa4..9c469dfeadd 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
    @@ -138,8 +138,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             fun getPreviewUrlRetriever(): PreviewUrlRetriever
     
             fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent)
    -        fun onVoiceWaveformTouchedUp(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float)
    -        fun onVoiceWaveformMovedTo(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float)
    +        fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float)
    +        fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float)
         }
     
         interface ReactionPillCallback {
    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 8b0b43009dc..9116de92dd8 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
    @@ -359,11 +359,13 @@ class MessageItemFactory @Inject constructor(
     
             val waveformTouchListener: MessageVoiceItem.WaveformTouchListener = object : MessageVoiceItem.WaveformTouchListener {
                 override fun onWaveformTouchedUp(percentage: Float) {
    -                params.callback?.onVoiceWaveformTouchedUp(informationData.eventId, messageContent, percentage)
    +                val duration = messageContent.audioInfo?.duration ?: 0
    +                params.callback?.onVoiceWaveformTouchedUp(informationData.eventId, duration, percentage)
                 }
     
                 override fun onWaveformMovedTo(percentage: Float) {
    -                params.callback?.onVoiceWaveformMovedTo(informationData.eventId, messageContent, percentage)
    +                val duration = messageContent.audioInfo?.duration ?: 0
    +                params.callback?.onVoiceWaveformMovedTo(informationData.eventId, duration, percentage)
                 }
             }
     
    
    From aae75ce52fa1541b32f54ee96a3442b65a92844a Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Fri, 4 Mar 2022 16:54:56 +0300
    Subject: [PATCH 06/13] Always stop all voice actions and media player if app
     enters to the background.
    
    ---
     .../home/room/detail/composer/MessageComposerViewModel.kt  | 7 +++++--
     1 file changed, 5 insertions(+), 2 deletions(-)
    
    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 fba3b8b5d3d..b71398c8a28 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
    @@ -872,11 +872,14 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     
         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)) }
    
    From 601f10a6fb7e62f43e9d1ec8dbcf898bcbf50b78 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Fri, 4 Mar 2022 17:16:09 +0300
    Subject: [PATCH 07/13] Support ltr and rtl flow of the recording waveform.
    
    ---
     .../java/im/vector/app/features/voice/AudioWaveformView.kt   | 5 +++--
     1 file changed, 3 insertions(+), 2 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    index 768635b2f77..7cdb1d51d50 100644
    --- a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    +++ b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    @@ -164,9 +164,10 @@ class AudioWaveformView @JvmOverloads constructor(
     
         private fun drawBars(canvas: Canvas) {
             var currentX = horizontalPadding
    -        visibleBarHeights.forEach {
    +        val flowableBarHeights = if (flow == Flow.LTR) visibleBarHeights else visibleBarHeights.reversed()
    +
    +        flowableBarHeights.forEach {
                 barPaint.color = it.color
    -            // TODO. Support flow
                 when (alignment) {
                     Alignment.BOTTOM -> {
                         val startY = height - verticalPadding
    
    From e09b123a9191e1d0aaea845657675ce05f982ca1 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Fri, 4 Mar 2022 17:21:00 +0300
    Subject: [PATCH 08/13] Changelog added.
    
    ---
     changelog.d/5426.feature | 1 +
     1 file changed, 1 insertion(+)
     create mode 100644 changelog.d/5426.feature
    
    diff --git a/changelog.d/5426.feature b/changelog.d/5426.feature
    new file mode 100644
    index 00000000000..2dee22f07ab
    --- /dev/null
    +++ b/changelog.d/5426.feature
    @@ -0,0 +1 @@
    +Allow scrolling position of Voice Message playback
    \ No newline at end of file
    
    From 4cb432e49704e2ccd5132fafcfad851754e40135 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Fri, 4 Mar 2022 17:47:34 +0300
    Subject: [PATCH 09/13] Do not allow to flow RTL after summarized, playback
     time always flows LTR.
    
    ---
     .../main/java/im/vector/app/features/voice/AudioWaveformView.kt  | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    index 7cdb1d51d50..32f30fe458c 100644
    --- a/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    +++ b/vector/src/main/java/im/vector/app/features/voice/AudioWaveformView.kt
    @@ -129,6 +129,7 @@ class AudioWaveformView @JvmOverloads constructor(
         }
     
         private fun List.summarize(target: Int): List {
    +        flow = Flow.LTR
             val result = mutableListOf()
             if (size <= target) {
                 result.addAll(this)
    
    From 3156410965eb913de7606d276962ae0dc715faef Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Mon, 7 Mar 2022 15:52:19 +0300
    Subject: [PATCH 10/13] Code review fixes.
    
    ---
     .../src/main/res/values/stylable_audio_waveform_view.xml        | 0
     .../home/room/detail/composer/voice/VoiceMessageViews.kt        | 2 +-
     .../home/room/detail/timeline/factory/MessageItemFactory.kt     | 1 +
     .../features/home/room/detail/timeline/item/MessageVoiceItem.kt | 2 +-
     4 files changed, 3 insertions(+), 2 deletions(-)
     rename vector/src/main/res/values/audio_waveform_attr.xml => library/ui-styles/src/main/res/values/stylable_audio_waveform_view.xml (100%)
    
    diff --git a/vector/src/main/res/values/audio_waveform_attr.xml b/library/ui-styles/src/main/res/values/stylable_audio_waveform_view.xml
    similarity index 100%
    rename from vector/src/main/res/values/audio_waveform_attr.xml
    rename to library/ui-styles/src/main/res/values/stylable_audio_waveform_view.xml
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
    index f3b1fc918d1..7a76657923d 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
    @@ -83,7 +83,7 @@ class VoiceMessageViews(
             observeMicButton(actions)
         }
     
    -    private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = motionEvent.x / view.width
    +    private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)
     
         @SuppressLint("ClickableViewAccessibility")
         private fun observeMicButton(actions: Actions) {
    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 865e8f80bd2..e8e8927b6dd 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
    @@ -709,6 +709,7 @@ class MessageItemFactory @Inject constructor(
             return this
                     ?.filterNotNull()
                     ?.map {
    +                    // Value comes from AudioWaveformView.MAX_FFT, and 1024 is the max value in the Matrix spec
                         it * AudioWaveformView.MAX_FFT / 1024
                     }
         }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    index d1c134a7430..722e0f620a4 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    @@ -134,7 +134,7 @@ abstract class MessageVoiceItem : AbsMessageItem() {
             }
         }
     
    -    private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = motionEvent.x / view.width
    +    private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)
     
         private fun renderIdleState(holder: Holder, idleColor: Int, playedColor: Int) {
             holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
    
    From 6fef2f6d4e87f4deadd45f74387b39ef32312ecc Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Mon, 7 Mar 2022 21:48:16 +0300
    Subject: [PATCH 11/13] Lint fixes.
    
    ---
     .../home/room/detail/composer/MessageComposerViewModel.kt       | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    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 36fbdf47888..0c89226f5ad 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
    @@ -877,7 +877,7 @@ class MessageComposerViewModel @AssistedInject constructor(
             // 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) {
                 viewModelScope.launch {
    
    From 24bdad3ae158cba4be1d4d39023ba87459d7f666 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Tue, 22 Mar 2022 17:04:35 +0300
    Subject: [PATCH 12/13] Code review fixes.
    
    ---
     .../detail/timeline/item/MessageVoiceItem.kt  | 72 ++++++++++---------
     1 file changed, 37 insertions(+), 35 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    index 722e0f620a4..bbaab3959d8 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
    @@ -24,6 +24,7 @@ import android.view.View
     import android.view.ViewGroup
     import android.widget.ImageButton
     import android.widget.TextView
    +import androidx.core.view.doOnLayout
     import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
    @@ -85,31 +86,8 @@ abstract class MessageVoiceItem : AbsMessageItem() {
                 holder.progressLayout.isVisible = false
             }
     
    -        holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
    -
    -        val waveformColorIdle = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quaternary)
    -        val waveformColorPlayed = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_secondary)
    -
    -        holder.voicePlaybackWaveform.post {
    -            holder.voicePlaybackWaveform.clear()
    -            waveform.forEach { amplitude ->
    -                holder.voicePlaybackWaveform.add(AudioWaveformView.FFT(amplitude.toFloat(), waveformColorIdle))
    -            }
    -            holder.voicePlaybackWaveform.summarize()
    -
    -            holder.voicePlaybackWaveform.setOnTouchListener { view, motionEvent ->
    -                when (motionEvent.action) {
    -                    MotionEvent.ACTION_UP   -> {
    -                        val percentage = getTouchedPositionPercentage(motionEvent, view)
    -                        waveformTouchListener?.onWaveformTouchedUp(percentage)
    -                    }
    -                    MotionEvent.ACTION_MOVE -> {
    -                        val percentage = getTouchedPositionPercentage(motionEvent, view)
    -                        waveformTouchListener?.onWaveformMovedTo(percentage)
    -                    }
    -                }
    -                true
    -            }
    +        holder.voicePlaybackWaveform.doOnLayout {
    +            onWaveformViewReady(holder)
             }
     
             val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
    @@ -119,19 +97,43 @@ abstract class MessageVoiceItem : AbsMessageItem() {
             }
             holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
             holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
    +    }
    +
    +    private fun onWaveformViewReady(holder: Holder) {
    +        holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
     
    -        // Don't track and don't try to update UI before view is present
    -        holder.view.post {
    -            voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
    -                override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
    -                    when (state) {
    -                        is VoiceMessagePlaybackTracker.Listener.State.Idle    -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
    -                        is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
    -                        is VoiceMessagePlaybackTracker.Listener.State.Paused  -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
    -                    }
    +        val waveformColorIdle = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quaternary)
    +        val waveformColorPlayed = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_secondary)
    +
    +        holder.voicePlaybackWaveform.clear()
    +        waveform.forEach { amplitude ->
    +            holder.voicePlaybackWaveform.add(AudioWaveformView.FFT(amplitude.toFloat(), waveformColorIdle))
    +        }
    +        holder.voicePlaybackWaveform.summarize()
    +
    +        holder.voicePlaybackWaveform.setOnTouchListener { view, motionEvent ->
    +            when (motionEvent.action) {
    +                MotionEvent.ACTION_UP   -> {
    +                    val percentage = getTouchedPositionPercentage(motionEvent, view)
    +                    waveformTouchListener?.onWaveformTouchedUp(percentage)
                     }
    -            })
    +                MotionEvent.ACTION_MOVE -> {
    +                    val percentage = getTouchedPositionPercentage(motionEvent, view)
    +                    waveformTouchListener?.onWaveformMovedTo(percentage)
    +                }
    +            }
    +            true
             }
    +
    +        voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
    +            override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
    +                when (state) {
    +                    is VoiceMessagePlaybackTracker.Listener.State.Idle    -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
    +                    is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
    +                    is VoiceMessagePlaybackTracker.Listener.State.Paused  -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
    +                }
    +            }
    +        })
         }
     
         private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)
    
    From 7ead3f93f45b81b9f42cae7454f7516c458e1550 Mon Sep 17 00:00:00 2001
    From: Onuray Sahin 
    Date: Wed, 23 Mar 2022 13:52:53 +0300
    Subject: [PATCH 13/13] Remove exhaustive.
    
    ---
     .../home/room/detail/composer/MessageComposerViewModel.kt       | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    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 a9b9a1d3023..976489eec37 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
    @@ -110,7 +110,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                 is MessageComposerAction.OnEntersBackground             -> handleEntersBackground(action.composerText)
                 is MessageComposerAction.VoiceWaveformTouchedUp         -> handleVoiceWaveformTouchedUp(action)
                 is MessageComposerAction.VoiceWaveformMovedTo           -> handleVoiceWaveformMovedTo(action)
    -        }.exhaustive
    +        }
         }
     
         private fun handleOnVoiceRecordingUiStateChanged(action: MessageComposerAction.OnVoiceRecordingUiStateChanged) = setState {