mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 01:15:54 +03:00
Pause voice broadcast if there is no network
This commit is contained in:
parent
1e951cd838
commit
fea3441bbf
9 changed files with 139 additions and 26 deletions
1
changelog.d/7890.feature
Normal file
1
changelog.d/7890.feature
Normal file
|
@ -0,0 +1 @@
|
|||
[Voice Broadcast] Handle connection errors while recording
|
|
@ -3125,6 +3125,7 @@
|
|||
<string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>
|
||||
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
|
||||
<string name="error_voice_broadcast_unable_to_play">Unable to play this voice broadcast.</string>
|
||||
<string name="error_voice_broadcast_no_connection_recording">Connection error - Recording paused</string>
|
||||
<!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
|
||||
<string name="voice_broadcast_recording_time_left">%1$s left</string>
|
||||
<string name="stop_voice_broadcast_dialog_title">Stop live broadcasting?</string>
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.Group
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
|
@ -55,11 +57,11 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
|||
}
|
||||
|
||||
override fun renderLiveIndicator(holder: Holder) {
|
||||
when (voiceBroadcastState) {
|
||||
VoiceBroadcastState.STARTED,
|
||||
VoiceBroadcastState.RESUMED -> renderPlayingLiveIndicator(holder)
|
||||
VoiceBroadcastState.PAUSED -> renderPausedLiveIndicator(holder)
|
||||
VoiceBroadcastState.STOPPED, null -> renderNoLiveIndicator(holder)
|
||||
when (recorder?.recordingState) {
|
||||
VoiceBroadcastRecorder.State.Recording -> renderPlayingLiveIndicator(holder)
|
||||
VoiceBroadcastRecorder.State.Error,
|
||||
VoiceBroadcastRecorder.State.Paused -> renderPausedLiveIndicator(holder)
|
||||
VoiceBroadcastRecorder.State.Idle, null -> renderNoLiveIndicator(holder)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +87,9 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
|||
VoiceBroadcastRecorder.State.Recording -> renderRecordingState(holder)
|
||||
VoiceBroadcastRecorder.State.Paused -> renderPausedState(holder)
|
||||
VoiceBroadcastRecorder.State.Idle -> renderStoppedState(holder)
|
||||
VoiceBroadcastRecorder.State.Error -> renderErrorState(holder, true)
|
||||
}
|
||||
renderLiveIndicator(holder)
|
||||
}
|
||||
|
||||
private fun renderVoiceBroadcastState(holder: Holder) {
|
||||
|
@ -101,6 +105,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
|||
private fun renderRecordingState(holder: Holder) = with(holder) {
|
||||
stopRecordButton.isEnabled = true
|
||||
recordButton.isEnabled = true
|
||||
renderErrorState(holder, false)
|
||||
|
||||
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
|
||||
val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
|
||||
|
@ -113,6 +118,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
|||
private fun renderPausedState(holder: Holder) = with(holder) {
|
||||
stopRecordButton.isEnabled = true
|
||||
recordButton.isEnabled = true
|
||||
renderErrorState(holder, false)
|
||||
|
||||
recordButton.setImageResource(R.drawable.ic_recording_dot)
|
||||
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
|
||||
|
@ -123,6 +129,12 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
|||
private fun renderStoppedState(holder: Holder) = with(holder) {
|
||||
recordButton.isEnabled = false
|
||||
stopRecordButton.isEnabled = false
|
||||
renderErrorState(holder, false)
|
||||
}
|
||||
|
||||
private fun renderErrorState(holder: Holder, isOnError: Boolean) = with(holder) {
|
||||
controlsGroup.isVisible = !isOnError
|
||||
errorView.isVisible = isOnError
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
|
@ -142,6 +154,8 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
|||
val remainingTimeMetadata by bind<VoiceBroadcastMetadataView>(R.id.remainingTimeMetadata)
|
||||
val recordButton by bind<ImageButton>(R.id.recordButton)
|
||||
val stopRecordButton by bind<ImageButton>(R.id.stopRecordButton)
|
||||
val errorView by bind<TextView>(R.id.errorView)
|
||||
val controlsGroup by bind<Group>(R.id.controlsGroup)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -33,6 +33,8 @@ interface VoiceBroadcastRecorder : VoiceRecorder {
|
|||
val currentRemainingTime: Long?
|
||||
|
||||
fun startRecordVoiceBroadcast(voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int)
|
||||
|
||||
fun pauseOnError()
|
||||
fun addListener(listener: Listener)
|
||||
fun removeListener(listener: Listener)
|
||||
|
||||
|
@ -46,5 +48,6 @@ interface VoiceBroadcastRecorder : VoiceRecorder {
|
|||
Recording,
|
||||
Paused,
|
||||
Idle,
|
||||
Error,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,10 +29,14 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
|||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -47,6 +51,7 @@ class VoiceBroadcastRecorderQ(
|
|||
private val sessionScope get() = session.coroutineScope
|
||||
|
||||
private var voiceBroadcastStateObserver: Job? = null
|
||||
private var syncStateObserver: Job? = null
|
||||
|
||||
private var maxFileSize = 0L // zero or negative for no limit
|
||||
private var currentVoiceBroadcast: VoiceBroadcast? = null
|
||||
|
@ -96,21 +101,36 @@ class VoiceBroadcastRecorderQ(
|
|||
observeVoiceBroadcastStateEvent(voiceBroadcast)
|
||||
}
|
||||
|
||||
override fun pauseRecord() {
|
||||
override fun startRecord(roomId: String) {
|
||||
super.startRecord(roomId)
|
||||
observeConnectionState()
|
||||
}
|
||||
|
||||
override fun pauseOnError() {
|
||||
if (recordingState != VoiceBroadcastRecorder.State.Recording) return
|
||||
tryOrNull { mediaRecorder?.stop() }
|
||||
mediaRecorder?.reset()
|
||||
|
||||
pauseRecorder()
|
||||
stopObservingConnectionState()
|
||||
recordingState = VoiceBroadcastRecorder.State.Error
|
||||
}
|
||||
|
||||
override fun pauseRecord() {
|
||||
if (recordingState !in arrayOf(VoiceBroadcastRecorder.State.Recording, VoiceBroadcastRecorder.State.Error)) return
|
||||
|
||||
pauseRecorder()
|
||||
stopObservingConnectionState()
|
||||
recordingState = VoiceBroadcastRecorder.State.Paused
|
||||
recordingTicker.pause()
|
||||
notifyOutputFileCreated()
|
||||
}
|
||||
|
||||
override fun resumeRecord() {
|
||||
if (recordingState != VoiceBroadcastRecorder.State.Paused) return
|
||||
|
||||
currentSequence++
|
||||
currentVoiceBroadcast?.let { startRecord(it.roomId) }
|
||||
recordingState = VoiceBroadcastRecorder.State.Recording
|
||||
recordingTicker.resume()
|
||||
observeConnectionState()
|
||||
}
|
||||
|
||||
override fun stopRecord() {
|
||||
|
@ -128,6 +148,8 @@ class VoiceBroadcastRecorderQ(
|
|||
voiceBroadcastStateObserver?.cancel()
|
||||
voiceBroadcastStateObserver = null
|
||||
|
||||
stopObservingConnectionState()
|
||||
|
||||
// Reset data
|
||||
currentSequence = 0
|
||||
currentMaxLength = 0
|
||||
|
@ -197,6 +219,27 @@ class VoiceBroadcastRecorderQ(
|
|||
}
|
||||
}
|
||||
|
||||
private fun pauseRecorder() {
|
||||
if (recordingState != VoiceBroadcastRecorder.State.Recording) return
|
||||
|
||||
tryOrNull { mediaRecorder?.stop() }
|
||||
mediaRecorder?.reset()
|
||||
recordingTicker.pause()
|
||||
}
|
||||
|
||||
private fun observeConnectionState() {
|
||||
syncStateObserver = session.flow().liveSyncState()
|
||||
.distinctUntilChanged()
|
||||
.filter { it is SyncState.NoNetwork }
|
||||
.onEach { pauseOnError() }
|
||||
.launchIn(sessionScope)
|
||||
}
|
||||
|
||||
private fun stopObservingConnectionState() {
|
||||
syncStateObserver?.cancel()
|
||||
syncStateObserver = null
|
||||
}
|
||||
|
||||
private inner class RecordingTicker(
|
||||
private var recordingTicker: CountUpTimer? = null,
|
||||
) {
|
||||
|
|
|
@ -16,17 +16,25 @@
|
|||
|
||||
package im.vector.app.features.voicebroadcast.recording.usecase
|
||||
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -51,25 +59,35 @@ class PauseVoiceBroadcastUseCase @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun pauseVoiceBroadcast(room: Room, reference: RelationDefaultContent?) {
|
||||
private suspend fun pauseVoiceBroadcast(room: Room, reference: RelationDefaultContent?, remainingRetry: Int = 3) {
|
||||
Timber.d("## PauseVoiceBroadcastUseCase: Send new voice broadcast info state event")
|
||||
|
||||
// save the last sequence number and immediately pause the recording
|
||||
val lastSequence = voiceBroadcastRecorder?.currentSequence
|
||||
pauseRecording()
|
||||
try {
|
||||
// save the last sequence number and immediately pause the recording
|
||||
val lastSequence = voiceBroadcastRecorder?.currentSequence
|
||||
|
||||
room.stateService().sendStateEvent(
|
||||
eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
|
||||
stateKey = session.myUserId,
|
||||
body = MessageVoiceBroadcastInfoContent(
|
||||
relatesTo = reference,
|
||||
voiceBroadcastStateStr = VoiceBroadcastState.PAUSED.value,
|
||||
lastChunkSequence = lastSequence,
|
||||
).toContent(),
|
||||
)
|
||||
}
|
||||
room.stateService().sendStateEvent(
|
||||
eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
|
||||
stateKey = session.myUserId,
|
||||
body = MessageVoiceBroadcastInfoContent(
|
||||
relatesTo = reference,
|
||||
voiceBroadcastStateStr = VoiceBroadcastState.PAUSED.value,
|
||||
lastChunkSequence = lastSequence,
|
||||
).toContent(),
|
||||
)
|
||||
|
||||
private fun pauseRecording() {
|
||||
voiceBroadcastRecorder?.pauseRecord()
|
||||
voiceBroadcastRecorder?.pauseRecord()
|
||||
} catch (e: Failure) {
|
||||
if (remainingRetry > 0) {
|
||||
voiceBroadcastRecorder?.pauseOnError()
|
||||
// Retry if there is no network issue (sync is running well)
|
||||
session.flow().liveSyncState()
|
||||
.filter { it is SyncState.Running }
|
||||
.take(1)
|
||||
.onEach { pauseVoiceBroadcast(room, reference, remainingRetry - 1) }
|
||||
.launchIn(session.coroutineScope)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||
private val buildMeta: BuildMeta,
|
||||
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
|
||||
private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase,
|
||||
private val pauseVoiceBroadcastUseCase: PauseVoiceBroadcastUseCase,
|
||||
) {
|
||||
|
||||
suspend fun execute(roomId: String): Result<Unit> = runCatching {
|
||||
|
@ -103,6 +104,14 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||
session.coroutineScope.launch { stopVoiceBroadcastUseCase.execute(room.roomId) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
||||
if (state == VoiceBroadcastRecorder.State.Error) {
|
||||
session.coroutineScope.launch {
|
||||
pauseVoiceBroadcastUseCase.execute(room.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
voiceBroadcastRecorder?.startRecordVoiceBroadcast(voiceBroadcast, chunkLength, maxLength)
|
||||
}
|
||||
|
|
|
@ -107,4 +107,27 @@
|
|||
android:contentDescription="@string/a11y_stop_voice_broadcast_record"
|
||||
android:src="@drawable/ic_stop" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/controlsGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="controllerButtonsFlow,recordButton,stopRecordButton" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorView"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="4dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/error_voice_broadcast_no_connection_recording"
|
||||
android:textColor="?colorError"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_voice_broadcast_error"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -60,7 +60,8 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
context = FakeContext().instance,
|
||||
buildMeta = mockk(),
|
||||
getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase,
|
||||
stopVoiceBroadcastUseCase = mockk()
|
||||
stopVoiceBroadcastUseCase = mockk(),
|
||||
pauseVoiceBroadcastUseCase = mockk(),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue