diff --git a/app/src/main/java/org/linphone/ui/call/fragment/AudioDevicesMenuDialogFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/AudioDevicesMenuDialogFragment.kt index 9b9721106..73ccc25ff 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/AudioDevicesMenuDialogFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/AudioDevicesMenuDialogFragment.kt @@ -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 } diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingWaitingRoomFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingWaitingRoomFragment.kt index 08218166f..e24f5e518 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingWaitingRoomFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingWaitingRoomFragment.kt @@ -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) { + val modalBottomSheet = AudioDevicesMenuDialogFragment(devicesList) + modalBottomSheet.show(parentFragmentManager, AudioDevicesMenuDialogFragment.TAG) + bottomSheetDialog = modalBottomSheet + } } diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt index e18f37e0f..f0b788ebb 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt @@ -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() + val isSpeakerEnabled = MutableLiveData() + + val isHeadsetEnabled = MutableLiveData() + + val isBluetoothEnabled = MutableLiveData() + val isVideoAvailable = MutableLiveData() val isVideoEnabled = MutableLiveData() @@ -61,12 +72,22 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() { val conferenceInfoFoundEvent = MutableLiveData>() + val showAudioDevicesListEvent: MutableLiveData>> by lazy { + MutableLiveData>>() + } + val conferenceCreatedEvent: MutableLiveData> by lazy { MutableLiveData>() } 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() + 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) } } diff --git a/app/src/main/res/layout-land/call_actions_bottom_sheet.xml b/app/src/main/res/layout-land/call_actions_bottom_sheet.xml index 12e721cff..07696a747 100644 --- a/app/src/main/res/layout-land/call_actions_bottom_sheet.xml +++ b/app/src/main/res/layout-land/call_actions_bottom_sheet.xml @@ -32,7 +32,7 @@