diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 230c68cb31..e1dab55979 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -139,6 +139,7 @@ import im.vector.app.features.home.room.detail.composer.TextComposerViewEvents import im.vector.app.features.home.room.detail.composer.TextComposerViewModel import im.vector.app.features.home.room.detail.composer.TextComposerViewState import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView +import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction @@ -694,15 +695,15 @@ class RoomDetailFragment @Inject constructor( private fun setupVoiceMessageView() { voiceMessagePlaybackTracker.track(VoiceMessagePlaybackTracker.RECORDING_ID, views.voiceMessageRecorderView) views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback { - override fun onVoiceRecordingStarted(): Boolean { - return if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) { + + private var currentUiState: RecordingUiState = RecordingUiState.None + + override fun onVoiceRecordingStarted() { + if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) { roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage) textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(true)) vibrate(requireContext()) - true - } else { - // Permission dialog is displayed - false + views.voiceMessageRecorderView.display(RecordingUiState.Started) } } @@ -718,6 +719,29 @@ class RoomDetailFragment @Inject constructor( override fun onVoicePlaybackButtonClicked() { roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseRecordingPlayback) } + + override fun onRecordingStopped() { + if (currentUiState != RecordingUiState.Locked && currentUiState != RecordingUiState.None) { + views.voiceMessageRecorderView.display(RecordingUiState.None) + } + } + + override fun onUiStateChanged(state: RecordingUiState) { + currentUiState = state + views.voiceMessageRecorderView.display(state) + } + + override fun sendVoiceMessage() { + views.voiceMessageRecorderView.display(RecordingUiState.None) + } + + override fun deleteVoiceMessage() { + views.voiceMessageRecorderView.display(RecordingUiState.None) + } + + override fun onRecordingLimitReached() { + views.voiceMessageRecorderView.display(RecordingUiState.Playback) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt index 4cbb96a703..5825e60ecf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt @@ -21,7 +21,7 @@ import android.view.MotionEvent import im.vector.app.R import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState -import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingState +import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState import kotlin.math.abs class DraggableStateProcessor( @@ -50,7 +50,7 @@ class DraggableStateProcessor( lastDistanceY = 0F } - fun process(event: MotionEvent, recordingState: RecordingState): RecordingState { + fun process(event: MotionEvent, recordingState: RecordingUiState): RecordingUiState { val currentX = event.rawX val currentY = event.rawY val distanceX = abs(firstX - currentX) @@ -63,9 +63,9 @@ class DraggableStateProcessor( } } - private fun nextRecordingState(recordingState: RecordingState, currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): RecordingState { + private fun nextRecordingState(recordingState: RecordingUiState, currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): RecordingUiState { return when (recordingState) { - RecordingState.Started -> { + RecordingUiState.Started -> { // Determine if cancelling or locking for the first move action. if (((currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1)) && distanceX > distanceY && distanceX > lastDistanceX) { DraggingState.Cancelling(distanceX) @@ -78,9 +78,9 @@ class DraggableStateProcessor( is DraggingState.Cancelling -> { // Check if cancelling conditions met, also check if it should be initial state if (distanceX < minimumMove && distanceX < lastDistanceX) { - RecordingState.Started + RecordingUiState.Started } else if (shouldCancelRecording(distanceX)) { - RecordingState.Cancelled + RecordingUiState.Cancelled } else { DraggingState.Cancelling(distanceX) } @@ -88,9 +88,9 @@ class DraggableStateProcessor( is DraggingState.Locking -> { // Check if locking conditions met, also check if it should be initial state if (distanceY < minimumMove && distanceY < lastDistanceY) { - RecordingState.Started + RecordingUiState.Started } else if (shouldLockRecording(distanceY)) { - RecordingState.Locked + RecordingUiState.Locked } else { DraggingState.Locking(distanceY) } 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 6bd55d4400..79898dad32 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 @@ -28,7 +28,6 @@ import im.vector.app.core.utils.CountUpTimer import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ViewVoiceMessageRecorderBinding import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker -import org.matrix.android.sdk.api.extensions.orFalse import kotlin.math.floor /** @@ -41,11 +40,15 @@ class VoiceMessageRecorderView @JvmOverloads constructor( ) : ConstraintLayout(context, attrs, defStyleAttr), VoiceMessagePlaybackTracker.Listener { interface Callback { - // Return true if the recording is started - fun onVoiceRecordingStarted(): Boolean + fun onVoiceRecordingStarted() fun onVoiceRecordingEnded(isCancelled: Boolean) fun onVoiceRecordingPlaybackModeOn() fun onVoicePlaybackButtonClicked() + fun onRecordingStopped() + fun onUiStateChanged(state: RecordingUiState) + fun sendVoiceMessage() + fun deleteVoiceMessage() + fun onRecordingLimitReached() } // We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22. @@ -54,7 +57,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( var callback: Callback? = null - private var recordingState: RecordingState = RecordingState.None + private var currentUiState: RecordingUiState = RecordingUiState.None private var recordingTicker: CountUpTimer? = null init { @@ -78,7 +81,6 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } fun initVoiceRecordingViews() { - recordingState = RecordingState.None stopRecordingTicker() voiceMessageViews.initViews(onVoiceRecordingEnded = {}) } @@ -86,36 +88,39 @@ class VoiceMessageRecorderView @JvmOverloads constructor( private fun initListeners() { voiceMessageViews.start(object : VoiceMessageViews.Actions { override fun onRequestRecording() { - if (callback?.onVoiceRecordingStarted().orFalse()) { - display(RecordingState.Started) - } + callback?.onVoiceRecordingStarted() } override fun onRecordingStopped() { - if (recordingState != RecordingState.Locked && recordingState != RecordingState.None) { - display(RecordingState.None) - } + callback?.onRecordingStopped() } - override fun isActive() = recordingState != RecordingState.Cancelled + override fun isActive() = currentUiState != RecordingUiState.Cancelled - override fun updateState(updater: (RecordingState) -> RecordingState) { - updater(recordingState).also { - display(it) + override fun updateState(updater: (RecordingUiState) -> RecordingUiState) { + updater(currentUiState).also { newState -> + when (newState) { + is DraggingState -> display(newState) + else -> { + if (newState != currentUiState) { + callback?.onUiStateChanged(newState) + } + } + } } } override fun sendMessage() { - display(RecordingState.None) + callback?.sendVoiceMessage() } override fun delete() { // this was previously marked as cancelled true - display(RecordingState.None) + callback?.deleteVoiceMessage() } override fun waveformClicked() { - display(RecordingState.Playback) + display(RecordingUiState.Playback) } override fun onVoicePlaybackButtonClicked() { @@ -124,43 +129,41 @@ class VoiceMessageRecorderView @JvmOverloads constructor( }) } - fun display(recordingState: RecordingState) { - val previousState = this.recordingState - val stateHasChanged = recordingState != this.recordingState - this.recordingState = recordingState + fun display(recordingState: RecordingUiState) { + if (recordingState == this.currentUiState) return - if (stateHasChanged) { - when (recordingState) { - RecordingState.None -> { - val isCancelled = previousState == RecordingState.Cancelled - voiceMessageViews.hideRecordingViews(recordingState, isCancelled = isCancelled) { callback?.onVoiceRecordingEnded(it) } - stopRecordingTicker() - } - RecordingState.Started -> { - startRecordingTicker() - voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast)) - voiceMessageViews.showRecordingViews() - } - RecordingState.Cancelled -> { - voiceMessageViews.hideRecordingViews(recordingState, isCancelled = true) { callback?.onVoiceRecordingEnded(it) } - vibrate(context) - } - RecordingState.Locked -> { - voiceMessageViews.renderLocked() - postDelayed({ - voiceMessageViews.showRecordingLockedViews(recordingState) { callback?.onVoiceRecordingEnded(it) } - }, 500) - } - RecordingState.Playback -> { - stopRecordingTicker() - voiceMessageViews.showPlaybackViews() - callback?.onVoiceRecordingPlaybackModeOn() - } - is DraggingState -> when (recordingState) { - is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(recordingState.distanceX) - is DraggingState.Locking -> voiceMessageViews.renderLocking(recordingState.distanceY) - }.exhaustive + val previousState = this.currentUiState + this.currentUiState = recordingState + when (recordingState) { + RecordingUiState.None -> { + val isCancelled = previousState == RecordingUiState.Cancelled + voiceMessageViews.hideRecordingViews(recordingState, isCancelled = isCancelled) { callback?.onVoiceRecordingEnded(it) } + stopRecordingTicker() } + RecordingUiState.Started -> { + startRecordingTicker() + voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast)) + voiceMessageViews.showRecordingViews() + } + RecordingUiState.Cancelled -> { + voiceMessageViews.hideRecordingViews(recordingState, isCancelled = true) { callback?.onVoiceRecordingEnded(it) } + vibrate(context) + } + RecordingUiState.Locked -> { + voiceMessageViews.renderLocked() + postDelayed({ + voiceMessageViews.showRecordingLockedViews(recordingState) { callback?.onVoiceRecordingEnded(it) } + }, 500) + } + RecordingUiState.Playback -> { + stopRecordingTicker() + voiceMessageViews.showPlaybackViews() + callback?.onVoiceRecordingPlaybackModeOn() + } + is DraggingState -> when (recordingState) { + is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(recordingState.distanceX) + is DraggingState.Locking -> voiceMessageViews.renderLocking(recordingState.distanceY) + }.exhaustive } } @@ -178,11 +181,11 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } private fun onRecordingTick(milliseconds: Long) { - voiceMessageViews.renderRecordingTimer(recordingState, milliseconds / 1_000) + voiceMessageViews.renderRecordingTimer(currentUiState, milliseconds / 1_000) val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds if (timeDiffToRecordingLimit <= 0) { post { - display(RecordingState.Playback) + callback?.onRecordingLimitReached() } } else if (timeDiffToRecordingLimit in 10_000..10_999) { post { @@ -200,7 +203,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( /** * Returns true if the voice message is recording or is in playback mode */ - fun isActive() = recordingState !in listOf(RecordingState.None, RecordingState.Cancelled) + fun isActive() = currentUiState !in listOf(RecordingUiState.None, RecordingUiState.Cancelled) override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) { when (state) { @@ -217,17 +220,16 @@ class VoiceMessageRecorderView @JvmOverloads constructor( } } - sealed interface RecordingState { - object None : RecordingState - object Started : RecordingState - object Cancelled : RecordingState - object Locked : RecordingState - object Playback : RecordingState + sealed interface RecordingUiState { + object None : RecordingUiState + object Started : RecordingUiState + object Cancelled : RecordingUiState + object Locked : RecordingUiState + object Playback : RecordingUiState } - sealed interface DraggingState : RecordingState { + sealed interface DraggingState : RecordingUiState { data class Cancelling(val distanceX: Float) : DraggingState data class Locking(val distanceY: Float) : DraggingState } } - 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 d9f5f9675b..ce4ec4b519 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 @@ -32,7 +32,7 @@ import im.vector.app.core.extensions.setAttributeTintedBackground import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ViewVoiceMessageRecorderBinding -import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingState +import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import org.matrix.android.sdk.api.extensions.orFalse @@ -155,9 +155,9 @@ class VoiceMessageViews( views.voiceMessageSendButton.isVisible = false } - fun hideRecordingViews(recordingState: RecordingState, isCancelled: Boolean?, onVoiceRecordingEnded: (Boolean) -> Unit) { + fun hideRecordingViews(recordingState: RecordingUiState, isCancelled: Boolean?, onVoiceRecordingEnded: (Boolean) -> Unit) { // We need to animate the lock image first - if (recordingState != RecordingState.Locked || isCancelled.orFalse()) { + if (recordingState != RecordingUiState.Locked || isCancelled.orFalse()) { views.voiceMessageLockImage.isVisible = false views.voiceMessageLockImage.animate().translationY(0f).start() views.voiceMessageLockBackground.isVisible = false @@ -171,7 +171,7 @@ class VoiceMessageViews( views.voiceMessageSlideToCancel.animate().translationX(0f).translationY(0f).start() views.voiceMessagePlaybackLayout.isVisible = false - if (recordingState != RecordingState.Locked) { + if (recordingState != RecordingUiState.Locked) { views.voiceMessageMicButton .animate() .scaleX(1f) @@ -203,7 +203,7 @@ class VoiceMessageViews( } // Hide toasts if user cancelled recording before the timeout of the toast. - if (recordingState == RecordingState.Cancelled || recordingState == RecordingState.None) { + if (recordingState == RecordingUiState.Cancelled || recordingState == RecordingUiState.None) { hideToast() } } @@ -266,7 +266,7 @@ class VoiceMessageViews( views.voiceMessageToast.isVisible = false } - fun showRecordingLockedViews(recordingState: RecordingState, onVoiceRecordingEnded: (Boolean) -> Unit) { + fun showRecordingLockedViews(recordingState: RecordingUiState, onVoiceRecordingEnded: (Boolean) -> Unit) { hideRecordingViews(recordingState, null, onVoiceRecordingEnded) views.voiceMessagePlaybackLayout.isVisible = true views.voiceMessagePlaybackTimerIndicator.isVisible = true @@ -283,7 +283,7 @@ class VoiceMessageViews( } fun initViews(onVoiceRecordingEnded: (Boolean) -> Unit) { - hideRecordingViews(RecordingState.None, null, onVoiceRecordingEnded) + hideRecordingViews(RecordingUiState.None, null, onVoiceRecordingEnded) views.voiceMessageMicButton.isVisible = true views.voiceMessageSendButton.isVisible = false views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() } @@ -312,9 +312,9 @@ class VoiceMessageViews( views.voiceMessageToast.isVisible = false } - fun renderRecordingTimer(recordingState: RecordingState, recordingTimeMillis: Long) { + fun renderRecordingTimer(recordingState: RecordingUiState, recordingTimeMillis: Long) { val formattedTimerText = DateUtils.formatElapsedTime(recordingTimeMillis) - if (recordingState == RecordingState.Locked) { + if (recordingState == RecordingUiState.Locked) { views.voicePlaybackTime.apply { post { text = formattedTimerText @@ -349,7 +349,7 @@ class VoiceMessageViews( fun onRequestRecording() fun onRecordingStopped() fun isActive(): Boolean - fun updateState(updater: (RecordingState) -> RecordingState) + fun updateState(updater: (RecordingUiState) -> RecordingUiState) fun sendMessage() fun delete() fun waveformClicked()