mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Added voice record player in chat bubble
This commit is contained in:
parent
f84f42d8bd
commit
fb5d89e987
8 changed files with 268 additions and 3 deletions
|
|
@ -26,7 +26,18 @@ import android.util.Patterns
|
|||
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 java.util.regex.Pattern
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.Address
|
||||
|
|
@ -35,9 +46,12 @@ import org.linphone.core.ChatMessageListenerStub
|
|||
import org.linphone.core.ChatMessageReaction
|
||||
import org.linphone.core.Content
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.Player
|
||||
import org.linphone.core.PlayerListener
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.ui.main.contacts.model.ContactAvatarModel
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.AudioRouteUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PatternClickableSpan
|
||||
|
|
@ -105,6 +119,31 @@ class ChatMessageModel @WorkerThread constructor(
|
|||
private lateinit var meetingConferenceUri: Address
|
||||
// End of conference info related fields
|
||||
|
||||
// Voice record related fields
|
||||
val isVoiceRecord = MutableLiveData<Boolean>()
|
||||
|
||||
val isPlayingVoiceRecord = MutableLiveData<Boolean>()
|
||||
|
||||
val voiceRecordPlayerPosition = MutableLiveData<Int>()
|
||||
|
||||
val voiceRecordingDuration = MutableLiveData<Int>()
|
||||
|
||||
val formattedVoiceRecordingDuration = MutableLiveData<String>()
|
||||
|
||||
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
||||
|
||||
private lateinit var voiceRecordPath: String
|
||||
|
||||
private lateinit var voiceRecordPlayer: Player
|
||||
|
||||
private val playerListener = PlayerListener {
|
||||
Log.i("$TAG End of file reached")
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
// End of voice record related fields
|
||||
|
||||
val dismissLongPressMenuEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
|
@ -131,6 +170,8 @@ class ChatMessageModel @WorkerThread constructor(
|
|||
}
|
||||
|
||||
init {
|
||||
isPlayingVoiceRecord.postValue(false)
|
||||
|
||||
chatMessage.addListener(chatMessageListener)
|
||||
statusIcon.postValue(LinphoneUtils.getChatIconResId(chatMessage.state))
|
||||
updateReactionsList()
|
||||
|
|
@ -169,6 +210,10 @@ class ChatMessageModel @WorkerThread constructor(
|
|||
displayableContentFound = true
|
||||
}
|
||||
"audio" -> {
|
||||
isVoiceRecord.postValue(true)
|
||||
voiceRecordPath = path
|
||||
initVoiceRecordPlayer()
|
||||
displayableContentFound = true
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
|
|
@ -221,6 +266,17 @@ class ChatMessageModel @WorkerThread constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun togglePlayPauseVoiceRecord() {
|
||||
coreContext.postOnCoreThread {
|
||||
if (isPlayingVoiceRecord.value == false) {
|
||||
startVoiceRecordPlayer()
|
||||
} else {
|
||||
pauseVoiceRecordPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun updateReactionsList() {
|
||||
var reactionsList = ""
|
||||
|
|
@ -375,4 +431,120 @@ class ChatMessageModel @WorkerThread constructor(
|
|||
meetingFound.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun initVoiceRecordPlayer() {
|
||||
if (!::voiceRecordPath.isInitialized) {
|
||||
Log.e("$TAG No voice record path was set!")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i("$TAG Creating player for voice record")
|
||||
|
||||
val playbackSoundCard = AudioRouteUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage()
|
||||
Log.i(
|
||||
"$TAG Using device $playbackSoundCard to make the voice message playback"
|
||||
)
|
||||
|
||||
val localPlayer = coreContext.core.createLocalPlayer(playbackSoundCard, null, null)
|
||||
if (localPlayer != null) {
|
||||
voiceRecordPlayer = localPlayer
|
||||
} else {
|
||||
Log.e("$TAG Couldn't create local player!")
|
||||
return
|
||||
}
|
||||
voiceRecordPlayer.addListener(playerListener)
|
||||
Log.i("$TAG Voice record player created")
|
||||
|
||||
val path = voiceRecordPath
|
||||
Log.i("$TAG Opening voice record file [$path]")
|
||||
voiceRecordPlayer.open(path)
|
||||
|
||||
val duration = voiceRecordPlayer.duration
|
||||
voiceRecordingDuration.postValue(duration)
|
||||
val formattedDuration = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration) // duration is in ms
|
||||
formattedVoiceRecordingDuration.postValue(formattedDuration)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun startVoiceRecordPlayer() {
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("$TAG Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
// TODO: check media volume
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AudioRouteUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
|
||||
Log.i("$TAG Playing voice record")
|
||||
isPlayingVoiceRecord.postValue(true)
|
||||
voiceRecordPlayer.start()
|
||||
|
||||
playerTickerFlow().onEach {
|
||||
withContext(Dispatchers.Main) {
|
||||
voiceRecordPlayerPosition.value = voiceRecordPlayer.currentPosition
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun pauseVoiceRecordPlayer() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("$TAG Pausing voice record")
|
||||
voiceRecordPlayer.pause()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AudioRouteUtils.releaseAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context,
|
||||
request
|
||||
)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPlayingVoiceRecord.postValue(false)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun isPlayerClosed(): Boolean {
|
||||
return !::voiceRecordPlayer.isInitialized || voiceRecordPlayer.state == Player.State.Closed
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun stopVoiceRecordPlayer() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("$TAG Stopping voice record")
|
||||
voiceRecordPlayer.pause()
|
||||
voiceRecordPlayer.seek(0)
|
||||
voiceRecordPlayerPosition.postValue(0)
|
||||
voiceRecordPlayer.close()
|
||||
}
|
||||
|
||||
voiceRecordPlayerPosition.postValue(0)
|
||||
isPlayingVoiceRecord.postValue(false)
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AudioRouteUtils.releaseAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context,
|
||||
request
|
||||
)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPlayingVoiceRecord.postValue(false)
|
||||
}
|
||||
|
||||
private fun playerTickerFlow() = flow {
|
||||
while (isPlayingVoiceRecord.value == true) {
|
||||
emit(Unit)
|
||||
delay(50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -588,6 +588,9 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() {
|
|||
voiceRecordPlayer.close()
|
||||
}
|
||||
|
||||
voiceRecordPlayerPosition.postValue(0)
|
||||
isPlayingVoiceRecord.postValue(false)
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AudioRouteUtils.releaseAudioFocusForVoiceRecordingOrPlayback(
|
||||
|
|
|
|||
|
|
@ -72,6 +72,13 @@ import org.linphone.ui.call.model.ConferenceParticipantDeviceModel
|
|||
* This file contains all the data binding necessary for the app
|
||||
*/
|
||||
|
||||
@BindingAdapter("inflatedLifecycleOwner")
|
||||
fun setInflatedViewStubLifecycleOwner(view: View, enable: Boolean) {
|
||||
val binding = DataBindingUtil.bind<ViewDataBinding>(view)
|
||||
// This is a bit hacky...
|
||||
binding?.lifecycleOwner = view.context as AppCompatActivity
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@BindingAdapter("entries", "layout")
|
||||
fun <T> setEntries(
|
||||
|
|
|
|||
|
|
@ -176,6 +176,14 @@
|
|||
android:layout="@layout/chat_bubble_meeting_invite_content"
|
||||
model="@{model}"/>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/voice_record"
|
||||
android:layout_width="@dimen/chat_bubble_voice_record_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{model.isVoiceRecord ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:layout="@layout/chat_bubble_voice_record_content"
|
||||
model="@{model}" />
|
||||
|
||||
<org.linphone.ui.main.chat.view.ChatBubbleTextView
|
||||
style="@style/default_text_style"
|
||||
android:id="@+id/text_content"
|
||||
|
|
|
|||
|
|
@ -137,6 +137,14 @@
|
|||
android:layout="@layout/chat_bubble_meeting_invite_content"
|
||||
model="@{model}"/>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/voice_record"
|
||||
android:layout_width="@dimen/chat_bubble_voice_record_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{model.isVoiceRecord ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:layout="@layout/chat_bubble_voice_record_content"
|
||||
model="@{model}" />
|
||||
|
||||
<org.linphone.ui.main.chat.view.ChatBubbleTextView
|
||||
style="@style/default_text_style"
|
||||
android:id="@+id/text_content"
|
||||
|
|
|
|||
69
app/src/main/res/layout/chat_bubble_voice_record_content.xml
Normal file
69
app/src/main/res/layout/chat_bubble_voice_record_content.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<import type="android.view.View" />
|
||||
<variable
|
||||
name="model"
|
||||
type="org.linphone.ui.main.chat.model.ChatMessageModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="@dimen/chat_bubble_voice_record_width"
|
||||
android:layout_height="wrap_content"
|
||||
inflatedLifecycleOwner="@{true}">
|
||||
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:id="@+id/voice_record_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:progressDrawable="@drawable/voice_recording_gradient_progress"
|
||||
android:progress="@{model.voiceRecordPlayerPosition}"
|
||||
android:max="@{model.voiceRecordingDuration}"
|
||||
tools:progress="60"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/play_pause_voice_record"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:onClick="@{() -> model.togglePlayPauseVoiceRecord()}"
|
||||
android:padding="8dp"
|
||||
android:src="@{model.isPlayingVoiceRecord ? @drawable/pause_fill : @drawable/play_fill, default=@drawable/play_fill}"
|
||||
android:background="@drawable/circle_white_button_background"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voice_record_duration"
|
||||
app:layout_constraintStart_toStartOf="@id/voice_record_progress"
|
||||
app:layout_constraintTop_toTopOf="@id/voice_record_duration"
|
||||
app:tint="@color/orange_main_500" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/default_text_style"
|
||||
android:id="@+id/voice_record_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:text="@{model.formattedVoiceRecordingDuration, default=`00:00`}"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/gray_main2_600"
|
||||
android:background="@drawable/shape_squircle_white_r50_background"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voice_record_progress"
|
||||
app:layout_constraintEnd_toEndOf="@id/voice_record_progress"
|
||||
app:layout_constraintTop_toTopOf="@id/voice_record_progress"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
|
|
@ -5,9 +5,6 @@
|
|||
|
||||
<data>
|
||||
<import type="android.view.View" />
|
||||
<variable
|
||||
name="openFilePickerClickListener"
|
||||
type="View.OnClickListener" />
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel" />
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
<dimen name="chat_bubble_grid_image_size">88dp</dimen>
|
||||
<dimen name="chat_bubble_big_image_max_size">150dp</dimen>
|
||||
<dimen name="chat_bubble_meeting_invite_width">271dp</dimen>
|
||||
<dimen name="chat_bubble_voice_record_width">271dp</dimen>
|
||||
<dimen name="chat_bubble_max_reply_width">271dp</dimen>
|
||||
<dimen name="chat_bubble_max_width">291dp</dimen>
|
||||
<dimen name="chat_bubble_images_rounded_corner_radius">5dp</dimen>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue