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 kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request 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.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
@ -120,13 +121,21 @@ internal class DefaultFileService @Inject constructor(
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url) .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
.build() .build()
val response = okHttpClient.newCall(request).execute() val response = try {
okHttpClient.newCall(request).execute()
if (!response.isSuccessful) { } catch (failure: Throwable) {
throw IOException() 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()}") 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.R
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.call.dialpad.DialPadLookup 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.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.MatrixIdFailure 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) stringProvider.getString(R.string.call_dial_pad_lookup_error)
is MatrixIdFailure.InvalidMatrixId -> is MatrixIdFailure.InvalidMatrixId ->
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id) stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
is VoiceFailure -> voiceMessageError(throwable)
else -> throwable.localizedMessage else -> throwable.localizedMessage
} }
?: stringProvider.getString(R.string.unknown_error) ?: 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 { private fun limitExceededError(error: MatrixError): String {
val delay = error.retryAfterMillis 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.share.SharedData
import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.spaces.share.ShareSpaceBottomSheet
import im.vector.app.features.themes.ThemeUtils 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.WidgetActivity
import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetArgs
import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.WidgetKind
@ -386,7 +387,12 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.observeViewEvents { roomDetailViewModel.observeViewEvents {
when (it) { 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.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(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) { private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
val intent = VectorCallActivity.newIntent( val intent = VectorCallActivity.newIntent(
context = vectorBaseActivity, context = vectorBaseActivity,

View file

@ -621,7 +621,11 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
private fun handleStartRecordingVoiceMessage() { private fun handleStartRecordingVoiceMessage() {
voiceMessageHelper.startRecording() try {
voiceMessageHelper.startRecording()
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(failure))
}
} }
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) { private fun handleEndRecordingVoiceMessage(isCancelled: Boolean) {
@ -640,8 +644,14 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handlePlayOrPauseVoicePlayback(action: RoomDetailAction.PlayOrPauseVoicePlayback) { private fun handlePlayOrPauseVoicePlayback(action: RoomDetailAction.PlayOrPauseVoicePlayback) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val audioFile = session.fileService().downloadFile(action.messageAudioContent) try {
voiceMessageHelper.startOrPausePlayback(action.eventId, audioFile) // 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.BuildConfig
import im.vector.app.core.utils.CountUpTimer import im.vector.app.core.utils.CountUpTimer
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker 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.entity.MultiPickerAudioType
import im.vector.lib.multipicker.utils.toMultiPickerAudioType import im.vector.lib.multipicker.utils.toMultiPickerAudioType
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
@ -44,7 +45,7 @@ class VoiceMessageHelper @Inject constructor(
private val playbackTracker: VoiceMessagePlaybackTracker private val playbackTracker: VoiceMessagePlaybackTracker
) { ) {
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private lateinit var mediaRecorder: MediaRecorder private var mediaRecorder: MediaRecorder? = null
private val outputDirectory = File(context.cacheDir, "downloads") private val outputDirectory = File(context.cacheDir, "downloads")
private var outputFile: File? = null private var outputFile: File? = null
private var lastRecordingFile: File? = null // In case of user pauses recording, plays another one in timeline 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() { private fun initMediaRecorder() {
mediaRecorder = MediaRecorder().apply { MediaRecorder().let {
setAudioSource(MediaRecorder.AudioSource.DEFAULT) it.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
setOutputFormat(MediaRecorder.OutputFormat.OGG) it.setOutputFormat(MediaRecorder.OutputFormat.OGG)
setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) it.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS)
setAudioEncodingBitRate(24000) it.setAudioEncodingBitRate(24000)
setAudioSamplingRate(48000) it.setAudioSamplingRate(48000)
mediaRecorder = it
} }
} }
@ -78,14 +80,19 @@ class VoiceMessageHelper @Inject constructor(
lastRecordingFile = outputFile lastRecordingFile = outputFile
amplitudeList.clear() amplitudeList.clear()
refreshMediaRecorder() try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { initMediaRecorder()
mediaRecorder.setOutputFile(outputFile) val mr = mediaRecorder!!
} else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mediaRecorder.setOutputFile(FileOutputStream(outputFile).fd) mr.setOutputFile(outputFile)
} else {
mr.setOutputFile(FileOutputStream(outputFile).fd)
}
mr.prepare()
mr.start()
} catch (failure: Throwable) {
throw VoiceFailure.UnableToRecord(failure)
} }
mediaRecorder.prepare()
mediaRecorder.start()
startRecordingAmplitudes() startRecordingAmplitudes()
} }
@ -117,9 +124,13 @@ class VoiceMessageHelper @Inject constructor(
} }
private fun releaseMediaRecorder() { private fun releaseMediaRecorder() {
mediaRecorder.stop() mediaRecorder?.let {
mediaRecorder.reset() it.stop()
mediaRecorder.release() it.reset()
it.release()
}
mediaRecorder = null
} }
fun pauseRecording() { fun pauseRecording() {
@ -143,27 +154,31 @@ class VoiceMessageHelper @Inject constructor(
if (playbackTracker.getPlaybackState(id) is VoiceMessagePlaybackTracker.Listener.State.Playing) { if (playbackTracker.getPlaybackState(id) is VoiceMessagePlaybackTracker.Listener.State.Playing) {
playbackTracker.pausePlayback(id) playbackTracker.pausePlayback(id)
} else { } else {
playbackTracker.startPlayback(id)
startPlayback(id, file) startPlayback(id, file)
playbackTracker.startPlayback(id)
} }
} }
private fun startPlayback(id: String, file: File) { private fun startPlayback(id: String, file: File) {
val currentPlaybackTime = playbackTracker.getPlaybackTime(id) val currentPlaybackTime = playbackTracker.getPlaybackTime(id)
FileInputStream(file).use { fis -> try {
mediaPlayer = MediaPlayer().apply { FileInputStream(file).use { fis ->
setAudioAttributes( mediaPlayer = MediaPlayer().apply {
AudioAttributes.Builder() setAudioAttributes(
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build() .setUsage(AudioAttributes.USAGE_MEDIA)
) .build()
setDataSource(fis.fd) )
prepare() setDataSource(fis.fd)
start() prepare()
seekTo(currentPlaybackTime) start()
seekTo(currentPlaybackTime)
}
} }
} catch (failure: Throwable) {
throw VoiceFailure.UnableToPlay(failure)
} }
startPlaybackTicker(id) startPlaybackTicker(id)
} }
@ -186,8 +201,9 @@ class VoiceMessageHelper @Inject constructor(
} }
private fun onAmplitudeTick() { private fun onAmplitudeTick() {
val mr = mediaRecorder ?: return
try { try {
val maxAmplitude = mediaRecorder.maxAmplitude val maxAmplitude = mr.maxAmplitude
amplitudeList.add(maxAmplitude) amplitudeList.add(maxAmplitude)
playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList) playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList)
} catch (e: IllegalStateException) { } 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="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="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="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> </resources>