Adds proper handling of audio seek bar
This commit is contained in:
parent
34dcd70a64
commit
d0155c9890
|
@ -105,7 +105,6 @@ import im.vector.app.core.utils.createJSonViewerStyleProvider
|
||||||
import im.vector.app.core.utils.createUIHandler
|
import im.vector.app.core.utils.createUIHandler
|
||||||
import im.vector.app.core.utils.isValidUrl
|
import im.vector.app.core.utils.isValidUrl
|
||||||
import im.vector.app.core.utils.onPermissionDeniedDialog
|
import im.vector.app.core.utils.onPermissionDeniedDialog
|
||||||
import im.vector.app.core.utils.onPermissionDeniedSnackbar
|
|
||||||
import im.vector.app.core.utils.openLocation
|
import im.vector.app.core.utils.openLocation
|
||||||
import im.vector.app.core.utils.openUrlInExternalBrowser
|
import im.vector.app.core.utils.openUrlInExternalBrowser
|
||||||
import im.vector.app.core.utils.registerForPermissionsResult
|
import im.vector.app.core.utils.registerForPermissionsResult
|
||||||
|
@ -2080,6 +2079,10 @@ class TimelineFragment @Inject constructor(
|
||||||
messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, duration, percentage))
|
messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, duration, percentage))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAudioSeekBarMovedTo(eventId: String, duration: Int, percentage: Float) {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.AudioSeekBarMovedTo(eventId, duration, percentage))
|
||||||
|
}
|
||||||
|
|
||||||
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
||||||
when (action.messageContent) {
|
when (action.messageContent) {
|
||||||
is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
|
is MessageTextContent -> shareText(requireContext(), action.messageContent.body)
|
||||||
|
|
|
@ -40,12 +40,13 @@ import javax.inject.Inject
|
||||||
/**
|
/**
|
||||||
* Helper class to record audio for voice messages.
|
* Helper class to record audio for voice messages.
|
||||||
*/
|
*/
|
||||||
class VoiceMessageHelper @Inject constructor(
|
class AudioMessageHelper @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||||
voiceRecorderProvider: VoiceRecorderProvider
|
voiceRecorderProvider: VoiceRecorderProvider
|
||||||
) {
|
) {
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
|
private var currentPlayingId: String? = null
|
||||||
private var voiceRecorder: VoiceRecorder = voiceRecorderProvider.provideVoiceRecorder()
|
private var voiceRecorder: VoiceRecorder = voiceRecorderProvider.provideVoiceRecorder()
|
||||||
|
|
||||||
private val amplitudeList = mutableListOf<Int>()
|
private val amplitudeList = mutableListOf<Int>()
|
||||||
|
@ -136,6 +137,7 @@ class VoiceMessageHelper @Inject constructor(
|
||||||
mediaPlayer?.stop()
|
mediaPlayer?.stop()
|
||||||
stopPlaybackTicker()
|
stopPlaybackTicker()
|
||||||
stopRecordingAmplitudes()
|
stopRecordingAmplitudes()
|
||||||
|
currentPlayingId = null
|
||||||
if (playbackState is AudioMessagePlaybackTracker.Listener.State.Playing) {
|
if (playbackState is AudioMessagePlaybackTracker.Listener.State.Playing) {
|
||||||
playbackTracker.pausePlayback(id)
|
playbackTracker.pausePlayback(id)
|
||||||
} else {
|
} else {
|
||||||
|
@ -163,6 +165,7 @@ class VoiceMessageHelper @Inject constructor(
|
||||||
seekTo(currentPlaybackTime)
|
seekTo(currentPlaybackTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
currentPlayingId = id
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "Unable to start playback")
|
Timber.e(failure, "Unable to start playback")
|
||||||
throw VoiceFailure.UnableToPlay(failure)
|
throw VoiceFailure.UnableToPlay(failure)
|
||||||
|
@ -174,14 +177,21 @@ class VoiceMessageHelper @Inject constructor(
|
||||||
playbackTracker.pausePlayback(AudioMessagePlaybackTracker.RECORDING_ID)
|
playbackTracker.pausePlayback(AudioMessagePlaybackTracker.RECORDING_ID)
|
||||||
mediaPlayer?.stop()
|
mediaPlayer?.stop()
|
||||||
stopPlaybackTicker()
|
stopPlaybackTicker()
|
||||||
|
currentPlayingId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun movePlaybackTo(id: String, percentage: Float, totalDuration: Int) {
|
fun movePlaybackTo(id: String, percentage: Float, totalDuration: Int) {
|
||||||
val toMillisecond = (totalDuration * percentage).toInt()
|
val toMillisecond = (totalDuration * percentage).toInt()
|
||||||
playbackTracker.updateCurrentPlaybackTime(id, toMillisecond, percentage)
|
playbackTracker.pauseAllPlaybacks()
|
||||||
|
|
||||||
stopPlayback()
|
if (currentPlayingId == id) {
|
||||||
playbackTracker.pausePlayback(id)
|
mediaPlayer?.seekTo(toMillisecond)
|
||||||
|
playbackTracker.updatePlayingAtPlaybackTime(id, toMillisecond, percentage)
|
||||||
|
} else {
|
||||||
|
mediaPlayer?.pause()
|
||||||
|
playbackTracker.updatePausedAtPlaybackTime(id, toMillisecond, percentage)
|
||||||
|
stopPlaybackTicker()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRecordingAmplitudes() {
|
private fun startRecordingAmplitudes() {
|
||||||
|
@ -233,7 +243,7 @@ class VoiceMessageHelper @Inject constructor(
|
||||||
val currentPosition = mediaPlayer?.currentPosition ?: 0
|
val currentPosition = mediaPlayer?.currentPosition ?: 0
|
||||||
val totalDuration = mediaPlayer?.duration ?: 0
|
val totalDuration = mediaPlayer?.duration ?: 0
|
||||||
val percentage = currentPosition.toFloat() / totalDuration
|
val percentage = currentPosition.toFloat() / totalDuration
|
||||||
playbackTracker.updateCurrentPlaybackTime(id, currentPosition, percentage)
|
playbackTracker.updatePlayingAtPlaybackTime(id, currentPosition, percentage)
|
||||||
} else {
|
} else {
|
||||||
playbackTracker.stopPlayback(id)
|
playbackTracker.stopPlayback(id)
|
||||||
stopPlaybackTicker()
|
stopPlaybackTicker()
|
|
@ -42,4 +42,5 @@ sealed class MessageComposerAction : VectorViewModelAction {
|
||||||
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction()
|
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction()
|
||||||
data class VoiceWaveformTouchedUp(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
|
data class VoiceWaveformTouchedUp(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
|
||||||
data class VoiceWaveformMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
|
data class VoiceWaveformMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
|
||||||
|
data class AudioSeekBarMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val commandParser: CommandParser,
|
private val commandParser: CommandParser,
|
||||||
private val rainbowGenerator: RainbowGenerator,
|
private val rainbowGenerator: RainbowGenerator,
|
||||||
private val voiceMessageHelper: VoiceMessageHelper,
|
private val audioMessageHelper: AudioMessageHelper,
|
||||||
private val analyticsTracker: AnalyticsTracker,
|
private val analyticsTracker: AnalyticsTracker,
|
||||||
private val voicePlayerHelper: VoicePlayerHelper
|
private val voicePlayerHelper: VoicePlayerHelper
|
||||||
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
|
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
|
||||||
|
@ -90,7 +90,6 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: MessageComposerAction) {
|
override fun handle(action: MessageComposerAction) {
|
||||||
Timber.v("Handle action: $action")
|
|
||||||
when (action) {
|
when (action) {
|
||||||
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
|
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
|
||||||
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
|
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
|
||||||
|
@ -110,6 +109,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||||
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
|
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
|
||||||
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
|
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
|
||||||
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
|
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
|
||||||
|
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -811,18 +811,18 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
private fun handleStartRecordingVoiceMessage() {
|
private fun handleStartRecordingVoiceMessage() {
|
||||||
try {
|
try {
|
||||||
voiceMessageHelper.startRecording(room.roomId)
|
audioMessageHelper.startRecording(room.roomId)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) {
|
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) {
|
||||||
voiceMessageHelper.stopPlayback()
|
audioMessageHelper.stopPlayback()
|
||||||
if (isCancelled) {
|
if (isCancelled) {
|
||||||
voiceMessageHelper.deleteRecording()
|
audioMessageHelper.deleteRecording()
|
||||||
} else {
|
} else {
|
||||||
voiceMessageHelper.stopRecording(convertForSending = true)?.let { audioType ->
|
audioMessageHelper.stopRecording(convertForSending = true)?.let { audioType ->
|
||||||
if (audioType.duration > 1000) {
|
if (audioType.duration > 1000) {
|
||||||
room.sendMedia(
|
room.sendMedia(
|
||||||
attachment = audioType.toContentAttachmentData(isVoiceMessage = true),
|
attachment = audioType.toContentAttachmentData(isVoiceMessage = true),
|
||||||
|
@ -830,7 +830,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||||
roomIds = emptySet(),
|
roomIds = emptySet(),
|
||||||
rootThreadEventId = rootThreadEventId)
|
rootThreadEventId = rootThreadEventId)
|
||||||
} else {
|
} else {
|
||||||
voiceMessageHelper.deleteRecording()
|
audioMessageHelper.deleteRecording()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -845,7 +845,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||||
// Conversion can fail, fallback to the original file in this case and let the player fail for us
|
// Conversion can fail, fallback to the original file in this case and let the player fail for us
|
||||||
val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile
|
val convertedFile = voicePlayerHelper.convertFile(audioFile) ?: audioFile
|
||||||
// Play can fail
|
// Play can fail
|
||||||
voiceMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
|
audioMessageHelper.startOrPausePlayback(action.eventId, convertedFile)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
||||||
}
|
}
|
||||||
|
@ -853,34 +853,38 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePlayOrPauseRecordingPlayback() {
|
private fun handlePlayOrPauseRecordingPlayback() {
|
||||||
voiceMessageHelper.startOrPauseRecordingPlayback()
|
audioMessageHelper.startOrPauseRecordingPlayback()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
|
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
|
||||||
voiceMessageHelper.clearTracker()
|
audioMessageHelper.clearTracker()
|
||||||
voiceMessageHelper.stopAllVoiceActions(deleteRecord)
|
audioMessageHelper.stopAllVoiceActions(deleteRecord)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
|
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
|
||||||
voiceMessageHelper.initializeRecorder(attachmentData)
|
audioMessageHelper.initializeRecorder(attachmentData)
|
||||||
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
|
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePauseRecordingVoiceMessage() {
|
private fun handlePauseRecordingVoiceMessage() {
|
||||||
voiceMessageHelper.pauseRecording()
|
audioMessageHelper.pauseRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) {
|
private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) {
|
||||||
voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) {
|
private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) {
|
||||||
voiceMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleAudioSeekBarMovedTo(action: MessageComposerAction.AudioSeekBarMovedTo) {
|
||||||
|
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEntersBackground(composerText: String) {
|
private fun handleEntersBackground(composerText: String) {
|
||||||
// Always stop all voice actions. It may be playing in timeline or active recording
|
// Always stop all voice actions. It may be playing in timeline or active recording
|
||||||
val playingAudioContent = voiceMessageHelper.stopAllVoiceActions(deleteRecord = false)
|
val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
|
||||||
|
|
||||||
val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
|
val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
|
||||||
if (isVoiceRecording) {
|
if (isVoiceRecording) {
|
||||||
|
|
|
@ -148,6 +148,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float)
|
fun onVoiceWaveformTouchedUp(eventId: String, duration: Int, percentage: Float)
|
||||||
fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float)
|
fun onVoiceWaveformMovedTo(eventId: String, duration: Int, percentage: Float)
|
||||||
|
|
||||||
|
fun onAudioSeekBarMovedTo(eventId: String, duration: Int, percentage: Float)
|
||||||
|
|
||||||
fun onAddMoreReaction(event: TimelineEvent)
|
fun onAddMoreReaction(event: TimelineEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -341,6 +341,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
): MessageAudioItem {
|
): MessageAudioItem {
|
||||||
val fileUrl = getAudioFileUrl(messageContent, informationData)
|
val fileUrl = getAudioFileUrl(messageContent, informationData)
|
||||||
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
|
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
|
||||||
|
val duration = messageContent.audioInfo?.duration ?: 0
|
||||||
|
|
||||||
return MessageAudioItem_()
|
return MessageAudioItem_()
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
|
@ -349,6 +350,8 @@ class MessageItemFactory @Inject constructor(
|
||||||
.playbackControlButtonClickListener(playbackControlButtonClickListener)
|
.playbackControlButtonClickListener(playbackControlButtonClickListener)
|
||||||
.audioMessagePlaybackTracker(audioMessagePlaybackTracker)
|
.audioMessagePlaybackTracker(audioMessagePlaybackTracker)
|
||||||
.isLocalFile(localFilesHelper.isLocalFile(fileUrl))
|
.isLocalFile(localFilesHelper.isLocalFile(fileUrl))
|
||||||
|
.fileSize(messageContent.audioInfo?.size ?: 0L)
|
||||||
|
.onSeek { params.callback?.onAudioSeekBarMovedTo(informationData.eventId, duration, it) }
|
||||||
.mxcUrl(fileUrl)
|
.mxcUrl(fileUrl)
|
||||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||||
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
|
||||||
|
|
|
@ -104,10 +104,14 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
setState(id, Listener.State.Idle)
|
setState(id, Listener.State.Idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCurrentPlaybackTime(id: String, time: Int, percentage: Float) {
|
fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) {
|
||||||
setState(id, Listener.State.Playing(time, percentage))
|
setState(id, Listener.State.Playing(time, percentage))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updatePausedAtPlaybackTime(id: String, time: Int, percentage: Float) {
|
||||||
|
setState(id, Listener.State.Paused(time, percentage))
|
||||||
|
}
|
||||||
|
|
||||||
fun updateCurrentRecording(id: String, amplitudeList: List<Int>) {
|
fun updateCurrentRecording(id: String, amplitudeList: List<Int>) {
|
||||||
setState(id, Listener.State.Recording(amplitudeList))
|
setState(id, Listener.State.Recording(amplitudeList))
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.graphics.Paint
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
|
import android.widget.SeekBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
@ -29,6 +30,7 @@ import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.epoxy.ClickListener
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
import im.vector.app.core.epoxy.onClick
|
import im.vector.app.core.epoxy.onClick
|
||||||
|
import im.vector.app.core.utils.TextUtils
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||||
|
@ -47,10 +49,16 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var duration: Int = 0
|
var duration: Int = 0
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var fileSize: Long = 0
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
@JvmField
|
@JvmField
|
||||||
var isLocalFile = false
|
var isLocalFile = false
|
||||||
|
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
|
var onSeek: ((percentage: Float) -> Unit)? = null
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
||||||
|
|
||||||
|
@ -63,12 +71,15 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
|
lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
|
||||||
|
|
||||||
|
private var isUserSeeking = false
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
renderSendState(holder.rootLayout, null)
|
renderSendState(holder.rootLayout, null)
|
||||||
bindFilenameViewAttributes(holder)
|
bindViewAttributes(holder)
|
||||||
bindUploadState(holder)
|
bindUploadState(holder)
|
||||||
applyLayoutTint(holder)
|
applyLayoutTint(holder)
|
||||||
|
bindSeekBar(holder)
|
||||||
holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
|
holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
|
||||||
renderStateBasedOnAudioPlayback(holder)
|
renderStateBasedOnAudioPlayback(holder)
|
||||||
}
|
}
|
||||||
|
@ -93,10 +104,30 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
||||||
holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
|
holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindFilenameViewAttributes(holder: Holder) {
|
private fun bindViewAttributes(holder: Holder) {
|
||||||
holder.filenameView.text = filename
|
holder.filenameView.text = filename
|
||||||
holder.filenameView.onClick(attributes.itemClickListener)
|
holder.filenameView.onClick(attributes.itemClickListener)
|
||||||
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
|
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
|
||||||
|
holder.audioPlaybackDuration.text = formatPlaybackTime(duration)
|
||||||
|
holder.fileSize.text = TextUtils.formatFileSize(holder.rootLayout.context, fileSize, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindSeekBar(holder: Holder) {
|
||||||
|
holder.audioSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
|
holder.audioPlaybackTime.text = formatPlaybackTime(
|
||||||
|
(duration * (progress.toFloat() / 100)).toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||||
|
isUserSeeking = true
|
||||||
|
}
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
|
isUserSeeking = false
|
||||||
|
val percentage = seekBar.progress.toFloat() / 100
|
||||||
|
onSeek?.invoke(percentage)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
||||||
|
@ -117,13 +148,18 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
||||||
holder.audioPlaybackControlButton.contentDescription =
|
holder.audioPlaybackControlButton.contentDescription =
|
||||||
holder.view.context.getString(R.string.a11y_play_audio_message, filename)
|
holder.view.context.getString(R.string.a11y_play_audio_message, filename)
|
||||||
holder.audioPlaybackTime.text = formatPlaybackTime(duration)
|
holder.audioPlaybackTime.text = formatPlaybackTime(duration)
|
||||||
|
holder.audioSeekBar.progress = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) {
|
private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) {
|
||||||
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
|
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||||
holder.audioPlaybackControlButton.contentDescription =
|
holder.audioPlaybackControlButton.contentDescription =
|
||||||
holder.view.context.getString(R.string.a11y_pause_audio_message, filename)
|
holder.view.context.getString(R.string.a11y_pause_audio_message, filename)
|
||||||
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
|
||||||
|
if (!isUserSeeking) {
|
||||||
|
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||||
|
holder.audioSeekBar.progress = (state.percentage * 100).toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused) {
|
private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused) {
|
||||||
|
@ -131,6 +167,7 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
||||||
holder.audioPlaybackControlButton.contentDescription =
|
holder.audioPlaybackControlButton.contentDescription =
|
||||||
holder.view.context.getString(R.string.a11y_play_audio_message, filename)
|
holder.view.context.getString(R.string.a11y_play_audio_message, filename)
|
||||||
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||||
|
holder.audioSeekBar.progress = (state.percentage * 100).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
|
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
|
||||||
|
@ -151,6 +188,9 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
||||||
val audioPlaybackControlButton by bind<ImageButton>(R.id.audioPlaybackControlButton)
|
val audioPlaybackControlButton by bind<ImageButton>(R.id.audioPlaybackControlButton)
|
||||||
val audioPlaybackTime by bind<TextView>(R.id.audioPlaybackTime)
|
val audioPlaybackTime by bind<TextView>(R.id.audioPlaybackTime)
|
||||||
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
|
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
|
||||||
|
val fileSize by bind<TextView>(R.id.fileSize)
|
||||||
|
val audioPlaybackDuration by bind<TextView>(R.id.audioPlaybackDuration)
|
||||||
|
val audioSeekBar by bind<SeekBar>(R.id.audioSeekBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -45,10 +45,11 @@
|
||||||
tools:text="Filename.mp3" />
|
tools:text="Filename.mp3" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/audioPlaybackTime"
|
android:id="@+id/audioPlaybackDuration"
|
||||||
style="@style/Widget.Vector.TextView.Body"
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
app:layout_constraintStart_toStartOf="@id/messageFilenameView"
|
app:layout_constraintStart_toStartOf="@id/messageFilenameView"
|
||||||
app:layout_constraintTop_toBottomOf="@id/messageFilenameView"
|
app:layout_constraintTop_toBottomOf="@id/messageFilenameView"
|
||||||
|
@ -61,8 +62,8 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="4dp"
|
||||||
app:layout_constraintStart_toEndOf="@id/audioPlaybackTime"
|
app:layout_constraintStart_toEndOf="@id/audioPlaybackDuration"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/audioPlaybackTime"
|
app:layout_constraintBottom_toBottomOf="@id/audioPlaybackDuration"
|
||||||
tools:text="(2MB)" />
|
tools:text="(2MB)" />
|
||||||
|
|
||||||
<SeekBar
|
<SeekBar
|
||||||
|
@ -74,13 +75,13 @@
|
||||||
android:progressDrawable="@drawable/bg_seek_bar"
|
android:progressDrawable="@drawable/bg_seek_bar"
|
||||||
android:thumbTint="?vctr_content_tertiary"
|
android:thumbTint="?vctr_content_tertiary"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/audioPlaybackDuration"
|
app:layout_constraintEnd_toStartOf="@id/audioPlaybackTime"
|
||||||
app:layout_constraintTop_toBottomOf="@id/audioPlaybackControlButton"
|
app:layout_constraintTop_toBottomOf="@id/audioPlaybackControlButton"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
tools:progress="40" />
|
tools:progress="40" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/audioPlaybackDuration"
|
android:id="@+id/audioPlaybackTime"
|
||||||
style="@style/Widget.Vector.TextView.Body"
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
Loading…
Reference in New Issue