Added audio route selection to conference waiting room fragment

This commit is contained in:
Sylvain Berfini 2024-01-29 11:53:37 +01:00
parent 4697af6c27
commit ce1d3d4807
8 changed files with 206 additions and 9 deletions

View file

@ -29,6 +29,7 @@ import androidx.annotation.UiThread
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.R
import org.linphone.databinding.CallAudioDevicesMenuBinding
import org.linphone.ui.call.model.AudioDeviceModel
@ -56,6 +57,9 @@ class AudioDevicesMenuDialogFragment(
// Makes sure all menu entries are visible,
// required for landscape mode (otherwise only first item is visible)
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
// Force this navigation bar color
dialog.window?.navigationBarColor = requireContext().getColor(R.color.gray_600)
return dialog
}

View file

@ -32,10 +32,13 @@ import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.MeetingWaitingRoomFragmentBinding
import org.linphone.ui.call.fragment.AudioDevicesMenuDialogFragment
import org.linphone.ui.call.model.AudioDeviceModel
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.main.meetings.viewmodel.MeetingWaitingRoomViewModel
@ -63,6 +66,8 @@ class MeetingWaitingRoomFragment : GenericFragment() {
}
}
private var bottomSheetDialog: BottomSheetDialogFragment? = null
private var navBarDefaultColor: Int = -1
override fun onCreateView(
@ -99,6 +104,12 @@ class MeetingWaitingRoomFragment : GenericFragment() {
goBack()
}
viewModel.showAudioDevicesListEvent.observe(viewLifecycleOwner) {
it.consume { devices ->
showAudioRoutesMenu(devices)
}
}
viewModel.conferenceInfoFoundEvent.observe(viewLifecycleOwner) {
it.consume { found ->
if (found) {
@ -144,6 +155,9 @@ class MeetingWaitingRoomFragment : GenericFragment() {
}
override fun onPause() {
bottomSheetDialog?.dismiss()
bottomSheetDialog = null
coreContext.postOnCoreThread { core ->
core.nativePreviewWindowId = null
core.isVideoPreviewEnabled = false
@ -177,4 +191,10 @@ class MeetingWaitingRoomFragment : GenericFragment() {
}
}
}
private fun showAudioRoutesMenu(devicesList: List<AudioDeviceModel>) {
val modalBottomSheet = AudioDevicesMenuDialogFragment(devicesList)
modalBottomSheet.show(parentFragmentManager, AudioDevicesMenuDialogFragment.TAG)
bottomSheetDialog = modalBottomSheet
}
}

View file

@ -27,6 +27,9 @@ import androidx.core.app.ActivityCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.AudioDevice
import org.linphone.core.Conference
import org.linphone.core.ConferenceInfo
import org.linphone.core.Core
@ -34,7 +37,9 @@ import org.linphone.core.CoreListenerStub
import org.linphone.core.Factory
import org.linphone.core.MediaDirection
import org.linphone.core.tools.Log
import org.linphone.ui.call.model.AudioDeviceModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.TimestampUtils
@ -51,6 +56,12 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() {
val isMicrophoneMuted = MutableLiveData<Boolean>()
val isSpeakerEnabled = MutableLiveData<Boolean>()
val isHeadsetEnabled = MutableLiveData<Boolean>()
val isBluetoothEnabled = MutableLiveData<Boolean>()
val isVideoAvailable = MutableLiveData<Boolean>()
val isVideoEnabled = MutableLiveData<Boolean>()
@ -61,12 +72,22 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() {
val conferenceInfoFoundEvent = MutableLiveData<Event<Boolean>>()
val showAudioDevicesListEvent: MutableLiveData<Event<ArrayList<AudioDeviceModel>>> by lazy {
MutableLiveData<Event<ArrayList<AudioDeviceModel>>>()
}
val conferenceCreatedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
private lateinit var conferenceInfo: ConferenceInfo
private lateinit var selectedOutputAudioDevice: AudioDevice
private var earpieceAudioDevice: AudioDevice? = null
private var speakerAudioDevice: AudioDevice? = null
private var bluetoothAudioDevice: AudioDevice? = null
private val coreListener = object : CoreListenerStub() {
override fun onConferenceStateChanged(
core: Core,
@ -84,6 +105,15 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() {
coreContext.postOnCoreThread { core ->
core.addListener(coreListener)
val audioDevices = core.audioDevices
earpieceAudioDevice = audioDevices.find { it.type == AudioDevice.Type.Earpiece }
speakerAudioDevice = audioDevices.find { it.type == AudioDevice.Type.Speaker }
bluetoothAudioDevice = audioDevices.find {
it.hasCapability(
AudioDevice.Capabilities.CapabilityPlay
) && (it.type == AudioDevice.Type.Bluetooth || it.type == AudioDevice.Type.HearingAid)
}
hideVideo.postValue(!core.isVideoEnabled)
}
}
@ -159,6 +189,9 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() {
params.isVideoEnabled = true
params.videoDirection = if (isVideoEnabled.value == true) MediaDirection.SendRecv else MediaDirection.RecvOnly
params.isMicEnabled = isMicrophoneMuted.value == false
if (::selectedOutputAudioDevice.isInitialized) {
params.outputAudioDevice = selectedOutputAudioDevice
}
params.account = core.defaultAccount
coreContext.startCall(conferenceUri, params)
}
@ -176,6 +209,20 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() {
@UiThread
fun toggleVideo() {
isVideoEnabled.value = isVideoEnabled.value == false
if (isVideoEnabled.value == true) {
coreContext.postOnCoreThread {
if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled) {
// If setting says to use speaker when video is enabled, use speaker instead of earpiece
if (!::selectedOutputAudioDevice.isInitialized || selectedOutputAudioDevice.type == AudioDevice.Type.Earpiece) {
val speaker = speakerAudioDevice
if (speaker != null) {
selectedOutputAudioDevice = speaker
updateOutputAudioDevice(speaker)
}
}
}
}
}
}
@UiThread
@ -184,8 +231,85 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() {
}
@UiThread
fun toggleSpeaker() {
// TODO
fun changeAudioOutputDevice() {
val routeAudioToSpeaker = isSpeakerEnabled.value != true
coreContext.postOnCoreThread { core ->
val audioDevices = core.audioDevices
val list = arrayListOf<AudioDeviceModel>()
var earpieceAudioDevice: AudioDevice? = null
var speakerAudioDevice: AudioDevice? = null
for (device in audioDevices) {
// Only list output audio devices
if (!device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) continue
val name = when (device.type) {
AudioDevice.Type.Earpiece -> {
earpieceAudioDevice = device
AppUtils.getString(R.string.call_audio_device_type_earpiece)
}
AudioDevice.Type.Speaker -> {
speakerAudioDevice = device
AppUtils.getString(R.string.call_audio_device_type_speaker)
}
AudioDevice.Type.Headset -> {
AppUtils.getString(R.string.call_audio_device_type_headset)
}
AudioDevice.Type.Headphones -> {
AppUtils.getString(R.string.call_audio_device_type_headphones)
}
AudioDevice.Type.Bluetooth -> {
AppUtils.getFormattedString(
R.string.call_audio_device_type_bluetooth,
device.deviceName
)
}
AudioDevice.Type.HearingAid -> {
AppUtils.getFormattedString(
R.string.call_audio_device_type_hearing_aid,
device.deviceName
)
}
else -> device.deviceName
}
val currentDevice = if (::selectedOutputAudioDevice.isInitialized) {
selectedOutputAudioDevice
} else {
core.outputAudioDevice ?: core.defaultOutputAudioDevice
}
val isCurrentlyInUse = device.type == currentDevice?.type && device.deviceName == currentDevice?.deviceName
val model = AudioDeviceModel(device, name, device.type, isCurrentlyInUse) {
// onSelected
coreContext.postOnCoreThread {
Log.i("$TAG Selected audio device with ID [${device.id}]")
selectedOutputAudioDevice = device
updateOutputAudioDevice(device)
}
}
list.add(model)
Log.i("$TAG Found audio device [$device]")
}
if (list.size > 2) {
Log.i("$TAG Found more than two devices, showing list to let user choose")
showAudioDevicesListEvent.postValue(Event(list))
} else {
Log.i(
"$TAG Found less than two devices, simply switching between earpiece & speaker"
)
val newAudioDevice = if (routeAudioToSpeaker) {
speakerAudioDevice
} else {
earpieceAudioDevice ?: speakerAudioDevice
}
if (newAudioDevice != null) {
selectedOutputAudioDevice = newAudioDevice
updateOutputAudioDevice(newAudioDevice)
}
}
}
}
@WorkerThread
@ -222,12 +346,61 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() {
coreContext.context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
isVideoEnabled.postValue(core.isVideoEnabled && cameraPermissionGranted)
val videoEnabled = core.isVideoEnabled && cameraPermissionGranted
isVideoEnabled.postValue(videoEnabled)
isSwitchCameraAvailable.postValue(coreContext.showSwitchCameraButton())
isMicrophoneMuted.postValue(!core.isMicEnabled)
// TODO: audio routes
initOutputAudioDevice(videoEnabled)
}
@WorkerThread
private fun initOutputAudioDevice(videoEnabled: Boolean) {
val core = coreContext.core
val audioDevice = if (corePreferences.routeAudioToBluetoothIfAvailable) {
// Prefer bluetooth audio device if setting says so
if (bluetoothAudioDevice != null) {
bluetoothAudioDevice
} else {
if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && videoEnabled) {
// If setting says to use speaker when video is enabled, use speaker instead of earpiece
val defaultDevice = core.outputAudioDevice ?: core.defaultOutputAudioDevice
if (defaultDevice?.type == AudioDevice.Type.Earpiece) {
speakerAudioDevice
} else {
defaultDevice
}
} else {
core.outputAudioDevice ?: core.defaultOutputAudioDevice
}
}
} else if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && videoEnabled) {
// If setting says to use speaker when video is enabled, use speaker instead of earpiece
val defaultDevice = core.outputAudioDevice ?: core.defaultOutputAudioDevice
if (defaultDevice?.type == AudioDevice.Type.Earpiece) {
speakerAudioDevice
} else {
defaultDevice
}
} else {
core.outputAudioDevice ?: core.defaultOutputAudioDevice
}
if (audioDevice != null) {
selectedOutputAudioDevice = audioDevice
updateOutputAudioDevice(audioDevice)
}
}
@WorkerThread
private fun updateOutputAudioDevice(audioDevice: AudioDevice) {
Log.i("$TAG Selected output audio device is [${audioDevice.id}]")
isSpeakerEnabled.postValue(audioDevice.type == AudioDevice.Type.Speaker)
isHeadsetEnabled.postValue(
audioDevice.type == AudioDevice.Type.Headphones || audioDevice.type == AudioDevice.Type.Headset
)
isBluetoothEnabled.postValue(audioDevice.type == AudioDevice.Type.Bluetooth)
}
}

View file

@ -32,7 +32,7 @@
<include
android:id="@+id/main_actions"
layout="@layout/call_common_actions"
layout="@layout/call_actions_generic"
android:layout_width="0dp"
android:layout_height="@dimen/call_main_actions_menu_height"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -32,7 +32,7 @@
<include
android:id="@+id/main_actions"
layout="@layout/call_common_actions"
layout="@layout/call_actions_generic"
android:layout_width="0dp"
android:layout_height="@dimen/call_main_actions_menu_height"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -29,7 +29,7 @@
<include
android:id="@+id/main_actions"
layout="@layout/call_common_actions"
layout="@layout/call_actions_generic"
android:layout_width="0dp"
android:layout_height="@dimen/call_main_actions_menu_height"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -157,12 +157,12 @@
<ImageView
android:id="@+id/change_audio_output"
android:onClick="@{() -> viewModel.toggleSpeaker()}"
android:onClick="@{() -> viewModel.changeAudioOutputDevice()}"
android:layout_width="@dimen/call_button_size"
android:layout_height="@dimen/call_button_size"
android:layout_marginEnd="16dp"
android:padding="@dimen/call_button_icon_padding"
android:src="@drawable/speaker_slash"
android:src="@{viewModel.isHeadsetEnabled ? @drawable/headset : viewModel.isBluetoothEnabled ? @drawable/bluetooth : viewModel.isSpeakerEnabled ? @drawable/speaker_high : @drawable/speaker_slash, default=@drawable/speaker_slash}"
android:background="@drawable/in_call_button_background_red"
app:tint="@color/in_call_button_tint_color"
app:layout_constraintTop_toTopOf="@id/toggle_mute_mic"