Handle record/play error

This commit is contained in:
Benoit Marty 2021-07-15 14:27:08 +02:00
parent 6ab9b462a3
commit bb742eb483
7 changed files with 120 additions and 41 deletions

View file

@ -25,6 +25,7 @@ import kotlinx.coroutines.completeWith
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
@ -120,13 +121,21 @@ internal class DefaultFileService @Inject constructor(
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
.build()
val response = okHttpClient.newCall(request).execute()
if (!response.isSuccessful) {
throw IOException()
val response = try {
okHttpClient.newCall(request).execute()
} catch (failure: Throwable) {
throw if (failure is IOException) {
Failure.NetworkConnection(failure)
} else {
failure
}
}
val source = response.body?.source() ?: throw IOException()
if (!response.isSuccessful) {
throw Failure.NetworkConnection(IOException())
}
val source = response.body?.source() ?: throw Failure.NetworkConnection(IOException())
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")

View file

@ -19,6 +19,7 @@ package im.vector.app.core.error
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.voice.VoiceFailure
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.MatrixIdFailure
@ -123,11 +124,19 @@ class DefaultErrorFormatter @Inject constructor(
stringProvider.getString(R.string.call_dial_pad_lookup_error)
is MatrixIdFailure.InvalidMatrixId ->
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
is VoiceFailure -> voiceMessageError(throwable)
else -> throwable.localizedMessage
}
?: stringProvider.getString(R.string.unknown_error)
}
private fun voiceMessageError(throwable: VoiceFailure): String {
return when (throwable) {
is VoiceFailure.UnableToPlay -> stringProvider.getString(R.string.error_voice_message_unable_to_play)
is VoiceFailure.UnableToRecord -> stringProvider.getString(R.string.error_voice_message_unable_to_record)
}
}
private fun limitExceededError(error: MatrixError): String {
val delay = error.retryAfterMillis

View file

@ -168,6 +168,7 @@ import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.share.SharedData
import im.vector.app.features.spaces.share.ShareSpaceBottomSheet
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.voice.VoiceFailure
import im.vector.app.features.widgets.WidgetActivity
import im.vector.app.features.widgets.WidgetArgs
import im.vector.app.features.widgets.WidgetKind
@ -386,7 +387,12 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.Failure -> {
if (it.throwable is VoiceFailure.UnableToRecord) {
onCannotRecord()
}
showErrorInSnackbar(it.throwable)
}
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
@ -428,6 +434,11 @@ class RoomDetailFragment @Inject constructor(
}
}
private fun onCannotRecord() {
// Update the UI, cancel the animation
views.voiceMessageRecorderView.initVoiceRecordingViews()
}
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
val intent = VectorCallActivity.newIntent(
context = vectorBaseActivity,

View file

@ -621,7 +621,11 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleStartRecordingVoiceMessage() {
try {
voiceMessageHelper.startRecording()
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(failure))
}
}
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) {
@ -640,8 +644,14 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handlePlayOrPauseVoicePlayback(action: RoomDetailAction.PlayOrPauseVoicePlayback) {
viewModelScope.launch(Dispatchers.IO) {
try {
// Download can fail
val audioFile = session.fileService().downloadFile(action.messageAudioContent)
// Play can fail
voiceMessageHelper.startOrPausePlayback(action.eventId, audioFile)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(failure))
}
}
}

View file

@ -25,6 +25,7 @@ import androidx.core.content.FileProvider
import im.vector.app.BuildConfig
import im.vector.app.core.utils.CountUpTimer
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
import im.vector.app.features.voice.VoiceFailure
import im.vector.lib.multipicker.entity.MultiPickerAudioType
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
import org.matrix.android.sdk.api.extensions.orFalse
@ -44,7 +45,7 @@ class VoiceMessageHelper @Inject constructor(
private val playbackTracker: VoiceMessagePlaybackTracker
) {
private var mediaPlayer: MediaPlayer? = null
private lateinit var mediaRecorder: MediaRecorder
private var mediaRecorder: MediaRecorder? = null
private val outputDirectory = File(context.cacheDir, "downloads")
private var outputFile: File? = null
private var lastRecordingFile: File? = null // In case of user pauses recording, plays another one in timeline
@ -60,13 +61,14 @@ class VoiceMessageHelper @Inject constructor(
}
}
private fun refreshMediaRecorder() {
mediaRecorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.DEFAULT)
setOutputFormat(MediaRecorder.OutputFormat.OGG)
setAudioEncoder(MediaRecorder.AudioEncoder.OPUS)
setAudioEncodingBitRate(24000)
setAudioSamplingRate(48000)
private fun initMediaRecorder() {
MediaRecorder().let {
it.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
it.setOutputFormat(MediaRecorder.OutputFormat.OGG)
it.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS)
it.setAudioEncodingBitRate(24000)
it.setAudioSamplingRate(48000)
mediaRecorder = it
}
}
@ -78,14 +80,19 @@ class VoiceMessageHelper @Inject constructor(
lastRecordingFile = outputFile
amplitudeList.clear()
refreshMediaRecorder()
try {
initMediaRecorder()
val mr = mediaRecorder!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mediaRecorder.setOutputFile(outputFile)
mr.setOutputFile(outputFile)
} else {
mediaRecorder.setOutputFile(FileOutputStream(outputFile).fd)
mr.setOutputFile(FileOutputStream(outputFile).fd)
}
mr.prepare()
mr.start()
} catch (failure: Throwable) {
throw VoiceFailure.UnableToRecord(failure)
}
mediaRecorder.prepare()
mediaRecorder.start()
startRecordingAmplitudes()
}
@ -117,9 +124,13 @@ class VoiceMessageHelper @Inject constructor(
}
private fun releaseMediaRecorder() {
mediaRecorder.stop()
mediaRecorder.reset()
mediaRecorder.release()
mediaRecorder?.let {
it.stop()
it.reset()
it.release()
}
mediaRecorder = null
}
fun pauseRecording() {
@ -143,14 +154,15 @@ class VoiceMessageHelper @Inject constructor(
if (playbackTracker.getPlaybackState(id) is VoiceMessagePlaybackTracker.Listener.State.Playing) {
playbackTracker.pausePlayback(id)
} else {
playbackTracker.startPlayback(id)
startPlayback(id, file)
playbackTracker.startPlayback(id)
}
}
private fun startPlayback(id: String, file: File) {
val currentPlaybackTime = playbackTracker.getPlaybackTime(id)
try {
FileInputStream(file).use { fis ->
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
@ -165,6 +177,9 @@ class VoiceMessageHelper @Inject constructor(
seekTo(currentPlaybackTime)
}
}
} catch (failure: Throwable) {
throw VoiceFailure.UnableToPlay(failure)
}
startPlaybackTicker(id)
}
@ -186,8 +201,9 @@ class VoiceMessageHelper @Inject constructor(
}
private fun onAmplitudeTick() {
val mr = mediaRecorder ?: return
try {
val maxAmplitude = mediaRecorder.maxAmplitude
val maxAmplitude = mr.maxAmplitude
amplitudeList.add(maxAmplitude)
playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList)
} catch (e: IllegalStateException) {

View file

@ -0,0 +1,22 @@
/*
* 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.features.voice
sealed class VoiceFailure(cause: Throwable? = null) : Throwable(cause = cause) {
data class UnableToPlay(val throwable: Throwable) : VoiceFailure(throwable)
data class UnableToRecord(val throwable: Throwable) : VoiceFailure(throwable)
}

View file

@ -3452,4 +3452,6 @@
<string name="voice_message_tap_on_waveform_to_stop_toast">Tap on the waveform to stop and playback</string>
<string name="labs_use_voice_message">Enable voice message</string>
<string name="voice_message_tap_to_stop_toast">Tap on the wavelength to stop and playback</string>
<string name="error_voice_message_unable_to_play">Cannot play this voice message</string>
<string name="error_voice_message_unable_to_record">Cannot record a voice message</string>
</resources>