Updated recordings list & added recording player (audio & video)

This commit is contained in:
Sylvain Berfini 2024-05-30 14:54:03 +02:00
parent 37786a0b83
commit 78f1a1e645
11 changed files with 685 additions and 210 deletions

View file

@ -44,6 +44,10 @@ class RecordingsListAdapter :
HeaderAdapter {
var selectedAdapterPosition = -1
val recordingClickedEvent: MutableLiveData<Event<RecordingModel>> by lazy {
MutableLiveData<Event<RecordingModel>>()
}
val recordingLongClickedEvent: MutableLiveData<Event<RecordingModel>> by lazy {
MutableLiveData<Event<RecordingModel>>()
}
@ -74,6 +78,11 @@ class RecordingsListAdapter :
binding.apply {
lifecycleOwner = parent.findViewTreeLifecycleOwner()
setOnClickListener {
recordingClickedEvent.value = Event(model!!)
resetSelection()
}
setOnLongClickListener {
selectedAdapterPosition = viewHolder.bindingAdapterPosition
root.isSelected = true

View file

@ -0,0 +1,200 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.recordings.fragment
import android.content.Intent
import android.graphics.SurfaceTexture
import android.os.Bundle
import android.view.LayoutInflater
import android.view.TextureView
import android.view.View
import android.view.ViewGroup
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.RecordingPlayerFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.main.fragment.GenericMainFragment
import org.linphone.ui.main.recordings.viewmodel.RecordingMediaPlayerViewModel
import org.linphone.utils.AppUtils
import org.linphone.utils.FileUtils
class RecordingMediaPlayerFragment : GenericMainFragment() {
companion object {
private const val TAG = "[Recording Media Player Fragment]"
}
private lateinit var binding: RecordingPlayerFragmentBinding
private lateinit var viewModel: RecordingMediaPlayerViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = RecordingPlayerFragmentBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(this)[RecordingMediaPlayerViewModel::class.java]
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
goBack()
}
binding.setShareClickListener {
Log.i("$TAG Sharing call recording [${viewModel.recordingModel.filePath}]")
shareFile(viewModel.recordingModel.filePath, viewModel.recordingModel.fileName)
}
binding.setExportClickListener {
Log.i("$TAG Saving call recording [${viewModel.recordingModel.filePath}]")
exportFile(viewModel.recordingModel.filePath)
}
val model = sharedViewModel.playingRecording
if (model != null) {
Log.i("$TAG Loading recording [${model.fileName}] from shared view model")
viewModel.loadRecording(model)
} else {
goBack()
}
}
override fun onResume() {
super.onResume()
val textureView = binding.videoPlayer
if (textureView.isAvailable) {
Log.i("$TAG Surface created, setting display in player")
viewModel.setVideoRenderingSurface(textureView)
} else {
Log.i("$TAG Surface not available yet, setting listener")
textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(
surfaceTexture: SurfaceTexture,
p1: Int,
p2: Int
) {
Log.i("$TAG Surface available, setting display in player")
viewModel.setVideoRenderingSurface(textureView)
}
override fun onSurfaceTextureSizeChanged(
surfaceTexture: SurfaceTexture,
p1: Int,
p2: Int
) {
}
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
return true
}
override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
}
}
}
viewModel.play()
}
override fun onPause() {
if (viewModel.isPlaying.value == true) {
Log.i("$TAG Paused, stopping player")
viewModel.pause()
}
super.onPause()
}
private fun exportFile(filePath: String) {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
Log.i(
"$TAG Export file [$filePath] to Android's MediaStore"
)
val mediaStorePath = FileUtils.addContentToMediaStore(filePath)
if (mediaStorePath.isNotEmpty()) {
Log.i(
"$TAG File [$filePath] has been successfully exported to MediaStore"
)
val message = AppUtils.getString(
R.string.toast_file_successfully_exported_to_media_store
)
(requireActivity() as GenericActivity).showGreenToast(
message,
R.drawable.check
)
} else {
Log.e(
"$TAG Failed to export file [$filePath] to MediaStore!"
)
val message = AppUtils.getString(
R.string.toast_export_file_to_media_store_error
)
(requireActivity() as GenericActivity).showRedToast(
message,
R.drawable.warning_circle
)
}
}
}
}
private fun shareFile(filePath: String, fileName: String) {
lifecycleScope.launch {
val publicUri = FileProvider.getUriForFile(
requireContext(),
getString(R.string.file_provider),
File(filePath)
)
Log.i(
"$TAG Public URI for file is [$publicUri], starting intent chooser"
)
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, publicUri)
putExtra(Intent.EXTRA_SUBJECT, fileName)
type = FileUtils.getMimeTypeFromExtension(
FileUtils.getExtensionFromFileName(fileName)
)
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
}
}

