Reworked audio route changes to make them go through Telecom Manager API if enabled to make smartwatches act as bluetooth headset properly

This commit is contained in:
Sylvain Berfini 2021-11-03 13:48:54 +01:00
parent 6d6ea9b4c4
commit f66c90d356
6 changed files with 99 additions and 42 deletions

View file

@ -34,6 +34,7 @@ import android.view.WindowManager
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import org.linphone.R import org.linphone.R
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.telecom.NativeCallWrapper
@TargetApi(26) @TargetApi(26)
class Api26Compatibility { class Api26Compatibility {
@ -133,5 +134,9 @@ class Api26Compatibility {
.build() .build()
vibrator.vibrate(effect, audioAttrs) vibrator.vibrate(effect, audioAttrs)
} }
fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int) {
connection.setAudioRoute(route)
}
} }
} }

View file

@ -33,6 +33,7 @@ import androidx.core.app.NotificationManagerCompat
import org.linphone.core.ChatRoom import org.linphone.core.ChatRoom
import org.linphone.core.Content import org.linphone.core.Content
import org.linphone.mediastream.Version import org.linphone.mediastream.Version
import org.linphone.telecom.NativeCallWrapper
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class Compatibility { class Compatibility {
@ -169,6 +170,12 @@ class Compatibility {
} }
} }
fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int) {
if (Version.sdkAboveOrEqual(Version.API26_O_80)) {
Api26Compatibility.changeAudioRouteForTelecomManager(connection, route)
}
}
/* Contacts */ /* Contacts */
fun createShortcutsToContacts(context: Context) { fun createShortcutsToContacts(context: Context) {

View file

@ -72,10 +72,10 @@ class NativeCallWrapper(var callId: String) : Connection() {
if (call != null) { if (call != null) {
call.microphoneMuted = state.isMuted call.microphoneMuted = state.isMuted
when (state.route) { when (state.route) {
CallAudioState.ROUTE_EARPIECE -> AudioRouteUtils.routeAudioToEarpiece(call) CallAudioState.ROUTE_EARPIECE -> AudioRouteUtils.routeAudioToEarpiece(call, true)
CallAudioState.ROUTE_SPEAKER -> AudioRouteUtils.routeAudioToSpeaker(call) CallAudioState.ROUTE_SPEAKER -> AudioRouteUtils.routeAudioToSpeaker(call, true)
CallAudioState.ROUTE_BLUETOOTH -> AudioRouteUtils.routeAudioToBluetooth(call) CallAudioState.ROUTE_BLUETOOTH -> AudioRouteUtils.routeAudioToBluetooth(call, true)
CallAudioState.ROUTE_WIRED_HEADSET -> AudioRouteUtils.routeAudioToHeadset(call) CallAudioState.ROUTE_WIRED_HEADSET -> AudioRouteUtils.routeAudioToHeadset(call, true)
} }
} else { } else {
selfDestroy() selfDestroy()

View file

@ -30,8 +30,6 @@ import org.linphone.core.CoreListenerStub
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
class TelecomConnectionService : ConnectionService() { class TelecomConnectionService : ConnectionService() {
private val connections = arrayListOf<NativeCallWrapper>()
private val listener: CoreListenerStub = object : CoreListenerStub() { private val listener: CoreListenerStub = object : CoreListenerStub() {
override fun onCallStateChanged( override fun onCallStateChanged(
core: Core, core: Core,
@ -42,7 +40,7 @@ class TelecomConnectionService : ConnectionService() {
Log.i("[Telecom Connection Service] call [${call.callLog.callId}] state changed: $state") Log.i("[Telecom Connection Service] call [${call.callLog.callId}] state changed: $state")
when (call.state) { when (call.state) {
Call.State.OutgoingProgress -> { Call.State.OutgoingProgress -> {
for (connection in connections) { for (connection in TelecomHelper.get().connections) {
if (connection.callId.isEmpty()) { if (connection.callId.isEmpty()) {
connection.callId = core.currentCall?.callLog?.callId ?: "" connection.callId = core.currentCall?.callLog?.callId ?: ""
} }
@ -105,7 +103,7 @@ class TelecomConnectionService : ConnectionService() {
connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED) connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
Log.i("[Telecom Connection Service] Address is $providedHandle") Log.i("[Telecom Connection Service] Address is $providedHandle")
connections.add(connection) TelecomHelper.get().connections.add(connection)
connection connection
} else { } else {
Log.e("[Telecom Connection Service] Error: $accountHandle $componentName") Log.e("[Telecom Connection Service] Error: $accountHandle $componentName")
@ -150,7 +148,7 @@ class TelecomConnectionService : ConnectionService() {
connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED) connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
Log.i("[Telecom Connection Service] Address is $providedHandle") Log.i("[Telecom Connection Service] Address is $providedHandle")
connections.add(connection) TelecomHelper.get().connections.add(connection)
connection connection
} else { } else {
Log.e("[Telecom Connection Service] Error: $accountHandle $componentName") Log.e("[Telecom Connection Service] Error: $accountHandle $componentName")
@ -163,26 +161,20 @@ class TelecomConnectionService : ConnectionService() {
} }
} }
private fun getConnectionForCallId(callId: String): NativeCallWrapper? {
return connections.find { connection ->
connection.callId == callId
}
}
private fun onCallError(call: Call) { private fun onCallError(call: Call) {
val connection = getConnectionForCallId(call.callLog.callId) val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId)
connection ?: return connection ?: return
connections.remove(connection) TelecomHelper.get().connections.remove(connection)
connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR)) connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR))
connection.destroy() connection.destroy()
} }
private fun onCallEnded(call: Call) { private fun onCallEnded(call: Call) {
val connection = getConnectionForCallId(call.callLog.callId) val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId)
connection ?: return connection ?: return
connections.remove(connection) TelecomHelper.get().connections.remove(connection)
val reason = call.reason val reason = call.reason
Log.i("[Telecom Connection Service] Call ended with reason: $reason") Log.i("[Telecom Connection Service] Call ended with reason: $reason")
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
@ -190,7 +182,7 @@ class TelecomConnectionService : ConnectionService() {
} }
private fun onCallConnected(call: Call) { private fun onCallConnected(call: Call) {
val connection = getConnectionForCallId(call.callLog.callId) val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId)
connection ?: return connection ?: return
if (connection.state != Connection.STATE_HOLDING) { if (connection.state != Connection.STATE_HOLDING) {

View file

@ -49,6 +49,8 @@ class TelecomHelper private constructor(context: Context) {
private var account: PhoneAccount = initPhoneAccount(context) private var account: PhoneAccount = initPhoneAccount(context)
val connections = arrayListOf<NativeCallWrapper>()
private val listener: CoreListenerStub = object : CoreListenerStub() { private val listener: CoreListenerStub = object : CoreListenerStub() {
override fun onCallStateChanged( override fun onCallStateChanged(
core: Core, core: Core,
@ -137,6 +139,12 @@ class TelecomHelper private constructor(context: Context) {
} }
} }
fun findConnectionForCallId(callId: String): NativeCallWrapper? {
return connections.find { connection ->
connection.callId == callId
}
}
private fun initPhoneAccount(context: Context): PhoneAccount { private fun initPhoneAccount(context: Context): PhoneAccount {
val account: PhoneAccount? = findExistingAccount(context) val account: PhoneAccount? = findExistingAccount(context)
if (account == null) { if (account == null) {

View file

@ -19,16 +19,19 @@
*/ */
package org.linphone.utils package org.linphone.utils
import android.telecom.CallAudioState
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.compatibility.Compatibility
import org.linphone.core.AudioDevice import org.linphone.core.AudioDevice
import org.linphone.core.Call import org.linphone.core.Call
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.telecom.TelecomHelper
class AudioRouteUtils { class AudioRouteUtils {
companion object { companion object {
private fun routeAudioTo( private fun applyAudioRouteChange(
call: Call?,
types: List<AudioDevice.Type>, types: List<AudioDevice.Type>,
call: Call? = null,
output: Boolean = true output: Boolean = true
) { ) {
val listSize = types.size val listSize = types.size
@ -71,28 +74,70 @@ class AudioRouteUtils {
Log.e("[Audio Route Helper] Couldn't find [$typesNames] audio device") Log.e("[Audio Route Helper] Couldn't find [$typesNames] audio device")
} }
fun routeAudioToEarpiece(call: Call? = null) { private fun changeCaptureDeviceToMatchAudioRoute(call: Call?, types: List<AudioDevice.Type>) {
routeAudioTo(arrayListOf(AudioDevice.Type.Earpiece), call) when (types.first()) {
} AudioDevice.Type.Bluetooth -> {
fun routeAudioToSpeaker(call: Call? = null) {
routeAudioTo(arrayListOf(AudioDevice.Type.Speaker), call)
}
fun routeAudioToBluetooth(call: Call? = null) {
routeAudioTo(arrayListOf(AudioDevice.Type.Bluetooth), call)
if (isBluetoothAudioRecorderAvailable()) { if (isBluetoothAudioRecorderAvailable()) {
Log.i("[Audio Route Helper] Bluetooth device is able to record audio, also change input audio device") Log.i("[Audio Route Helper] Bluetooth device is able to record audio, also change input audio device")
routeAudioTo(arrayListOf(AudioDevice.Type.Bluetooth), call, false) applyAudioRouteChange(call, arrayListOf(AudioDevice.Type.Bluetooth), false)
}
}
AudioDevice.Type.Headset, AudioDevice.Type.Headphones -> {
if (isHeadsetAudioRecorderAvailable()) {
Log.i("[Audio Route Helper] Headphones/headset device is able to record audio, also change input audio device")
applyAudioRouteChange(call, (arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset)), false)
}
}
} }
} }
fun routeAudioToHeadset(call: Call? = null) { private fun routeAudioTo(
routeAudioTo(arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset), call) call: Call?,
if (isHeadsetAudioRecorderAvailable()) { types: List<AudioDevice.Type>,
Log.i("[Audio Route Helper] Headphones/headset device is able to record audio, also change input audio device") skipTelecom: Boolean = false
routeAudioTo((arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset)), call, false) ) {
val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0]
if ((call != null || currentCall != null) && !skipTelecom && TelecomHelper.exists()) {
val callToUse = call ?: currentCall
Log.i("[Audio Route Helper] Call provided & Telecom Helper exists, trying to dispatch audio route change through Telecom API")
val connection = TelecomHelper.get().findConnectionForCallId(callToUse.callLog.callId)
if (connection != null) {
val route = when (types.first()) {
AudioDevice.Type.Earpiece -> CallAudioState.ROUTE_EARPIECE
AudioDevice.Type.Speaker -> CallAudioState.ROUTE_SPEAKER
AudioDevice.Type.Headphones, AudioDevice.Type.Headset -> CallAudioState.ROUTE_WIRED_HEADSET
AudioDevice.Type.Bluetooth, AudioDevice.Type.BluetoothA2DP -> CallAudioState.ROUTE_BLUETOOTH
else -> CallAudioState.ROUTE_WIRED_OR_EARPIECE
} }
Log.i("[Audio Route Helper] Telecom Helper & matching connection found, dispatching audio route change through it")
// We will be called here again by NativeCallWrapper.onCallAudioStateChanged()
// but this time with skipTelecom = true
Compatibility.changeAudioRouteForTelecomManager(connection, route)
} else {
Log.w("[Audio Route Helper] Telecom Helper found but no matching connection!")
applyAudioRouteChange(callToUse, types)
changeCaptureDeviceToMatchAudioRoute(callToUse, types)
}
} else {
applyAudioRouteChange(call, types)
changeCaptureDeviceToMatchAudioRoute(call, types)
}
}
fun routeAudioToEarpiece(call: Call? = null, skipTelecom: Boolean = false) {
routeAudioTo(call, arrayListOf(AudioDevice.Type.Earpiece), skipTelecom)
}
fun routeAudioToSpeaker(call: Call? = null, skipTelecom: Boolean = false) {
routeAudioTo(call, arrayListOf(AudioDevice.Type.Speaker), skipTelecom)
}
fun routeAudioToBluetooth(call: Call? = null, skipTelecom: Boolean = false) {
routeAudioTo(call, arrayListOf(AudioDevice.Type.Bluetooth), skipTelecom)
}
fun routeAudioToHeadset(call: Call? = null, skipTelecom: Boolean = false) {
routeAudioTo(call, arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset), skipTelecom)
} }
fun isSpeakerAudioRouteCurrentlyUsed(call: Call? = null): Boolean { fun isSpeakerAudioRouteCurrentlyUsed(call: Call? = null): Boolean {
@ -104,7 +149,7 @@ class AudioRouteUtils {
val conference = coreContext.core.conference val conference = coreContext.core.conference
val audioDevice = if (conference != null && conference.isIn) conference.outputAudioDevice else currentCall.outputAudioDevice val audioDevice = if (conference != null && conference.isIn) conference.outputAudioDevice else currentCall.outputAudioDevice
Log.i("[Audio Route Helper] Audio device currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})") Log.i("[Audio Route Helper] Playback audio device currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})")
return audioDevice?.type == AudioDevice.Type.Speaker return audioDevice?.type == AudioDevice.Type.Speaker
} }
@ -117,7 +162,7 @@ class AudioRouteUtils {
val conference = coreContext.core.conference val conference = coreContext.core.conference
val audioDevice = if (conference != null && conference.isIn) conference.outputAudioDevice else currentCall.outputAudioDevice val audioDevice = if (conference != null && conference.isIn) conference.outputAudioDevice else currentCall.outputAudioDevice
Log.i("[Audio Route Helper] Audio device currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})") Log.i("[Audio Route Helper] Playback audio device currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})")
return audioDevice?.type == AudioDevice.Type.Bluetooth return audioDevice?.type == AudioDevice.Type.Bluetooth
} }