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 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()}")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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="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>
|
||||||
|
|
Loading…
Reference in New Issue