View file

@ -28,6 +28,7 @@ import androidx.annotation.UiThread
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import java.io.File
@ -119,6 +120,18 @@ class RecordingsListFragment : GenericMainFragment() {
}
}
adapter.recordingClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
val action = RecordingsListFragmentDirections.actionRecordingsListFragmentToRecordingMediaPlayerFragment()
if (findNavController().currentDestination?.id == R.id.recordingsListFragment) {
Log.i("$TAG Navigating to recording player for file [${model.filePath}]")
sharedViewModel.playingRecording = model
findNavController().navigate(action)
}
}
}
adapter.recordingLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
val modalBottomSheet = RecordingsMenuDialogFragment(

View file

@ -21,7 +21,6 @@ package org.linphone.ui.main.recordings.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import java.text.SimpleDateFormat
import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext
@ -34,8 +33,6 @@ import org.linphone.utils.TimestampUtils
class RecordingModel @WorkerThread constructor(
val filePath: String,
val fileName: String,
private val onPlay: ((model: RecordingModel) -> Unit),
private val onPause: ((model: RecordingModel) -> Unit),
isLegacy: Boolean = false
) {
companion object {
@ -56,13 +53,7 @@ class RecordingModel @WorkerThread constructor(
val duration: Int
val isPlaying = MutableLiveData<Boolean>()
val position = MutableLiveData<Int>()
init {
isPlaying.postValue(false)
if (isLegacy) {
val username = fileName.split("_")[0]
val sipAddress = coreContext.core.interpretUrl(username, false)
@ -122,22 +113,6 @@ class RecordingModel @WorkerThread constructor(
duration = 0
formattedDuration = "??:??"
}
position.postValue(0)
}
@WorkerThread
fun destroy() {
}
@UiThread
fun togglePlayPause() {
coreContext.postOnCoreThread {
if (isPlaying.value == true) {
onPause.invoke(this)
} else {
onPlay.invoke(this)
}
}
}
@UiThread

View file

@ -0,0 +1,225 @@
/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.recordings.viewmodel
import android.view.TextureView
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.media.AudioFocusRequestCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.ticker
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Player
import org.linphone.core.PlayerListener
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.ui.main.recordings.model.RecordingModel
import org.linphone.utils.AudioUtils
import org.linphone.utils.Event
class RecordingMediaPlayerViewModel @UiThread constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Recording Media Player ViewModel]"
}
lateinit var recordingModel: RecordingModel
lateinit var player: Player
val isVideo = MutableLiveData<Boolean>()
val isPlaying = MutableLiveData<Boolean>()
val position = MutableLiveData<Int>()
private var audioFocusRequest: AudioFocusRequestCompat? = null
private val playerListener = PlayerListener {
Log.i("$TAG End of file reached")
stop()
}
private val tickerChannel = ticker(1000, 1000)
private var updatePositionJob: Job? = null
init {
isVideo.value = false
isPlaying.value = false
position.value = 0
}
override fun onCleared() {
if (::recordingModel.isInitialized) {
stop()
if (::player.isInitialized) {
player.removeListener(playerListener)
}
}
super.onCleared()
}
@UiThread
fun loadRecording(model: RecordingModel) {
recordingModel = model
coreContext.postOnCoreThread { core ->
initPlayer()
}
}
@UiThread
fun setVideoRenderingSurface(textureView: TextureView) {
coreContext.postOnCoreThread {
Log.i("$TAG Setting window ID in player")
player.setWindowId(textureView.surfaceTexture)
}
}
@WorkerThread
private fun initPlayer() {
Log.i("$TAG Creating player")
val playbackSoundCard = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage()
val recordingPlayer = coreContext.core.createLocalPlayer(
playbackSoundCard,
null,
null
)
if (recordingPlayer != null) {
player = recordingPlayer
} else {
Log.e("$TAG Failed to create a Player!")
return
}
player.addListener(playerListener)
val lowMediaVolume = AudioUtils.isMediaVolumeLow(coreContext.context)
if (lowMediaVolume) {
Log.w("$TAG Media volume is low, notifying user as they may not hear voice message")
showRedToastEvent.postValue(
Event(Pair(R.string.toast_low_media_volume, R.drawable.speaker_slash))
)
}
if (player.state == Player.State.Closed) {
player.open(recordingModel.filePath)
player.seek(0)
}
}
@UiThread
fun togglePlayPause() {
coreContext.postOnCoreThread {
if (isPlaying.value == true) {
pausePlayback()
} else {
startPlayback()
}
}
}
@UiThread
fun play() {
coreContext.postOnCoreThread {
startPlayback()
}
}
@UiThread
fun pause() {
coreContext.postOnCoreThread {
pausePlayback()
}
}
@WorkerThread
private fun startPlayback() {
if (!::player.isInitialized) return
Log.i("$TAG Starting player")
if (player.state == Player.State.Closed) {
player.open(recordingModel.filePath)
player.seek(0)
}
Log.i("$TAG Acquiring audio focus")
audioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
coreContext.context
)
player.start()
isPlaying.postValue(true)
// We have to wait for the player to be started to have that information!
val isVideoAvailable = player.isVideoAvailable
Log.i(
"$TAG Recording says video is ${if (isVideoAvailable) "available" else "not available"}"
)
isVideo.postValue(isVideoAvailable)
updatePositionJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
for (tick in tickerChannel) {
coreContext.postOnCoreThread {
if (player.state == Player.State.Playing) {
position.postValue(player.currentPosition)
}
}
}
}
}
}
@WorkerThread
private fun pausePlayback() {
if (!::player.isInitialized) return
Log.i("$TAG Pausing player, releasing audio focus")
if (audioFocusRequest != null) {
AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback(
coreContext.context,
audioFocusRequest!!
)
}
player.pause()
isPlaying.postValue(false)
updatePositionJob?.cancel()
updatePositionJob = null
}
@WorkerThread
private fun stop() {
if (!::player.isInitialized) return
Log.i("$TAG Stopping player")
pause()
position.postValue(0)
player.seek(0)
player.close()
}
}

