mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 03:18:06 +00:00
Ignore Telecom Manager endpoints availability/requests, using our own preferred endpoint policy (to workaround device disconnect/reconnect not always notified)
This commit is contained in:
parent
d5c836b8b5
commit
3f22a596db
5 changed files with 63 additions and 207 deletions
|
|
@ -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<out AudioDeviceInfo>?) {
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<AudioDevice.Type>()
|
||||
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<AudioDevice.Type>): 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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AudioDevice.Type>, 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AudioDevice.Type>,
|
||||
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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue