mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +03:00
Merge pull request #4556 from vector-im/feature/adm/voice-rotation
Supporting rotation during voice recordings
This commit is contained in:
commit
b59ae53805
10 changed files with 105 additions and 55 deletions
1
changelog.d/4067.bugfix
Normal file
1
changelog.d/4067.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Allow voice messages to continue recording during device rotation
|
|
@ -42,6 +42,7 @@ import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
|
||||||
import im.vector.app.features.home.UnreadMessagesSharedViewModel
|
import im.vector.app.features.home.UnreadMessagesSharedViewModel
|
||||||
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
|
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailViewModel
|
import im.vector.app.features.home.room.detail.RoomDetailViewModel
|
||||||
|
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
|
||||||
import im.vector.app.features.home.room.detail.search.SearchViewModel
|
import im.vector.app.features.home.room.detail.search.SearchViewModel
|
||||||
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
|
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
|
||||||
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
|
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
|
||||||
|
@ -508,6 +509,11 @@ interface MavericksViewModelModule {
|
||||||
@MavericksViewModelKey(RoomDetailViewModel::class)
|
@MavericksViewModelKey(RoomDetailViewModel::class)
|
||||||
fun roomDetailViewModelFactory(factory: RoomDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
fun roomDetailViewModelFactory(factory: RoomDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@MavericksViewModelKey(MessageComposerViewModel::class)
|
||||||
|
fun messageComposerViewModelFactory(factory: MessageComposerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@MavericksViewModelKey(SetIdentityServerViewModel::class)
|
@MavericksViewModelKey(SetIdentityServerViewModel::class)
|
||||||
|
|
|
@ -29,6 +29,8 @@ import dagger.hilt.components.SingletonComponent
|
||||||
import im.vector.app.core.dispatchers.CoroutineDispatchers
|
import im.vector.app.core.dispatchers.CoroutineDispatchers
|
||||||
import im.vector.app.core.error.DefaultErrorFormatter
|
import im.vector.app.core.error.DefaultErrorFormatter
|
||||||
import im.vector.app.core.error.ErrorFormatter
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
|
import im.vector.app.core.time.Clock
|
||||||
|
import im.vector.app.core.time.DefaultClock
|
||||||
import im.vector.app.features.invite.AutoAcceptInvites
|
import im.vector.app.features.invite.AutoAcceptInvites
|
||||||
import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
|
import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
|
||||||
import im.vector.app.features.navigation.DefaultNavigator
|
import im.vector.app.features.navigation.DefaultNavigator
|
||||||
|
@ -66,6 +68,9 @@ abstract class VectorBindModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindAutoAcceptInvites(autoAcceptInvites: CompileTimeAutoAcceptInvites): AutoAcceptInvites
|
abstract fun bindAutoAcceptInvites(autoAcceptInvites: CompileTimeAutoAcceptInvites): AutoAcceptInvites
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindDefaultClock(clock: DefaultClock): Clock
|
||||||
}
|
}
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
|
36
vector/src/main/java/im/vector/app/core/time/Clock.kt
Normal file
36
vector/src/main/java/im/vector/app/core/time/Clock.kt
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.core.time
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface Clock {
|
||||||
|
fun epochMillis(): Long
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultClock @Inject constructor() : Clock {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a UTC epoch in milliseconds
|
||||||
|
*
|
||||||
|
* This value is not guaranteed to be correct with reality
|
||||||
|
* as a User can override the system time and date to any values.
|
||||||
|
*/
|
||||||
|
override fun epochMillis(): Long {
|
||||||
|
return System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,6 +87,7 @@ import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.core.platform.lifecycleAwareLazy
|
import im.vector.app.core.platform.lifecycleAwareLazy
|
||||||
import im.vector.app.core.platform.showOptimizedSnackbar
|
import im.vector.app.core.platform.showOptimizedSnackbar
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
|
import im.vector.app.core.time.Clock
|
||||||
import im.vector.app.core.ui.views.CurrentCallsView
|
import im.vector.app.core.ui.views.CurrentCallsView
|
||||||
import im.vector.app.core.ui.views.CurrentCallsViewPresenter
|
import im.vector.app.core.ui.views.CurrentCallsViewPresenter
|
||||||
import im.vector.app.core.ui.views.FailedMessagesWarningView
|
import im.vector.app.core.ui.views.FailedMessagesWarningView
|
||||||
|
@ -240,7 +241,6 @@ class RoomDetailFragment @Inject constructor(
|
||||||
autoCompleterFactory: AutoCompleter.Factory,
|
autoCompleterFactory: AutoCompleter.Factory,
|
||||||
private val permalinkHandler: PermalinkHandler,
|
private val permalinkHandler: PermalinkHandler,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
val messageComposerViewModelFactory: MessageComposerViewModel.Factory,
|
|
||||||
private val eventHtmlRenderer: EventHtmlRenderer,
|
private val eventHtmlRenderer: EventHtmlRenderer,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
|
@ -251,7 +251,8 @@ class RoomDetailFragment @Inject constructor(
|
||||||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
|
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
|
||||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||||
private val callManager: WebRtcCallManager,
|
private val callManager: WebRtcCallManager,
|
||||||
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker
|
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
|
||||||
|
private val clock: Clock
|
||||||
) :
|
) :
|
||||||
VectorBaseFragment<FragmentRoomDetailBinding>(),
|
VectorBaseFragment<FragmentRoomDetailBinding>(),
|
||||||
TimelineEventController.Callback,
|
TimelineEventController.Callback,
|
||||||
|
@ -393,8 +394,8 @@ class RoomDetailFragment @Inject constructor(
|
||||||
when (mode) {
|
when (mode) {
|
||||||
is SendMode.Regular -> renderRegularMode(mode.text)
|
is SendMode.Regular -> renderRegularMode(mode.text)
|
||||||
is SendMode.Edit -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
|
is SendMode.Edit -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
|
||||||
is SendMode.Quote -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
|
is SendMode.Quote -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
|
||||||
is SendMode.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
|
is SendMode.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -700,7 +701,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
|
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
|
||||||
vibrate(requireContext())
|
vibrate(requireContext())
|
||||||
updateRecordingUiState(RecordingUiState.Started)
|
updateRecordingUiState(RecordingUiState.Started(clock.epochMillis()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -714,7 +715,9 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoiceRecordingLocked() {
|
override fun onVoiceRecordingLocked() {
|
||||||
updateRecordingUiState(RecordingUiState.Locked)
|
val startedState = withState(messageComposerViewModel) { it.voiceRecordingUiState as? RecordingUiState.Started }
|
||||||
|
val startTime = startedState?.recordingStartTimestamp ?: clock.epochMillis()
|
||||||
|
updateRecordingUiState(RecordingUiState.Locked(startTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoiceRecordingEnded() {
|
override fun onVoiceRecordingEnded() {
|
||||||
|
@ -1130,14 +1133,17 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
notificationDrawerManager.setCurrentRoom(null)
|
notificationDrawerManager.setCurrentRoom(null)
|
||||||
|
voiceMessagePlaybackTracker.unTrack(VoiceMessagePlaybackTracker.RECORDING_ID)
|
||||||
|
|
||||||
messageComposerViewModel.handle(MessageComposerAction.SaveDraft(views.composerLayout.text.toString()))
|
if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
|
||||||
|
// we're rotating, maintain any active recordings
|
||||||
// We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
|
} else {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.EndAllVoiceActions(deleteRecord = false))
|
messageComposerViewModel.handle(MessageComposerAction.SaveDraft(views.composerLayout.text.toString()))
|
||||||
views.voiceMessageRecorderView.render(RecordingUiState.None)
|
// We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.EndAllVoiceActions(deleteRecord = false))
|
||||||
|
views.voiceMessageRecorderView.render(RecordingUiState.None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
|
private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.composer
|
package im.vector.app.features.home.room.detail.composer
|
||||||
|
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
@ -30,7 +30,6 @@ import im.vector.app.features.attachments.toContentAttachmentData
|
||||||
import im.vector.app.features.command.CommandParser
|
import im.vector.app.features.command.CommandParser
|
||||||
import im.vector.app.features.command.ParsedCommand
|
import im.vector.app.features.command.ParsedCommand
|
||||||
import im.vector.app.features.home.room.detail.ChatEffect
|
import im.vector.app.features.home.room.detail.ChatEffect
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailFragment
|
|
||||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||||
import im.vector.app.features.home.room.detail.toMessageType
|
import im.vector.app.features.home.room.detail.toMessageType
|
||||||
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
||||||
|
@ -764,23 +763,9 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory : MavericksAssistedViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
|
||||||
fun create(initialState: MessageComposerViewState): MessageComposerViewModel
|
override fun create(initialState: MessageComposerViewState): MessageComposerViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
companion object : MavericksViewModelFactory<MessageComposerViewModel, MessageComposerViewState> by hiltMavericksViewModelFactory()
|
||||||
* We're unable to create this ViewModel with `by hiltMavericksViewModelFactory()` due to the
|
|
||||||
* VoiceMessagePlaybackTracker being ActivityScoped
|
|
||||||
*
|
|
||||||
* This factory allows us to provide the ViewModel instance from the Fragment directly
|
|
||||||
* bypassing the Singleton scope requirement
|
|
||||||
*/
|
|
||||||
companion object : MavericksViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: MessageComposerViewState): MessageComposerViewModel {
|
|
||||||
val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
|
||||||
return fragment.messageComposerViewModelFactory.create(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,8 @@ data class MessageComposerViewState(
|
||||||
VoiceMessageRecorderView.RecordingUiState.None,
|
VoiceMessageRecorderView.RecordingUiState.None,
|
||||||
VoiceMessageRecorderView.RecordingUiState.Cancelled,
|
VoiceMessageRecorderView.RecordingUiState.Cancelled,
|
||||||
VoiceMessageRecorderView.RecordingUiState.Playback -> false
|
VoiceMessageRecorderView.RecordingUiState.Playback -> false
|
||||||
VoiceMessageRecorderView.RecordingUiState.Locked,
|
is VoiceMessageRecorderView.RecordingUiState.Locked,
|
||||||
VoiceMessageRecorderView.RecordingUiState.Started -> true
|
is VoiceMessageRecorderView.RecordingUiState.Started -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
val isVoiceMessageIdle = !isVoiceRecording
|
val isVoiceMessageIdle = !isVoiceRecording
|
||||||
|
|
|
@ -20,19 +20,23 @@ import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.hardware.vibrate
|
import im.vector.app.core.hardware.vibrate
|
||||||
|
import im.vector.app.core.time.Clock
|
||||||
import im.vector.app.core.utils.CountUpTimer
|
import im.vector.app.core.utils.CountUpTimer
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
|
import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates the voice message recording view and animations.
|
* Encapsulates the voice message recording view and animations.
|
||||||
*/
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
class VoiceMessageRecorderView @JvmOverloads constructor(
|
class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
|
@ -51,6 +55,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||||
fun onRecordingWaveformClicked()
|
fun onRecordingWaveformClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Inject lateinit var clock: Clock
|
||||||
|
|
||||||
// We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22.
|
// We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22.
|
||||||
@Suppress("UNNECESSARY_LATEINIT")
|
@Suppress("UNNECESSARY_LATEINIT")
|
||||||
private lateinit var voiceMessageViews: VoiceMessageViews
|
private lateinit var voiceMessageViews: VoiceMessageViews
|
||||||
|
@ -105,32 +111,35 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||||
|
|
||||||
fun render(recordingState: RecordingUiState) {
|
fun render(recordingState: RecordingUiState) {
|
||||||
if (lastKnownState == recordingState) return
|
if (lastKnownState == recordingState) return
|
||||||
lastKnownState = recordingState
|
|
||||||
when (recordingState) {
|
when (recordingState) {
|
||||||
RecordingUiState.None -> {
|
RecordingUiState.None -> {
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
RecordingUiState.Started -> {
|
is RecordingUiState.Started -> {
|
||||||
startRecordingTicker()
|
startRecordingTicker(startFromLocked = false, startAt = recordingState.recordingStartTimestamp)
|
||||||
voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
|
voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
|
||||||
voiceMessageViews.showRecordingViews()
|
voiceMessageViews.showRecordingViews()
|
||||||
dragState = DraggingState.Ready
|
dragState = DraggingState.Ready
|
||||||
}
|
}
|
||||||
RecordingUiState.Cancelled -> {
|
RecordingUiState.Cancelled -> {
|
||||||
reset()
|
reset()
|
||||||
vibrate(context)
|
vibrate(context)
|
||||||
}
|
}
|
||||||
RecordingUiState.Locked -> {
|
is RecordingUiState.Locked -> {
|
||||||
|
if (lastKnownState == null) {
|
||||||
|
startRecordingTicker(startFromLocked = true, startAt = recordingState.recordingStartTimestamp)
|
||||||
|
}
|
||||||
voiceMessageViews.renderLocked()
|
voiceMessageViews.renderLocked()
|
||||||
postDelayed({
|
postDelayed({
|
||||||
voiceMessageViews.showRecordingLockedViews(recordingState)
|
voiceMessageViews.showRecordingLockedViews(recordingState)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
RecordingUiState.Playback -> {
|
RecordingUiState.Playback -> {
|
||||||
stopRecordingTicker()
|
stopRecordingTicker()
|
||||||
voiceMessageViews.showPlaybackViews()
|
voiceMessageViews.showPlaybackViews()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lastKnownState = recordingState
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reset() {
|
private fun reset() {
|
||||||
|
@ -140,6 +149,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDrag(currentDragState: DraggingState, newDragState: DraggingState) {
|
private fun onDrag(currentDragState: DraggingState, newDragState: DraggingState) {
|
||||||
|
if (currentDragState == newDragState) return
|
||||||
when (newDragState) {
|
when (newDragState) {
|
||||||
is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(newDragState.distanceX)
|
is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(newDragState.distanceX)
|
||||||
is DraggingState.Locking -> {
|
is DraggingState.Locking -> {
|
||||||
|
@ -158,22 +168,23 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||||
dragState = newDragState
|
dragState = newDragState
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRecordingTicker() {
|
private fun startRecordingTicker(startFromLocked: Boolean, startAt: Long) {
|
||||||
|
val startMs = ((clock.epochMillis() - startAt)).coerceAtLeast(0)
|
||||||
recordingTicker?.stop()
|
recordingTicker?.stop()
|
||||||
recordingTicker = CountUpTimer().apply {
|
recordingTicker = CountUpTimer().apply {
|
||||||
tickListener = object : CountUpTimer.TickListener {
|
tickListener = object : CountUpTimer.TickListener {
|
||||||
override fun onTick(milliseconds: Long) {
|
override fun onTick(milliseconds: Long) {
|
||||||
onRecordingTick(milliseconds)
|
val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
|
||||||
|
onRecordingTick(isLocked, milliseconds + startMs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resume()
|
resume()
|
||||||
}
|
}
|
||||||
onRecordingTick(0L)
|
onRecordingTick(startFromLocked, milliseconds = startMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRecordingTick(milliseconds: Long) {
|
private fun onRecordingTick(isLocked: Boolean, milliseconds: Long) {
|
||||||
val currentState = lastKnownState ?: return
|
voiceMessageViews.renderRecordingTimer(isLocked, milliseconds / 1_000)
|
||||||
voiceMessageViews.renderRecordingTimer(currentState, milliseconds / 1_000)
|
|
||||||
val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds
|
val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds
|
||||||
if (timeDiffToRecordingLimit <= 0) {
|
if (timeDiffToRecordingLimit <= 0) {
|
||||||
post {
|
post {
|
||||||
|
@ -210,9 +221,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||||
|
|
||||||
sealed interface RecordingUiState {
|
sealed interface RecordingUiState {
|
||||||
object None : RecordingUiState
|
object None : RecordingUiState
|
||||||
object Started : RecordingUiState
|
data class Started(val recordingStartTimestamp: Long) : RecordingUiState
|
||||||
object Cancelled : RecordingUiState
|
object Cancelled : RecordingUiState
|
||||||
object Locked : RecordingUiState
|
data class Locked(val recordingStartTimestamp: Long) : RecordingUiState
|
||||||
object Playback : RecordingUiState
|
object Playback : RecordingUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,7 @@ class VoiceMessageViews(
|
||||||
|
|
||||||
fun hideRecordingViews(recordingState: RecordingUiState) {
|
fun hideRecordingViews(recordingState: RecordingUiState) {
|
||||||
// We need to animate the lock image first
|
// We need to animate the lock image first
|
||||||
if (recordingState != RecordingUiState.Locked) {
|
if (recordingState !is RecordingUiState.Locked) {
|
||||||
views.voiceMessageLockImage.isVisible = false
|
views.voiceMessageLockImage.isVisible = false
|
||||||
views.voiceMessageLockImage.animate().translationY(0f).start()
|
views.voiceMessageLockImage.animate().translationY(0f).start()
|
||||||
views.voiceMessageLockBackground.isVisible = false
|
views.voiceMessageLockBackground.isVisible = false
|
||||||
|
@ -171,7 +171,7 @@ class VoiceMessageViews(
|
||||||
views.voiceMessageTimerIndicator.isVisible = false
|
views.voiceMessageTimerIndicator.isVisible = false
|
||||||
views.voiceMessageTimer.isVisible = false
|
views.voiceMessageTimer.isVisible = false
|
||||||
|
|
||||||
if (recordingState != RecordingUiState.Locked) {
|
if (recordingState !is RecordingUiState.Locked) {
|
||||||
views.voiceMessageMicButton
|
views.voiceMessageMicButton
|
||||||
.animate()
|
.animate()
|
||||||
.scaleX(1f)
|
.scaleX(1f)
|
||||||
|
@ -304,9 +304,9 @@ class VoiceMessageViews(
|
||||||
views.voiceMessageToast.isVisible = false
|
views.voiceMessageToast.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun renderRecordingTimer(recordingState: RecordingUiState, recordingTimeMillis: Long) {
|
fun renderRecordingTimer(isLocked: Boolean, recordingTimeMillis: Long) {
|
||||||
val formattedTimerText = DateUtils.formatElapsedTime(recordingTimeMillis)
|
val formattedTimerText = DateUtils.formatElapsedTime(recordingTimeMillis)
|
||||||
if (recordingState == RecordingUiState.Locked) {
|
if (isLocked) {
|
||||||
views.voicePlaybackTime.apply {
|
views.voicePlaybackTime.apply {
|
||||||
post {
|
post {
|
||||||
text = formattedTimerText
|
text = formattedTimerText
|
||||||
|
|
|
@ -18,10 +18,10 @@ package im.vector.app.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import dagger.hilt.android.scopes.ActivityScoped
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ActivityScoped
|
@Singleton
|
||||||
class VoiceMessagePlaybackTracker @Inject constructor() {
|
class VoiceMessagePlaybackTracker @Inject constructor() {
|
||||||
|
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
Loading…
Reference in a new issue