View file

@ -22,22 +22,11 @@ package org.linphone.ui.main.recordings.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.media.AudioFocusRequestCompat
import java.util.regex.Pattern
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.ticker
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Player
import org.linphone.core.PlayerListener
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.ui.main.recordings.model.RecordingModel
import org.linphone.utils.AudioUtils
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
@ -61,19 +50,6 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
MutableLiveData<Event<Boolean>>()
}
private var audioFocusRequest: AudioFocusRequestCompat? = null
private var currentlyPlayedRecording: RecordingModel? = null
private var player: Player? = null
private val playerListener = PlayerListener {
Log.i("$TAG End of file reached")
stop(currentlyPlayedRecording)
}
private val tickerChannel = ticker(1000, 1000)
private var updatePositionJob: Job? = null
init {
searchBarVisible.value = false
fetchInProgress.value = true
@ -83,17 +59,6 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
}
}
override fun onCleared() {
if (currentlyPlayedRecording != null) {
stop(currentlyPlayedRecording)
player?.removeListener(playerListener)
player = null
}
recordings.value.orEmpty().forEach(RecordingModel::destroy)
super.onCleared()
}
@UiThread
fun openSearchBar() {
searchBarVisible.value = true
@ -119,106 +84,8 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
}
}
@WorkerThread
fun onRecordingStartedPlaying(model: RecordingModel) {
val lowMediaVolume = AudioUtils.isMediaVolumeLow(coreContext.context)
if (lowMediaVolume) {
Log.w("$TAG Media volume is low, notifying user as they may not hear voice message")
showRedToastEvent.postValue(
Event(Pair(R.string.toast_low_media_volume, R.drawable.speaker_slash))
)
}
if (player == null) {
initAudioPlayer()
}
if (currentlyPlayedRecording != null && model != currentlyPlayedRecording) {
Log.i("$TAG Recording model has changed, stopping player before starting it")
stop(currentlyPlayedRecording)
}
currentlyPlayedRecording = model
play(model)
}
@WorkerThread
fun onRecordingPaused(model: RecordingModel) {
pause(model)
}
@WorkerThread
private fun initAudioPlayer() {
Log.i("$TAG Creating player")
val playbackSoundCard = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage()
player = coreContext.core.createLocalPlayer(playbackSoundCard, null, null)
player?.addListener(playerListener)
}
@WorkerThread
private fun play(model: RecordingModel?) {
model ?: return
Log.i("$TAG Starting player")
if (player?.state == Player.State.Closed) {
player?.open(model.filePath)
player?.seek(0)
}
Log.i("$TAG Acquiring audio focus")
audioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
coreContext.context
)
player?.start()
model.isPlaying.postValue(true)
updatePositionJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
for (tick in tickerChannel) {
coreContext.postOnCoreThread {
if (player?.state == Player.State.Playing) {
model.position.postValue(player?.currentPosition)
}
}
}
}
}
}
@WorkerThread
private fun pause(model: RecordingModel?) {
model ?: return
Log.i("$TAG Pausing player, releasing audio focus")
if (audioFocusRequest != null) {
AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback(
coreContext.context,
audioFocusRequest!!
)
}
player?.pause()
model.isPlaying.postValue(false)
updatePositionJob?.cancel()
updatePositionJob = null
}
@WorkerThread
private fun stop(model: RecordingModel?) {
model ?: return
Log.i("$TAG Stopping player")
pause(model)
model.position.postValue(0)
player?.seek(0)
player?.close()
currentlyPlayedRecording = null
}
@WorkerThread
private fun computeList(filter: String) {
recordings.value.orEmpty().forEach(RecordingModel::destroy)
val list = arrayListOf<RecordingModel>()
for (file in FileUtils.getFileStorageDir(isRecording = true).listFiles().orEmpty()) {
@ -226,16 +93,7 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
val name = file.name
Log.d("$TAG Found file $path")
val model = RecordingModel(
path,
name,
{ model ->
onRecordingStartedPlaying(model)
},
{ model ->
onRecordingPaused(model)
}
)
val model = RecordingModel(path, name)
if (filter.isEmpty() || model.sipUri.contains(filter)) {
Log.i("$TAG Added file $path")
@ -249,17 +107,7 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
if (LEGACY_RECORD_PATTERN.matcher(path).matches()) {
Log.d("$TAG Found legacy file $path")
val model = RecordingModel(
path,
name,
{ model ->
onRecordingStartedPlaying(model)
},
{ model ->
onRecordingPaused(model)
},
true
)
val model = RecordingModel(path, name, true)
if (filter.isEmpty() || model.sipUri.contains(filter)) {
Log.i("$TAG Added legacy file $path")

View file

@ -28,6 +28,7 @@ import org.linphone.core.ChatRoom
import org.linphone.core.ConferenceInfo
import org.linphone.core.Friend
import org.linphone.ui.main.chat.model.MessageModel
import org.linphone.ui.main.recordings.model.RecordingModel
import org.linphone.utils.Event
class SharedMainViewModel @UiThread constructor() : ViewModel() {
@ -160,6 +161,10 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
MutableLiveData<Event<ArrayList<String>>>()
}
/* Recordings related */
var playingRecording: RecordingModel? = null
/* Other */
val listOfSelectedSipUrisEvent: MutableLiveData<Event<ArrayList<String>>> by lazy {

View file

@ -8,6 +8,9 @@
<variable
name="model"
type="org.linphone.ui.main.recordings.model.RecordingModel" />
<variable
name="onClickListener"
type="View.OnClickListener" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
@ -21,6 +24,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cardview"
android:onClick="@{onClickListener}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -53,16 +57,16 @@
android:drawableTint="?attr/color_main2_600"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/play_pause"/>
app:layout_constraintEnd_toStartOf="@id/play"/>
<ImageView
android:id="@+id/play_pause"
android:onClick="@{() -> model.togglePlayPause()}"
android:id="@+id/play"
android:onClick="@{onClickListener}"
android:layout_width="@dimen/big_icon_size"
android:layout_height="@dimen/big_icon_size"
android:layout_marginEnd="10dp"
android:padding="12dp"
android:src="@{model.isPlaying ? @drawable/pause_fill : @drawable/play_fill, default=@drawable/play_fill}"
android:src="@drawable/play_fill"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="?attr/color_main1_500" />
@ -81,8 +85,9 @@
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintTop_toTopOf="@id/duration"
app:layout_constraintBottom_toBottomOf="@id/duration"
app:layout_constraintEnd_toStartOf="@id/duration" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
@ -92,33 +97,16 @@
android:layout_marginTop="3dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="10dp"
android:text="@{model.formattedDuration, default=`00:42`}"
android:textSize="12sp"
android:textColor="?attr/color_main2_500"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/time"
app:layout_constraintBottom_toTopOf="@id/progress"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="10dp"
android:max="@{model.duration, default=100}"
android:progress="@{model.position, default=75}"
app:trackCornerRadius="5dp"
app:trackThickness="10dp"
app:trackColor="?attr/color_main1_100"
app:indicatorColor="?attr/color_main1_500"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/duration"
app:layout_constraintBottom_toBottomOf="parent"/>
app:layout_constraintTop_toBottomOf="@id/play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/play"
app:layout_constraintEnd_toEndOf="@id/play" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="shareClickListener"
type="View.OnClickListener" />
<variable
name="exportClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.recordings.viewmodel.RecordingMediaPlayerViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<TextureView
android:id="@+id/video_player"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/top_bar_barrier"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/audio_file_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/music_notes"
android:contentDescription="@null"
android:visibility="@{viewModel.isVideo ? View.GONE : View.VISIBLE}"
app:layout_constraintTop_toBottomOf="@id/top_bar_barrier"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" />
<ImageView
android:id="@+id/play_pause_audio_playback"
android:onClick="@{() -> viewModel.togglePlayPause()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="@dimen/screen_bottom_margin"
android:padding="8dp"
android:contentDescription="@string/content_description_play_pause_audio_playback"
android:src="@{viewModel.isPlaying ? @drawable/pause_fill : @drawable/play_fill, default=@drawable/play_fill}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:tint="@color/white"/>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:max="@{viewModel.recordingModel.duration, default=100}"
android:progress="@{viewModel.position, default=75}"
app:trackCornerRadius="5dp"
app:trackThickness="10dp"
app:trackColor="?attr/color_main1_100"
app:indicatorColor="?attr/color_main1_500"
app:layout_constraintTop_toTopOf="@id/play_pause_audio_playback"
app:layout_constraintBottom_toBottomOf="@id/play_pause_audio_playback"
app:layout_constraintStart_toEndOf="@id/play_pause_audio_playback"
app:layout_constraintEnd_toStartOf="@id/duration"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_700"
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@{viewModel.recordingModel.formattedDuration, default=`00:42`}"
android:textSize="13sp"
android:textColor="@color/white"
app:layout_constraintTop_toTopOf="@id/play_pause_audio_playback"
app:layout_constraintBottom_toBottomOf="@id/play_pause_audio_playback"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/top_bar_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="back, date_time"
app:barrierDirection="bottom" />
<View
android:id="@+id/top_bar_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/top_bar_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/back"
android:onClick="@{backClickListener}"
android:layout_width="wrap_content"
android:layout_height="@dimen/top_bar_height"
android:adjustViewBounds="true"
android:padding="15dp"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main1_500"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_700"
android:id="@+id/file_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.recordingModel.displayName, default=`nomdufichier.jpg`}"
android:textSize="13sp"
android:textColor="?attr/color_main2_600"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintEnd_toStartOf="@id/share"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/date_time"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/date_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.recordingModel.dateTime, default=`envoyé le 02/05/2023 à 11h05`}"
android:textSize="12sp"
android:textColor="?attr/color_main2_500"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/file_name"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintEnd_toStartOf="@id/share"
app:layout_constraintStart_toEndOf="@id/back"/>
<ImageView
android:id="@+id/share"
android:onClick="@{shareClickListener}"
android:layout_width="wrap_content"
android:layout_height="@dimen/top_bar_height"
android:adjustViewBounds="true"
android:padding="15dp"
android:src="@drawable/share_network"
android:contentDescription="@string/content_description_share_file"
app:tint="?attr/color_main2_500"
app:layout_constraintEnd_toStartOf="@id/save"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/save"
android:onClick="@{exportClickListener}"
android:layout_width="wrap_content"
android:layout_height="@dimen/top_bar_height"
android:adjustViewBounds="true"
android:padding="15dp"
android:src="@drawable/download_simple"
android:contentDescription="@string/content_description_save_file"
app:tint="?attr/color_main2_500"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/toasts_area"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="@dimen/toast_top_margin"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
app:layout_constraintWidth_max="@dimen/toast_max_width"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -176,7 +176,18 @@
android:id="@+id/recordingsListFragment"
android:name="org.linphone.ui.main.recordings.fragment.RecordingsListFragment"
android:label="RecordingsListFragment"
tools:layout="@layout/recordings_list_fragment" />
tools:layout="@layout/recordings_list_fragment">
<action
android:id="@+id/action_recordingsListFragment_to_recordingMediaPlayerFragment"
app:destination="@id/recordingMediaPlayerFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true" />
</fragment>
<action
android:id="@+id/action_global_recordingsListFragment"
@ -410,4 +421,10 @@
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
<fragment
android:id="@+id/recordingMediaPlayerFragment"
android:name="org.linphone.ui.main.recordings.fragment.RecordingMediaPlayerFragment"
android:label="RecordingMediaPlayerFragment"
tools:layout="@layout/recording_player_fragment"/>
</navigation>

View file

@ -8,7 +8,7 @@ ktlint = "11.3.1"
annotations = "1.8.0"
activity = "1.9.0"
appcompat = "1.7.0-rc01"
appcompat = "1.7.0"
constraintLayout = "2.1.4"
coreKtx = "1.13.1"
splashscreen = "1.2.0-alpha01"
@ -16,7 +16,7 @@ telecom = "1.0.0-alpha03"
media = "1.7.0"
recyclerview = "1.3.2"
slidingpanelayout = "1.2.0"
window = "1.2.0"
window = "1.3.0"
gridlayout = "1.0.0"
securityCryptoKtx = "1.1.0-alpha06"
navigation = "2.7.7"