diff --git a/changelog.d/7890.feature b/changelog.d/7890.feature
new file mode 100644
index 0000000000..d86e01c36c
--- /dev/null
+++ b/changelog.d/7890.feature
@@ -0,0 +1 @@
+[Voice Broadcast] Handle connection errors while recording
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 8b51a6f95b..852478a173 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3125,6 +3125,7 @@
Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.
You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
Unable to play this voice broadcast.
+ Connection error - Recording paused
%1$s left
Stop live broadcasting?
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
index 39d2d73c68..abf14c0867 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt
@@ -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(R.id.remainingTimeMetadata)
val recordButton by bind(R.id.recordButton)
val stopRecordButton by bind(R.id.stopRecordButton)
+ val errorView by bind(R.id.errorView)
+ val controlsGroup by bind(R.id.controlsGroup)
}
companion object {
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt
index 00e4bb17dd..4f8b614e3a 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt
@@ -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,
}
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
index 2da807293f..7ca6ab3c9c 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
@@ -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,
) {
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
index 0b22d7adf5..ee51f8280b 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
@@ -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
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
index 87ea49cece..d807c67f74 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
@@ -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 = 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)
}
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml
index 2bac6a8e42..ebf4618692 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml
@@ -107,4 +107,27 @@
android:contentDescription="@string/a11y_stop_voice_broadcast_record"
android:src="@drawable/ic_stop" />
+
+
+
+
diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt
index 5dfdd379e0..9aa0ddf3b2 100644
--- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt
@@ -60,7 +60,8 @@ class StartVoiceBroadcastUseCaseTest {
context = FakeContext().instance,
buildMeta = mockk(),
getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase,
- stopVoiceBroadcastUseCase = mockk()
+ stopVoiceBroadcastUseCase = mockk(),
+ pauseVoiceBroadcastUseCase = mockk(),
)
)