diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 487e9b42e..f80055ac4 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -52,6 +52,7 @@ import org.linphone.telecom.TelecomManager import org.linphone.ui.call.CallActivity import org.linphone.utils.ActivityMonitor import org.linphone.utils.AppUtils +import org.linphone.utils.AudioUtils import org.linphone.utils.Event import org.linphone.utils.FileUtils import org.linphone.utils.LinphoneUtils @@ -143,20 +144,29 @@ class CoreContext override fun onAudioDevicesAdded(addedDevices: Array?) { if (!addedDevices.isNullOrEmpty()) { Log.i("$TAG [${addedDevices.size}] new device(s) have been added:") + var atLeastOneNewDeviceIsBluetooth = false for (device in addedDevices) { Log.i( "$TAG Added device [${device.productName}] with ID [${device.id}] and type [${device.type}]" ) + + when (device.type) { + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_BLE_SPEAKER, AudioDeviceInfo.TYPE_HEARING_AID -> { + atLeastOneNewDeviceIsBluetooth = true + } + } } - if (telecomManager.getCurrentlyFollowedCalls() <= 0) { - Log.i("$TAG No call found in Telecom's CallsManager, reloading sound devices in 500ms") - postOnCoreThreadDelayed({ core.reloadSoundDevices() }, 500) - } else { - Log.i( - "$TAG At least one active call in Telecom's CallsManager, let it handle the added device(s)" - ) - } + Log.i("$TAG Reloading sound devices in 500ms") + postOnCoreThreadDelayed({ + Log.i("$TAG Reloading sound devices") + core.reloadSoundDevices() + + if (atLeastOneNewDeviceIsBluetooth && core.callsNb > 0 && corePreferences.routeAudioToBluetoothWhenPossible) { + Log.i("$TAG It seems a bluetooth device is now available, trying to route audio to it") + AudioUtils.routeAudioToEitherBluetoothOrHearingAid() + } + }, 500) } } @@ -169,14 +179,12 @@ class CoreContext "$TAG Removed device [${device.id}][${device.productName}][${device.type}]" ) } - if (telecomManager.getCurrentlyFollowedCalls() <= 0) { - Log.i("$TAG No call found in Telecom's CallsManager, reloading sound devices in 500ms") - postOnCoreThreadDelayed({ core.reloadSoundDevices() }, 500) - } else { - Log.i( - "$TAG At least one active call in Telecom's CallsManager, let it handle the removed device(s)" - ) - } + + Log.i("$TAG Reloading sound devices in 500ms") + postOnCoreThreadDelayed({ + Log.i("$TAG Reloading sound devices") + core.reloadSoundDevices() + }, 500) } } } @@ -348,10 +356,20 @@ class CoreContext ) } } + Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> { + if (corePreferences.routeAudioToBluetoothWhenPossible) { + Log.i("$TAG Trying to route audio to either bluetooth or hearing aid if available") + AudioUtils.routeAudioToEitherBluetoothOrHearingAid(call) + } + } Call.State.Connected -> { postOnMainThread { showCallActivity() } + if (corePreferences.routeAudioToBluetoothWhenPossible) { + Log.i("$TAG Call is connected, trying to route audio to either bluetooth or hearing aid if available") + AudioUtils.routeAudioToEitherBluetoothOrHearingAid(call) + } } Call.State.StreamsRunning -> { if (previousCallState == Call.State.Connected) { @@ -407,6 +425,11 @@ class CoreContext Log.i("$TAG Available audio devices list was updated") } + @WorkerThread + override fun onFirstCallStarted(core: Core) { + Log.i("$TAG First call started") + } + @WorkerThread override fun onLastCallEnded(core: Core) { Log.i("$TAG Last call ended") diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 953931de7..51437bbbe 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -127,6 +127,13 @@ class CorePreferences // Call settings // This won't be done if bluetooth or wired headset is used + @get:AnyThread @set:WorkerThread + var routeAudioToBluetoothWhenPossible: Boolean + get() = config.getBool("app", "route_audio_to_bluetooth_when_possible", true) + set(value) { + config.setBool("app", "route_audio_to_bluetooth_when_possible", value) + } + @get:AnyThread @set:WorkerThread var routeAudioToSpeakerWhenVideoIsEnabled: Boolean get() = config.getBool("app", "route_audio_to_speaker_when_video_enabled", true) diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index 46b31ff90..e9953930d 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -26,13 +26,11 @@ import androidx.core.telecom.CallControlResult import androidx.core.telecom.CallControlScope import androidx.core.telecom.CallEndpointCompat import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.core.AudioDevice import org.linphone.core.Call import org.linphone.core.CallListenerStub import org.linphone.core.Reason @@ -125,75 +123,11 @@ class TelecomCallControlCallback( } callControl.availableEndpoints.onEach { list -> - Log.i("$TAG New available audio endpoints list") - if (availableEndpoints != list) { - Log.i( - "$TAG List size of available audio endpoints has changed, reload sound devices in SDK in [$DELAY_BEFORE_RELOADING_SOUND_DEVICES_MS] ms" - ) - coreContext.postOnCoreThreadDelayed({ core -> - core.reloadSoundDevices() - Log.i("$TAG Sound devices reloaded") - }, DELAY_BEFORE_RELOADING_SOUND_DEVICES_MS) - } - - availableEndpoints = list - for (endpoint in list) { - Log.i("$TAG Available audio endpoint [${endpoint.name}]") - } + Log.i("$TAG New available audio endpoints list but ignoring it") }.launchIn(scope) callControl.currentCallEndpoint.onEach { endpoint -> - var newEndpointToUse = endpoint - if (endpointUpdateRequestFromLinphone) { - Log.i("$TAG Linphone requests to use [${endpoint.name}] audio endpoint with type [${endpointTypeToString(endpoint.type)}]") - } else { - Log.i("$TAG Android requests us to use [${endpoint.name}] audio endpoint with type [${endpointTypeToString(endpoint.type)}]") - } - - val requestedEndpoint = latestLinphoneRequestedEndpoint - if (endpointUpdateRequestFromLinphone && requestedEndpoint != null && requestedEndpoint != endpoint) { - Log.w("$TAG WARNING: Linphone requested endpoint [${requestedEndpoint.name}] but Telecom Manager notified endpoint [${endpoint.name}], trying to use the one we requested anyway") - newEndpointToUse = requestedEndpoint - } - - val type = newEndpointToUse.type - currentEndpoint = type - if (!endpointUpdateRequestFromLinphone && !coreContext.isConnectedToAndroidAuto && (type == CallEndpointCompat.Companion.TYPE_EARPIECE || type == CallEndpointCompat.Companion.TYPE_SPEAKER)) { - endpointUpdateRequestFromLinphone = false - Log.w("$TAG Device isn't connected to Android Auto, do not follow system request to change audio endpoint to [${newEndpointToUse.name}] with type [${endpointTypeToString(type)}]") - return@onEach - } - endpointUpdateRequestFromLinphone = false - - // Change audio route in SDK, this way the usual listener will trigger - // and we'll be able to update the UI accordingly - val route = arrayListOf() - when (type) { - CallEndpointCompat.Companion.TYPE_EARPIECE -> { - route.add(AudioDevice.Type.Earpiece) - } - CallEndpointCompat.Companion.TYPE_SPEAKER -> { - route.add(AudioDevice.Type.Speaker) - } - CallEndpointCompat.Companion.TYPE_BLUETOOTH -> { - route.add(AudioDevice.Type.Bluetooth) - route.add(AudioDevice.Type.HearingAid) - } - CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> { - route.add(AudioDevice.Type.Headphones) - route.add(AudioDevice.Type.Headset) - } - } - if (route.isNotEmpty()) { - coreContext.postOnCoreThread { - if (!AudioUtils.applyAudioRouteChangeInLinphone(call, route)) { - Log.w("$TAG Failed to apply audio route change, trying again in 200ms") - coreContext.postOnCoreThreadDelayed({ - AudioUtils.applyAudioRouteChangeInLinphone(call, route) - }, 200) - } - } - } + Log.i("$TAG Android requests us to use [${endpoint.name}] audio endpoint with type [${endpointTypeToString(endpoint.type)}], ignoring it") }.launchIn(scope) callControl.isMuted.onEach { muted -> @@ -224,89 +158,12 @@ class TelecomCallControlCallback( }.launchIn(scope) } - fun applyAudioRouteToCallWithId(routes: List): Boolean { - Log.i("$TAG Looking for audio endpoint with type [${routes.first()}]") - - var wiredHeadsetFound = false - var skippedBecauseAlreadyInUse = false - for (endpoint in availableEndpoints) { - Log.i( - "$TAG Found audio endpoint [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]" - ) - val matches = when (endpoint.type) { - CallEndpointCompat.Companion.TYPE_EARPIECE -> { - routes.find { it == AudioDevice.Type.Earpiece } - } - CallEndpointCompat.Companion.TYPE_SPEAKER -> { - routes.find { it == AudioDevice.Type.Speaker } - } - CallEndpointCompat.Companion.TYPE_BLUETOOTH -> { - routes.find { it == AudioDevice.Type.Bluetooth || it == AudioDevice.Type.HearingAid } - } - CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> { - wiredHeadsetFound = true - routes.find { it == AudioDevice.Type.Headset || it == AudioDevice.Type.Headphones } - } - else -> null - } - - if (matches != null) { - Log.i( - "$TAG Found matching audio endpoint [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}], trying to use it" - ) - if (currentEndpoint == endpoint.type) { - Log.w("$TAG Endpoint already in use, skipping") - skippedBecauseAlreadyInUse = true - continue - } - - var success = false - scope.launch { - Log.i("$TAG Requesting audio endpoint change to [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]") - endpointUpdateRequestFromLinphone = true - latestLinphoneRequestedEndpoint = endpoint - var result: CallControlResult = callControl.requestEndpointChange(endpoint) - var attempts = 1 - while (result is CallControlResult.Error && attempts <= 2) { - delay(100) - Log.i( - "$TAG Previous attempt failed [$result], requesting again audio endpoint change to [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]" - ) - result = callControl.requestEndpointChange(endpoint) - attempts += 1 - } - - if (result is CallControlResult.Error) { - Log.e("$TAG Failed to change endpoint audio device, error [$result]") - } else { - Log.i( - "$TAG It took [$attempts] attempt(s) to change endpoint audio device..." - ) - currentEndpoint = endpoint.type - success = true - } - } - - return success - } - } - - if (routes.size == 1 && routes[0] == AudioDevice.Type.Earpiece && wiredHeadsetFound) { - Log.e("$TAG User asked for earpiece but endpoint doesn't exists!") - } else if (skippedBecauseAlreadyInUse) { - Log.w("$TAG This endpoint was already in use (according to Telecom Manager), force changing the device in Linphone just in case") - } else { - Log.e("$TAG No matching endpoint found") - } - return false - } - private fun answerCall() { val isVideo = LinphoneUtils.isVideoEnabled(call) val type = if (isVideo) { - CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL + CallAttributesCompat.CALL_TYPE_VIDEO_CALL } else { - CallAttributesCompat.Companion.CALL_TYPE_AUDIO_CALL + CallAttributesCompat.CALL_TYPE_AUDIO_CALL } scope.launch { Log.i("$TAG Answering [${if (isVideo) "video" else "audio"}] call") @@ -389,12 +246,12 @@ class TelecomCallControlCallback( private fun endpointTypeToString(type: Int): String { return when (type) { - CallEndpointCompat.Companion.TYPE_UNKNOWN -> "UNKNOWN" - CallEndpointCompat.Companion.TYPE_EARPIECE -> "EARPIECE" - CallEndpointCompat.Companion.TYPE_BLUETOOTH -> "BLUETOOTH" - CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> "WIRED HEADSET" - CallEndpointCompat.Companion.TYPE_SPEAKER -> "SPEAKER" - CallEndpointCompat.Companion.TYPE_STREAMING -> "STREAMING" + CallEndpointCompat.TYPE_UNKNOWN -> "UNKNOWN" + CallEndpointCompat.TYPE_EARPIECE -> "EARPIECE" + CallEndpointCompat.TYPE_BLUETOOTH -> "BLUETOOTH" + CallEndpointCompat.TYPE_WIRED_HEADSET -> "WIRED HEADSET" + CallEndpointCompat.TYPE_SPEAKER -> "SPEAKER" + CallEndpointCompat.TYPE_STREAMING -> "STREAMING" else -> "UNEXPECTED: $type" } } diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt index f71b43fe8..4a9867f92 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomManager.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.AudioDevice import org.linphone.core.Call import org.linphone.core.Core import org.linphone.core.CoreListenerStub @@ -89,11 +88,6 @@ class TelecomManager } } - @WorkerThread - fun getCurrentlyFollowedCalls(): Int { - return currentlyFollowedCalls - } - @WorkerThread fun onCallCreated(call: Call) { Log.i("$TAG Call to [${call.remoteAddress.asStringUriOnly()}] created in state [${call.state}]") @@ -208,18 +202,4 @@ class TelecomManager Log.i("$TAG Core is being stopped") core.removeListener(coreListener) } - - @WorkerThread - fun applyAudioRouteToCallWithId(routes: List, callId: String): Boolean { - Log.i( - "$TAG Looking for audio endpoint with type [${routes.first()}] for call with ID [$callId]" - ) - val callControlCallback = map[callId] - if (callControlCallback == null) { - Log.w("$TAG Failed to find callbacks for call with ID [$callId]") - return false - } - - return callControlCallback.applyAudioRouteToCallWithId(routes) - } } diff --git a/app/src/main/java/org/linphone/utils/AudioUtils.kt b/app/src/main/java/org/linphone/utils/AudioUtils.kt index 3ac0268cf..83762908c 100644 --- a/app/src/main/java/org/linphone/utils/AudioUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioUtils.kt @@ -55,6 +55,11 @@ class AudioUtils { routeAudioTo(call, arrayListOf(AudioDevice.Type.HearingAid)) } + @WorkerThread + fun routeAudioToEitherBluetoothOrHearingAid(call: Call? = null) { + routeAudioTo(call, arrayListOf(AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid)) + } + @WorkerThread fun routeAudioToHeadset(call: Call? = null) { routeAudioTo( @@ -85,8 +90,7 @@ class AudioUtils { private fun applyAudioRouteChange( call: Call?, types: List, - output: Boolean = true, - skipTelecom: Boolean = false + output: Boolean = true ) { val currentCall = if (coreContext.core.callsNb > 0) { call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] @@ -94,22 +98,7 @@ class AudioUtils { Log.w("$TAG No call found, setting audio route on Core") null } - - if (!skipTelecom) { - val callId = currentCall?.callLog?.callId.orEmpty() - Log.i("$TAG Trying to change audio endpoint using Telecom Manager APIs") - val success = coreContext.telecomManager.applyAudioRouteToCallWithId(types, callId) - if (!success) { - Log.w("$TAG Failed to change audio endpoint to [$types] for call ID [$callId]") - applyAudioRouteChange(currentCall, types, output, skipTelecom = true) - } else { - Log.i("$TAG It seems audio endpoint update using Telecom Manager was successful") - return - } - } else { - Log.i("$TAG Trying to change audio endpoint directly in Linphone SDK") - applyAudioRouteChangeInLinphone(currentCall, types, output) - } + applyAudioRouteChangeInLinphone(currentCall, types, output) } fun applyAudioRouteChangeInLinphone(