diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 7b4dcffe7..47e5f4f66 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -54,7 +54,7 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C val notificationsManager = NotificationsManager(context) - private val telecomManager = TelecomManager(context) + val telecomManager = TelecomManager(context) private val activityMonitor = ActivityMonitor() diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index 25d1ab3bc..2e6c6db45 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -23,14 +23,18 @@ import android.telecom.DisconnectCause import androidx.core.telecom.CallAttributesCompat import androidx.core.telecom.CallControlCallback 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.core.AudioDevice import org.linphone.core.Call import org.linphone.core.CallListenerStub import org.linphone.core.tools.Log +import org.linphone.utils.AudioRouteUtils class TelecomCallControlCallback constructor( private val call: Call, @@ -41,14 +45,21 @@ class TelecomCallControlCallback constructor( private const val TAG = "[Telecom Call Control Callback]" } + private var availableEndpoints: List = arrayListOf() + private val callListener = object : CallListenerStub() { override fun onStateChanged(call: Call, state: Call.State?, message: String) { Log.i("$TAG Call state changed [$state]") if (state == Call.State.Connected) { if (call.dir == Call.Dir.Incoming) { scope.launch { - Log.i("$TAG Answering call") - callControl.answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL) // TODO + val type = if (call.currentParams.isVideoEnabled) { + CallAttributesCompat.CALL_TYPE_VIDEO_CALL + } else { + CallAttributesCompat.CALL_TYPE_AUDIO_CALL + } + Log.i("$TAG Answering call with type [$type]") + callControl.answer(type) } } else { scope.launch { @@ -90,6 +101,7 @@ class TelecomCallControlCallback constructor( callControl.availableEndpoints.onEach { list -> Log.i("$TAG New available audio endpoints list") + availableEndpoints = list for (endpoint in list) { Log.i("$TAG Available audio endpoint [${endpoint.name}]") } @@ -97,6 +109,30 @@ class TelecomCallControlCallback constructor( callControl.currentCallEndpoint.onEach { endpoint -> Log.i("$TAG We're asked to use [${endpoint.name}] audio endpoint") + // 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 (endpoint.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) + } + CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> { + route.add(AudioDevice.Type.Headphones) + route.add(AudioDevice.Type.Headset) + } + else -> null + } + if (route.isNotEmpty()) { + coreContext.postOnCoreThread { + AudioRouteUtils.applyAudioRouteChangeInLinphone(call, route) + } + } }.launchIn(scope) callControl.isMuted.onEach { muted -> @@ -105,12 +141,63 @@ class TelecomCallControlCallback constructor( }.launchIn(scope) } + fun applyAudioRouteToCallWithId(routes: List) { + Log.i("$TAG Looking for audio endpoint with type [${routes.first()}]") + + for (endpoint in availableEndpoints) { + Log.i( + "$TAG Found audio endpoint [${endpoint.name}] with type [${endpoint.type}]" + ) + val found = 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 } + } + CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> { + routes.find { it == AudioDevice.Type.Headset || it == AudioDevice.Type.Headphones } + } + else -> null + } + + if (found != null) { + Log.i( + "$TAG Found matching audio endpoint [${endpoint.name}], trying to use it" + ) + + scope.launch { + Log.i("$TAG Requesting audio endpoint change with [${endpoint.name}]") + var audioRouteUpdated = callControl.requestEndpointChange(endpoint) + var attempts = 1 + while (!audioRouteUpdated && attempts <= 10) { + delay(100) + Log.i("$TAG Requesting audio endpoint change with [${endpoint.name}]") + audioRouteUpdated = callControl.requestEndpointChange(endpoint) + attempts += 1 + } + + if (!audioRouteUpdated) { + Log.e("$TAG Failed to change endpoint audio device!") + } else { + Log.i("$TAG It took [$attempts] to change endpoint audio device...") + } + } + } else { + Log.w("$TAG No matching audio endpoint found...") + } + } + } + override suspend fun onAnswer(callType: Int): Boolean { - Log.i("$TAG We're asked to answer the call") + Log.i("$TAG We're asked to answer the call with type [$callType]") coreContext.postOnCoreThread { if (call.state == Call.State.IncomingReceived || call.state == Call.State.IncomingEarlyMedia) { Log.i("$TAG Answering call") - call.accept() + coreContext.answerCall(call) // TODO: use call type } } return true @@ -120,7 +207,7 @@ class TelecomCallControlCallback constructor( Log.i("$TAG We're asked to terminate the call with reason [$disconnectCause]") coreContext.postOnCoreThread { Log.i("$TAG Terminating call") - call.terminate() + call.terminate() // TODO: use cause } return true } diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt index 6cc73ac42..a87ba5385 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomManager.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt @@ -29,6 +29,7 @@ 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 @@ -77,7 +78,7 @@ class TelecomManager @WorkerThread constructor(context: Context) { coreContext.postOnCoreThread { val callId = call.callLog.callId.orEmpty() if (callId.isNotEmpty()) { - Log.i("$TAG Storing callbacks (why?) for call ID [$callId]") + Log.i("$TAG Storing our callbacks for call ID [$callId]") map[callId] = callbacks } } @@ -108,4 +109,19 @@ class TelecomManager @WorkerThread constructor(context: Context) { 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 + } + + callControlCallback.applyAudioRouteToCallWithId(routes) + return true + } } diff --git a/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt index 8ed2a9ecf..2fe5c285a 100644 --- a/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt @@ -182,7 +182,7 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { coreContext.postOnCoreThread { if (::call.isInitialized) { Log.i("$TAG Answering call [$call]") - call.accept() + coreContext.answerCall(call) } } } @@ -244,7 +244,12 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() { coreContext.postOnCoreThread { Log.i("$TAG Selected audio device with ID [${device.id}]") if (::call.isInitialized) { - call.outputAudioDevice = device + when { + isHeadset -> AudioRouteUtils.routeAudioToHeadset(call) + isBluetooth -> AudioRouteUtils.routeAudioToBluetooth(call) + isSpeaker -> AudioRouteUtils.routeAudioToSpeaker(call) + else -> AudioRouteUtils.routeAudioToEarpiece(call) + } } } } diff --git a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt index bdfe11453..769104e64 100644 --- a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt @@ -29,31 +29,6 @@ class AudioRouteUtils { companion object { private const val TAG = "[Audio Route Utils]" - @WorkerThread - fun isSpeakerAudioRouteCurrentlyUsed(call: Call? = null): Boolean { - val currentCall = if (coreContext.core.callsNb > 0) { - call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] - } else { - Log.w("$TAG No call found, checking audio route on Core") - null - } - val conference = coreContext.core.conference - - val audioDevice = if (conference != null && conference.isIn) { - conference.outputAudioDevice - } else if (currentCall != null) { - currentCall.outputAudioDevice - } else { - coreContext.core.outputAudioDevice - } - - if (audioDevice == null) return false - Log.i( - "$TAG Playback audio device currently in use is [${audioDevice.deviceName} (${audioDevice.driverName}) ${audioDevice.type}]" - ) - return audioDevice.type == AudioDevice.Type.Speaker - } - @WorkerThread fun routeAudioToEarpiece(call: Call? = null) { routeAudioTo(call, arrayListOf(AudioDevice.Type.Earpiece)) @@ -85,10 +60,8 @@ class AudioRouteUtils { val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() if (currentCall != null) { applyAudioRouteChange(currentCall, types) - changeCaptureDeviceToMatchAudioRoute(currentCall, types) } else { - applyAudioRouteChange(call, types) - changeCaptureDeviceToMatchAudioRoute(call, types) + applyAudioRouteChange(null, types) } } @@ -96,7 +69,8 @@ class AudioRouteUtils { private fun applyAudioRouteChange( call: Call?, types: List, - output: Boolean = true + output: Boolean = true, + skipTelecom: Boolean = false ) { val currentCall = if (coreContext.core.callsNb > 0) { call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] @@ -105,6 +79,25 @@ class AudioRouteUtils { null } + if (!skipTelecom) { + val callId = currentCall?.callLog?.callId.orEmpty() + 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 { + return + } + } + + applyAudioRouteChangeInLinphone(currentCall, types, output) + } + + fun applyAudioRouteChangeInLinphone( + call: Call?, + types: List, + output: Boolean = true + ) { val capability = if (output) { AudioDevice.Capabilities.CapabilityPlay } else { @@ -147,14 +140,14 @@ class AudioRouteUtils { } return } - if (currentCall != null) { + if (call != null) { Log.i( "$TAG Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], routing call audio to it" ) if (output) { - currentCall.outputAudioDevice = audioDevice + call.outputAudioDevice = audioDevice } else { - currentCall.inputAudioDevice = audioDevice + call.inputAudioDevice = audioDevice } } else { Log.i( @@ -167,20 +160,5 @@ class AudioRouteUtils { } } } - - @WorkerThread - private fun changeCaptureDeviceToMatchAudioRoute(call: Call?, types: List) { - when (types.first()) { - AudioDevice.Type.Earpiece, AudioDevice.Type.Speaker -> { - Log.i( - "$TAG Audio route requested to Earpiece or Speaker, setting input to Microphone" - ) - applyAudioRouteChange(call, (arrayListOf(AudioDevice.Type.Microphone)), false) - } - else -> { - Log.w("$TAG Unexpected audio device type: ${types.first()}") - } - } - } } }