diff --git a/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt index e43d9284c..2c8495d10 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt @@ -21,9 +21,23 @@ package org.linphone.ui.main.recordings.model import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.lifecycle.MutableLiveData +import androidx.media.AudioFocusRequestCompat +import java.text.SimpleDateFormat +import java.util.Locale +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.ticker +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.Factory +import org.linphone.core.Player +import org.linphone.core.PlayerListener import org.linphone.core.tools.Log +import org.linphone.utils.AudioUtils import org.linphone.utils.FileUtils import org.linphone.utils.LinphoneUtils import org.linphone.utils.TimestampUtils @@ -39,7 +53,31 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam val dateTime: String + val formattedDuration: String + + val duration: Int + + val isPlaying = MutableLiveData() + + val position = MutableLiveData() + + private var audioFocusRequest: AudioFocusRequestCompat? = null + + private lateinit var player: Player + private val playerListener = PlayerListener { + Log.i("$TAG End of file reached") + pause() + player.seek(0) + position.postValue(0) + player.close() + } + + private val tickerChannel = ticker(1000, 1000) + private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + init { + isPlaying.postValue(false) + val withoutHeader = fileName.substring(LinphoneUtils.RECORDING_FILE_NAME_HEADER.length) val indexOfSeparator = withoutHeader.indexOf( LinphoneUtils.RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR @@ -69,6 +107,52 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam } else { sipUri } + + val playbackSoundCard = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage() + val audioPlayer = coreContext.core.createLocalPlayer(playbackSoundCard, null, null) + if (audioPlayer != null) { + player = audioPlayer + player.open(filePath) + player.addListener(playerListener) + duration = player.duration + formattedDuration = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration) + } else { + duration = 0 + formattedDuration = "??:??" + } + position.postValue(0) + } + + @WorkerThread + fun destroy() { + scope.cancel() + tickerChannel.cancel() + + if (::player.isInitialized) { + if (player.state != Player.State.Closed) { + player.close() + } + + if (audioFocusRequest != null) { + AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback( + coreContext.context, + audioFocusRequest!! + ) + } + + player.removeListener(playerListener) + } + } + + @UiThread + fun togglePlayPause() { + coreContext.postOnCoreThread { + if (isPlaying.value == true) { + pause() + } else { + play() + } + } } @UiThread @@ -76,4 +160,58 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam Log.i("$TAG Deleting call recording [$filePath]") FileUtils.deleteFile(filePath) } + + @WorkerThread + private fun play() { + if (!::player.isInitialized) return + + Log.i("$TAG Starting player, acquiring audio focus") + audioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback( + coreContext.context + ) + + if (player.state == Player.State.Closed) { + player.open(filePath) + player.seek(0) + } + + player.start() + isPlaying.postValue(true) + + scope.launch { + withContext(Dispatchers.IO) { + for (tick in tickerChannel) { + withContext(Dispatchers.Main) { + if (player.state == Player.State.Playing) { + updatePosition() + } + } + } + } + } + } + + @UiThread + private fun updatePosition() { + val progress = if (player.state == Player.State.Closed) 0 else player.currentPosition + position.value = progress + } + + @WorkerThread + private fun pause() { + if (!::player.isInitialized) return + + Log.i("$TAG Stopping player, releasing audio focus") + if (audioFocusRequest != null) { + AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback( + coreContext.context, + audioFocusRequest!! + ) + } + + coreContext.postOnCoreThread { + player.pause() + } + isPlaying.postValue(false) + } } diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt index c177079c2..e4f76a0fd 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt @@ -55,6 +55,11 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() { } } + override fun onCleared() { + recordings.value.orEmpty().forEach(RecordingModel::destroy) + super.onCleared() + } + @UiThread fun openSearchBar() { searchBarVisible.value = true @@ -82,7 +87,7 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() { @WorkerThread private fun computeList(filter: String) { - // TODO FIXME: use filter + recordings.value.orEmpty().forEach(RecordingModel::destroy) val list = arrayListOf() // TODO FIXME: also load recordings from previous Linphone versions diff --git a/app/src/main/res/layout/recording_list_cell.xml b/app/src/main/res/layout/recording_list_cell.xml index bb81084c2..be1795597 100644 --- a/app/src/main/res/layout/recording_list_cell.xml +++ b/app/src/main/res/layout/recording_list_cell.xml @@ -57,10 +57,11 @@ + + + +