From ab2001cd7f8f97440a57a7f85e28723c32bc20a2 Mon Sep 17 00:00:00 2001
From: Onuray Sahin <onuray.sahin@gmail.com>
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 2e87353303..81d2e7581d 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 @@
 <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>
\ 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 735d356476..f9dfecd1f5 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 09284ea5fc..8adecaad6e 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<Int>) {
         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 0c836748c8..da97cf6984 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 c6204bff1c..076c05b9c4 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<Int>) {
@@ -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<Int>) : 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 e9f728d976..82400a431d 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<MessageVoiceItem.Holder>() {
@@ -78,11 +78,15 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
 
         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<MessageVoiceItem.Holder>() {
         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<MessageVoiceItem.Holder>() {
         val voiceLayout by bind<ViewGroup>(R.id.voiceLayout)
         val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton)
         val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime)
-        val voicePlaybackWaveform by bind<AudioRecordView>(R.id.voicePlaybackWaveform)
+        val voicePlaybackWaveform by bind<AudioWaveformView>(R.id.voicePlaybackWaveform)
         val progressLayout by bind<ViewGroup>(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 0000000000..9ba7597e60
--- /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<FFT>()
+    private var visibleBarHeights = mutableListOf<FFT>()
+
+    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<FFT>) {
+        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<FFT>.summarize(target: Int): List<FFT> {
+        val result = mutableListOf<FFT>()
+        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<FFT>) {
+        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 a180afbf8e..0fad714bd4 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" />
 
-        <com.visualizer.amplitude.AudioRecordView
+        <im.vector.app.features.voice.AudioWaveformView
             android:id="@+id/voicePlaybackWaveform"
             style="@style/VoicePlaybackWaveform"
             android:layout_width="0dp"
diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml
index 70711e71a6..f7b5f5da1d 100644
--- a/vector/src/main/res/layout/view_voice_message_recorder.xml
+++ b/vector/src/main/res/layout/view_voice_message_recorder.xml
@@ -208,7 +208,7 @@
                 app:layout_goneMarginStart="24dp"
                 tools:text="0:23" />
 
-            <com.visualizer.amplitude.AudioRecordView
+            <im.vector.app.features.voice.AudioWaveformView
                 android:id="@+id/voicePlaybackWaveform"
                 style="@style/VoicePlaybackWaveform"
                 android:layout_width="0dp"
diff --git a/vector/src/main/res/values/audio_waveform_attr.xml b/vector/src/main/res/values/audio_waveform_attr.xml
new file mode 100644
index 0000000000..f2c703764a
--- /dev/null
+++ b/vector/src/main/res/values/audio_waveform_attr.xml
@@ -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>
\ No newline at end of file

From 243a714586b13afeaa2432344687bddbf9c25658 Mon Sep 17 00:00:00 2001
From: Onuray Sahin <onuray.sahin@gmail.com>
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 cee58414c7..0ac513b252 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 c6a2636acf..d58118eb24 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 2c25606f57..0bead1f826 100755
--- a/vector/src/main/assets/open_source_licenses.html
+++ b/vector/src/main/assets/open_source_licenses.html
@@ -437,11 +437,6 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
         <br/>
         Copyright (c) 2017-present, dialog LLC &lt;info@dlg.im&gt;
     </li>
-    <li>
-        <b>Armen101 / AudioRecordView</b>
-        <br/>
-        Copyright 2019 Armen Gevorgyan
-    </li>
 </ul>
 <pre>
 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 9ba7597e60..768635b2f7 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 <onuray.sahin@gmail.com>
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 2da69bbe6c..d019cb1777 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 10cef39942..daa5631d84 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 0d90227168..ccb51d3796 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 f9dfecd1f5..b6a8dc2cd5 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 2ac592797c..3965afdbaa 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 da97cf6984..8b0b43009d 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 82400a431d..d1c134a743 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<MessageVoiceItem.Holder>() {
 
+    interface WaveformTouchListener {
+        fun onWaveformTouchedUp(percentage: Float)
+        fun onWaveformMovedTo(percentage: Float)
+    }
+
     @EpoxyAttribute
     var mxcUrl: String = ""
 
@@ -62,6 +68,9 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
     @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<MessageVoiceItem.Holder>() {
                 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<MessageVoiceItem.Holder>() {
         }
     }
 
+    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 <onuray.sahin@gmail.com>
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 b6a8dc2cd5..6bde4ada3d 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 076c05b9c4..8167ad94af 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 <onuray.sahin@gmail.com>
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 d019cb1777..a0e8ddce3d 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 daa5631d84..091e9f7869 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 ccb51d3796..fba3b8b5d3 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 6bde4ada3d..c5d8b7a5c1 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 9a643796a9..87a2630f2a 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 8adecaad6e..f3b1fc918d 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<Int>) {
         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 3965afdbaa..9c469dfead 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 8b0b43009d..9116de92dd 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 <onuray.sahin@gmail.com>
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 fba3b8b5d3..b71398c8a2 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 <onuray.sahin@gmail.com>
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 768635b2f7..7cdb1d51d5 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 <onuray.sahin@gmail.com>
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 0000000000..2dee22f07a
--- /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 <onuray.sahin@gmail.com>
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 7cdb1d51d5..32f30fe458 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<FFT>.summarize(target: Int): List<FFT> {
+        flow = Flow.LTR
         val result = mutableListOf<FFT>()
         if (size <= target) {
             result.addAll(this)

From 3156410965eb913de7606d276962ae0dc715faef Mon Sep 17 00:00:00 2001
From: Onuray Sahin <onuray.sahin@gmail.com>
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 f3b1fc918d..7a76657923 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 865e8f80bd..e8e8927b6d 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 d1c134a743..722e0f620a 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<MessageVoiceItem.Holder>() {
         }
     }
 
-    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 <onuray.sahin@gmail.com>
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 36fbdf4788..0c89226f5a 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 <onurays@element.io>
Date: Tue, 22 Mar 2022 17:04:35 +0300
Subject: [PATCH 12/13] Code review fixes.

---
 .../detail/timeline/item/MessageVoiceItem.kt  | 74 ++++++++++---------
 1 file changed, 38 insertions(+), 36 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 722e0f620a..bbaab3959d 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<MessageVoiceItem.Holder>() {
             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<MessageVoiceItem.Holder>() {
         }
         holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
         holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
+    }
 
-        // 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 onWaveformViewReady(holder: Holder) {
+        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.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 <onurays@element.io>
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 a9b9a1d302..976489eec3 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 {