From f66c90d3562a482f825967b674a1cfadc1e67676 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 3 Nov 2021 13:48:54 +0100 Subject: [PATCH] Reworked audio route changes to make them go through Telecom Manager API if enabled to make smartwatches act as bluetooth headset properly --- .../compatibility/Api26Compatibility.kt | 5 ++ .../linphone/compatibility/Compatibility.kt | 7 ++ .../org/linphone/telecom/NativeCallWrapper.kt | 8 +- .../telecom/TelecomConnectionService.kt | 24 ++--- .../org/linphone/telecom/TelecomHelper.kt | 8 ++ .../org/linphone/utils/AudioRouteUtils.kt | 89 ++++++++++++++----- 6 files changed, 99 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt index 8a5b1daec..8f66015e0 100644 --- a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt @@ -34,6 +34,7 @@ import android.view.WindowManager import androidx.core.app.NotificationManagerCompat import org.linphone.R import org.linphone.core.tools.Log +import org.linphone.telecom.NativeCallWrapper @TargetApi(26) class Api26Compatibility { @@ -133,5 +134,9 @@ class Api26Compatibility { .build() vibrator.vibrate(effect, audioAttrs) } + + fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int) { + connection.setAudioRoute(route) + } } } diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt index 8079c1c86..c1c23718a 100644 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -33,6 +33,7 @@ import androidx.core.app.NotificationManagerCompat import org.linphone.core.ChatRoom import org.linphone.core.Content import org.linphone.mediastream.Version +import org.linphone.telecom.NativeCallWrapper @Suppress("DEPRECATION") 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 */ fun createShortcutsToContacts(context: Context) { diff --git a/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt b/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt index 04e73a289..05e7fe67f 100644 --- a/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt +++ b/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt @@ -72,10 +72,10 @@ class NativeCallWrapper(var callId: String) : Connection() { if (call != null) { call.microphoneMuted = state.isMuted when (state.route) { - CallAudioState.ROUTE_EARPIECE -> AudioRouteUtils.routeAudioToEarpiece(call) - CallAudioState.ROUTE_SPEAKER -> AudioRouteUtils.routeAudioToSpeaker(call) - CallAudioState.ROUTE_BLUETOOTH -> AudioRouteUtils.routeAudioToBluetooth(call) - CallAudioState.ROUTE_WIRED_HEADSET -> AudioRouteUtils.routeAudioToHeadset(call) + CallAudioState.ROUTE_EARPIECE -> AudioRouteUtils.routeAudioToEarpiece(call, true) + CallAudioState.ROUTE_SPEAKER -> AudioRouteUtils.routeAudioToSpeaker(call, true) + CallAudioState.ROUTE_BLUETOOTH -> AudioRouteUtils.routeAudioToBluetooth(call, true) + CallAudioState.ROUTE_WIRED_HEADSET -> AudioRouteUtils.routeAudioToHeadset(call, true) } } else { selfDestroy() diff --git a/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt b/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt index b34a13e48..ac8f8da9c 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt @@ -30,8 +30,6 @@ import org.linphone.core.CoreListenerStub import org.linphone.core.tools.Log class TelecomConnectionService : ConnectionService() { - private val connections = arrayListOf() - private val listener: CoreListenerStub = object : CoreListenerStub() { override fun onCallStateChanged( core: Core, @@ -42,7 +40,7 @@ class TelecomConnectionService : ConnectionService() { Log.i("[Telecom Connection Service] call [${call.callLog.callId}] state changed: $state") when (call.state) { Call.State.OutgoingProgress -> { - for (connection in connections) { + for (connection in TelecomHelper.get().connections) { if (connection.callId.isEmpty()) { connection.callId = core.currentCall?.callLog?.callId ?: "" } @@ -105,7 +103,7 @@ class TelecomConnectionService : ConnectionService() { connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED) Log.i("[Telecom Connection Service] Address is $providedHandle") - connections.add(connection) + TelecomHelper.get().connections.add(connection) connection } else { Log.e("[Telecom Connection Service] Error: $accountHandle $componentName") @@ -150,7 +148,7 @@ class TelecomConnectionService : ConnectionService() { connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED) Log.i("[Telecom Connection Service] Address is $providedHandle") - connections.add(connection) + TelecomHelper.get().connections.add(connection) connection } else { 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) { - val connection = getConnectionForCallId(call.callLog.callId) + val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId) connection ?: return - connections.remove(connection) + TelecomHelper.get().connections.remove(connection) connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR)) connection.destroy() } private fun onCallEnded(call: Call) { - val connection = getConnectionForCallId(call.callLog.callId) + val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId) connection ?: return - connections.remove(connection) + TelecomHelper.get().connections.remove(connection) val reason = call.reason Log.i("[Telecom Connection Service] Call ended with reason: $reason") connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) @@ -190,7 +182,7 @@ class TelecomConnectionService : ConnectionService() { } private fun onCallConnected(call: Call) { - val connection = getConnectionForCallId(call.callLog.callId) + val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId) connection ?: return if (connection.state != Connection.STATE_HOLDING) { diff --git a/app/src/main/java/org/linphone/telecom/TelecomHelper.kt b/app/src/main/java/org/linphone/telecom/TelecomHelper.kt index c9161ebee..531cb028b 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomHelper.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomHelper.kt @@ -49,6 +49,8 @@ class TelecomHelper private constructor(context: Context) { private var account: PhoneAccount = initPhoneAccount(context) + val connections = arrayListOf() + private val listener: CoreListenerStub = object : CoreListenerStub() { override fun onCallStateChanged( 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 { val account: PhoneAccount? = findExistingAccount(context) if (account == null) { diff --git a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt index 4fa2cf5db..d3f06fda9 100644 --- a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt @@ -19,16 +19,19 @@ */ package org.linphone.utils +import android.telecom.CallAudioState import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.compatibility.Compatibility import org.linphone.core.AudioDevice import org.linphone.core.Call import org.linphone.core.tools.Log +import org.linphone.telecom.TelecomHelper class AudioRouteUtils { companion object { - private fun routeAudioTo( + private fun applyAudioRouteChange( + call: Call?, types: List, - call: Call? = null, output: Boolean = true ) { val listSize = types.size @@ -71,30 +74,72 @@ class AudioRouteUtils { Log.e("[Audio Route Helper] Couldn't find [$typesNames] audio device") } - fun routeAudioToEarpiece(call: Call? = null) { - routeAudioTo(arrayListOf(AudioDevice.Type.Earpiece), call) - } - - fun routeAudioToSpeaker(call: Call? = null) { - routeAudioTo(arrayListOf(AudioDevice.Type.Speaker), call) - } - - fun routeAudioToBluetooth(call: Call? = null) { - routeAudioTo(arrayListOf(AudioDevice.Type.Bluetooth), call) - if (isBluetoothAudioRecorderAvailable()) { - Log.i("[Audio Route Helper] Bluetooth device is able to record audio, also change input audio device") - routeAudioTo(arrayListOf(AudioDevice.Type.Bluetooth), call, false) + private fun changeCaptureDeviceToMatchAudioRoute(call: Call?, types: List) { + when (types.first()) { + AudioDevice.Type.Bluetooth -> { + if (isBluetoothAudioRecorderAvailable()) { + Log.i("[Audio Route Helper] Bluetooth device is able to record audio, also change input audio device") + 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) { - routeAudioTo(arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset), call) - if (isHeadsetAudioRecorderAvailable()) { - Log.i("[Audio Route Helper] Headphones/headset device is able to record audio, also change input audio device") - routeAudioTo((arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset)), call, false) + private fun routeAudioTo( + call: Call?, + types: List, + skipTelecom: Boolean = 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 { if (coreContext.core.callsNb == 0) { Log.w("[Audio Route Helper] No call found, so speaker audio route isn't used") @@ -104,7 +149,7 @@ class AudioRouteUtils { val conference = coreContext.core.conference 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 } @@ -117,7 +162,7 @@ class AudioRouteUtils { val conference = coreContext.core.conference 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 }