Reworked audio route handling code and added auto switch to bluetooth if available and to speaker when video is enabled

This commit is contained in:
Sylvain Berfini 2021-03-25 11:58:08 +01:00
parent 704e7d84fa
commit 7172d7cf60
15 changed files with 186 additions and 66 deletions

View file

@ -36,6 +36,7 @@ import org.linphone.compatibility.Compatibility
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.AudioRouteUtils
import org.linphone.utils.Event
import org.linphone.utils.PermissionHelper
@ -68,7 +69,7 @@ class ControlsViewModel : ViewModel() {
val optionsVisibility = MutableLiveData<Boolean>()
val audioRoutesVisibility = MutableLiveData<Boolean>()
val audioRoutesSelected = MutableLiveData<Boolean>()
val audioRoutesEnabled = MutableLiveData<Boolean>()
@ -170,7 +171,9 @@ class ControlsViewModel : ViewModel() {
state: Call.State,
message: String
) {
if (state == Call.State.StreamsRunning) isVideoUpdateInProgress.value = false
if (state == Call.State.StreamsRunning) {
isVideoUpdateInProgress.value = false
}
if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) {
askPermissionEvent.value = Event(Manifest.permission.CAMERA)
@ -186,9 +189,14 @@ class ControlsViewModel : ViewModel() {
}
override fun onAudioDevicesListUpdated(core: Core) {
if (core.callsNb == 0) return
Log.i("[Call] Audio devices list updated")
val wasBluetoothPreviouslyAvailable = audioRoutesEnabled.value == true
updateAudioRoutesState()
coreContext.routeAudioToBluetoothIfAvailable(core.currentCall ?: core.calls[0])
if (!wasBluetoothPreviouslyAvailable && corePreferences.routeAudioToBluetoothIfAvailable) {
// Only attempt to route audio to bluetooth automatically when bluetooth device is connected
AudioRouteUtils.routeAudioToBluetooth()
}
}
}
@ -202,7 +210,7 @@ class ControlsViewModel : ViewModel() {
numpadVisibility.value = false
optionsVisibility.value = false
audioRoutesVisibility.value = false
audioRoutesSelected.value = false
isRecording.value = currentCall?.isRecording
isVideoUpdateInProgress.value = false
@ -309,8 +317,8 @@ class ControlsViewModel : ViewModel() {
fun toggleRoutesMenu() {
somethingClickedEvent.value = Event(true)
audioRoutesVisibility.value = audioRoutesVisibility.value != true
if (audioRoutesVisibility.value == true) {
audioRoutesSelected.value = audioRoutesSelected.value != true
if (audioRoutesSelected.value == true) {
audioRoutesMenuAnimator.start()
} else {
audioRoutesMenuAnimator.reverse()
@ -365,38 +373,17 @@ class ControlsViewModel : ViewModel() {
fun forceEarpieceAudioRoute() {
somethingClickedEvent.value = Event(true)
for (audioDevice in coreContext.core.audioDevices) {
if (audioDevice.type == AudioDevice.Type.Earpiece) {
Log.i("[Call] Found earpiece audio device [${audioDevice.deviceName}], routing audio to it")
coreContext.core.outputAudioDevice = audioDevice
return
}
}
Log.e("[Call] Couldn't find earpiece audio device")
AudioRouteUtils.routeAudioToEarpiece()
}
fun forceSpeakerAudioRoute() {
somethingClickedEvent.value = Event(true)
for (audioDevice in coreContext.core.audioDevices) {
if (audioDevice.type == AudioDevice.Type.Speaker) {
Log.i("[Call] Found speaker audio device [${audioDevice.deviceName}], routing audio to it")
coreContext.core.outputAudioDevice = audioDevice
return
}
}
Log.e("[Call] Couldn't find speaker audio device")
AudioRouteUtils.routeAudioToSpeaker()
}
fun forceBluetoothAudioRoute() {
somethingClickedEvent.value = Event(true)
for (audioDevice in coreContext.core.audioDevices) {
if ((audioDevice.type == AudioDevice.Type.Bluetooth) && audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
Log.i("[Call] Found bluetooth audio device [${audioDevice.deviceName}], routing audio to it")
coreContext.core.outputAudioDevice = audioDevice
return
}
}
Log.e("[Call] Couldn't find bluetooth audio device")
AudioRouteUtils.routeAudioToBluetooth()
}
private fun updateAudioRelated() {
@ -425,21 +412,16 @@ class ControlsViewModel : ViewModel() {
}
private fun updateAudioRoutesState() {
var bluetoothDeviceAvailable = false
for (audioDevice in coreContext.core.audioDevices) {
if (audioDevice.type == AudioDevice.Type.Bluetooth) {
bluetoothDeviceAvailable = true
break
}
}
val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable()
audioRoutesEnabled.value = bluetoothDeviceAvailable
if (!bluetoothDeviceAvailable) {
audioRoutesVisibility.value = false
audioRoutesSelected.value = false
}
}
private fun updateBluetoothHeadsetState() {
val audioDevice = coreContext.core.outputAudioDevice
if (coreContext.core.callsNb == 0) return
val audioDevice = (coreContext.core.currentCall ?: coreContext.core.calls[0]).outputAudioDevice
isBluetoothHeadsetSelected.value = audioDevice?.type == AudioDevice.Type.Bluetooth
}

View file

@ -21,7 +21,7 @@ package org.linphone.activities.main.about
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.utils.LogsUploadViewModel
import org.linphone.activities.main.viewmodels.LogsUploadViewModel
class AboutViewModel : LogsUploadViewModel() {
val appVersion: String = coreContext.appVersion

View file

@ -17,7 +17,7 @@
* 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.utils
package org.linphone.activities.main.adapters
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter

View file

@ -33,6 +33,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.main.adapters.SelectionListAdapter
import org.linphone.activities.main.chat.viewmodels.ChatMessageViewModel
import org.linphone.activities.main.chat.viewmodels.EventViewModel
import org.linphone.activities.main.chat.viewmodels.OnContentClickedListener
@ -43,7 +44,6 @@ import org.linphone.core.EventLog
import org.linphone.databinding.ChatEventListCellBinding
import org.linphone.databinding.ChatMessageListCellBinding
import org.linphone.utils.Event
import org.linphone.utils.SelectionListAdapter
class ChatMessagesListAdapter(
selectionVM: ListTopBarViewModel,

View file

@ -27,12 +27,12 @@ import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.activities.main.adapters.SelectionListAdapter
import org.linphone.activities.main.chat.viewmodels.ChatRoomViewModel
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.core.ChatRoom
import org.linphone.databinding.ChatRoomListCellBinding
import org.linphone.utils.Event
import org.linphone.utils.SelectionListAdapter
class ChatRoomsListAdapter(
selectionVM: ListTopBarViewModel,

View file

@ -29,6 +29,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.activities.main.adapters.SelectionListAdapter
import org.linphone.activities.main.contact.viewmodels.ContactViewModel
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.contact.Contact
@ -37,7 +38,6 @@ import org.linphone.databinding.GenericListHeaderBinding
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.HeaderAdapter
import org.linphone.utils.SelectionListAdapter
class ContactsListAdapter(
selectionVM: ListTopBarViewModel,

View file

@ -27,12 +27,12 @@ import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.activities.main.dialer.NumpadDigitListener
import org.linphone.activities.main.viewmodels.LogsUploadViewModel
import org.linphone.compatibility.Compatibility
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.LogsUploadViewModel
class DialerViewModel : LogsUploadViewModel() {
val enteredUri = MutableLiveData<String>()

View file

@ -28,12 +28,12 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.linphone.LinphoneApplication
import org.linphone.R
import org.linphone.activities.main.adapters.SelectionListAdapter
import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.DialogUtils
import org.linphone.utils.SelectionListAdapter
/**
* This fragment can be inherited by all fragments that will display a list

View file

@ -29,6 +29,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.activities.main.adapters.SelectionListAdapter
import org.linphone.activities.main.history.viewmodels.CallLogViewModel
import org.linphone.activities.main.history.viewmodels.GroupedCallLogViewModel
import org.linphone.activities.main.viewmodels.ListTopBarViewModel

View file

@ -30,6 +30,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.activities.main.adapters.SelectionListAdapter
import org.linphone.activities.main.recordings.viewmodels.RecordingViewModel
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.databinding.GenericListHeaderBinding

View file

@ -18,13 +18,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.utils
package org.linphone.activities.main.viewmodels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.utils.Event
open class LogsUploadViewModel : ViewModel() {
val uploadInProgress = MutableLiveData<Boolean>()

View file

@ -47,6 +47,7 @@ import org.linphone.core.tools.Log
import org.linphone.mediastream.Version
import org.linphone.notifications.NotificationsManager
import org.linphone.utils.AppUtils
import org.linphone.utils.AudioRouteUtils
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
@ -110,6 +111,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
private var overlayY = 0f
private var callOverlay: View? = null
private var isVibrating = false
private var previousCallState = Call.State.Idle
private val listener: CoreListenerStub = object : CoreListenerStub() {
override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String) {
@ -169,8 +171,8 @@ class CoreContext(val context: Context, coreConfig: Config) {
} else if (state == Call.State.OutgoingInit) {
onOutgoingStarted()
} else if (state == Call.State.OutgoingProgress) {
if (core.callsNb == 1) {
routeAudioToBluetoothIfAvailable(call)
if (core.callsNb == 1 && corePreferences.routeAudioToBluetoothIfAvailable) {
AudioRouteUtils.routeAudioToBluetooth(call)
}
} else if (state == Call.State.Connected) {
if (isVibrating) {
@ -180,11 +182,23 @@ class CoreContext(val context: Context, coreConfig: Config) {
isVibrating = false
}
if (call.dir == Call.Dir.Incoming && core.callsNb == 1) {
routeAudioToBluetoothIfAvailable(call)
onCallStarted()
} else if (state == Call.State.StreamsRunning) {
// Do not automatically route audio to bluetooth after first call
if (core.callsNb == 1) {
// Only try to route bluetooth when the call is in StreamsRunning for the first time
if (previousCallState == Call.State.Connected && corePreferences.routeAudioToBluetoothIfAvailable) {
AudioRouteUtils.routeAudioToBluetooth(call)
}
}
onCallStarted()
if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && call.currentParams.videoEnabled()) {
// Do not turn speaker on when video is enabled if headset or bluetooth is used
if (!AudioRouteUtils.isHeadsetAudioRouteAvailable() && !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed(call)) {
Log.i("[Context] Video enabled and no wired headset not bluetooth in use, routing audio to speaker")
AudioRouteUtils.routeAudioToSpeaker(call)
}
}
} else if (state == Call.State.End || state == Call.State.Error || state == Call.State.Released) {
if (core.callsNb == 0) {
if (isVibrating) {
@ -215,6 +229,8 @@ class CoreContext(val context: Context, coreConfig: Config) {
callErrorMessageResourceId.value = Event(id)
}
}
previousCallState = state
}
}
@ -549,18 +565,6 @@ class CoreContext(val context: Context, coreConfig: Config) {
}
}
fun routeAudioToBluetoothIfAvailable(call: Call) {
for (audioDevice in core.audioDevices) {
if (audioDevice.type == AudioDevice.Type.Bluetooth &&
audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
Log.i("[Context] Found bluetooth audio device [${audioDevice.deviceName}], routing audio to it")
call.outputAudioDevice = audioDevice
return
}
}
Log.w("[Context] Didn't find any bluetooth audio device, keeping default audio route")
}
/* Start call related activities */
private fun onIncomingReceived() {

View file

@ -225,6 +225,19 @@ class CorePreferences constructor(private val context: Context) {
config.setBool("app", "full_screen_call", value)
}
var routeAudioToBluetoothIfAvailable: Boolean
get() = config.getBool("app", "route_audio_to_bluetooth_if_available", true)
set(value) {
config.setBool("app", "route_audio_to_bluetooth_if_available", value)
}
// This won't be done if bluetooth or wired headset is used
var routeAudioToSpeakerWhenVideoIsEnabled: Boolean
get() = config.getBool("app", "route_audio_to_speaker_when_video_enabled", true)
set(value) {
config.setBool("app", "route_audio_to_speaker_when_video_enabled", value)
}
/* Assistant */
var firstStart: Boolean

View file

@ -0,0 +1,118 @@
/*
* Copyright (c) 2010-2021 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.utils
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.AudioDevice
import org.linphone.core.Call
import org.linphone.core.tools.Log
class AudioRouteUtils {
companion object {
fun routeAudioToEarpiece(call: Call? = null) {
if (coreContext.core.callsNb == 0) {
Log.e("[Audio Route Helper] No call found, aborting earpiece audio route change")
return
}
val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0]
for (audioDevice in coreContext.core.audioDevices) {
if (audioDevice.type == AudioDevice.Type.Earpiece) {
Log.i("[Audio Route Helper] Found earpiece audio device [${audioDevice.deviceName}], routing audio to it")
currentCall.outputAudioDevice = audioDevice
return
}
}
Log.e("[Audio Route Helper] Couldn't find earpiece audio device")
}
fun routeAudioToSpeaker(call: Call? = null) {
if (coreContext.core.callsNb == 0) {
Log.e("[Audio Route Helper] No call found, aborting speaker audio route change")
return
}
val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0]
for (audioDevice in coreContext.core.audioDevices) {
if (audioDevice.type == AudioDevice.Type.Speaker) {
Log.i("[Audio Route Helper] Found speaker audio device [${audioDevice.deviceName}], routing audio to it")
currentCall.outputAudioDevice = audioDevice
return
}
}
Log.e("[Audio Route Helper] Couldn't find speaker audio device")
}
fun routeAudioToBluetooth(call: Call? = null) {
if (coreContext.core.callsNb == 0) {
Log.e("[Audio Route Helper] No call found, aborting bluetooth audio route change")
return
}
val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0]
for (audioDevice in coreContext.core.audioDevices) {
if (audioDevice.type == AudioDevice.Type.Bluetooth && audioDevice.hasCapability(
AudioDevice.Capabilities.CapabilityPlay
)
) {
Log.i("[Audio Route Helper] Found bluetooth audio device [${audioDevice.deviceName}], routing audio to it")
currentCall.outputAudioDevice = audioDevice
return
}
}
Log.e("[Audio Route Helper] Couldn't find bluetooth audio device")
}
fun isBluetoothAudioRouteCurrentlyUsed(call: Call? = null): Boolean {
if (coreContext.core.callsNb == 0) {
Log.w("[Audio Route Helper] No call found, so bluetooth audio route isn't used")
return false
}
val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0]
val audioDevice = currentCall.outputAudioDevice
Log.i("[Audio Route Helper] Audio device currently in use is [${audioDevice?.deviceName}]")
return audioDevice?.type == AudioDevice.Type.Bluetooth
}
fun isBluetoothAudioRouteAvailable(): Boolean {
for (audioDevice in coreContext.core.audioDevices) {
if (audioDevice.type == AudioDevice.Type.Bluetooth && audioDevice.hasCapability(
AudioDevice.Capabilities.CapabilityPlay
)
) {
Log.i("[Audio Route Helper] Found bluetooth audio device [${audioDevice.deviceName}]")
return true
}
}
return false
}
fun isHeadsetAudioRouteAvailable(): Boolean {
for (audioDevice in coreContext.core.audioDevices) {
if (audioDevice.type == AudioDevice.Type.Headset || audioDevice.type == AudioDevice.Type.Headphones) {
Log.i("[Audio Route Helper] Found headset/headphones audio device [${audioDevice.deviceName}]")
return true
}
}
return false
}
}
}

View file

@ -144,7 +144,7 @@
android:id="@+id/audio_routes"
android:onClick="@{() -> viewModel.toggleRoutesMenu()}"
android:visibility="@{viewModel.audioRoutesEnabled ? View.VISIBLE : View.INVISIBLE, default=invisible}"
android:selected="@{viewModel.audioRoutesVisibility}"
android:selected="@{viewModel.audioRoutesSelected}"
android:contentDescription="@string/content_description_toggle_audio_menu"
android:layout_width="match_parent"
android:layout_height="@dimen/call_button_size"