Handle record/play error
This commit is contained in:
parent
6ab9b462a3
commit
bb742eb483
|
@ -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()}")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue