|
|
|
|
@ -33,6 +33,12 @@ class UtunConnection(
|
|
|
|
|
|
|
|
|
|
/** Доступные CallEndpoint'ы (для requestCallEndpointChange), обновляются системой. */ |
|
|
|
|
private val availableEndpoints = CopyOnWriteArrayList<CallEndpoint>() |
|
|
|
|
var onRouteChanged: ((AudioRoute) -> Unit)? = null |
|
|
|
|
var confirmedRoute: AudioRoute? = null |
|
|
|
|
private set |
|
|
|
|
private var wantedRoute: AudioRoute? = null |
|
|
|
|
private var requestedEndpoint: android.os.ParcelUuid? = null |
|
|
|
|
private var requestSerial = 0L |
|
|
|
|
|
|
|
|
|
init { |
|
|
|
|
setConnectionProperties(PROPERTY_SELF_MANAGED) |
|
|
|
|
@ -60,6 +66,36 @@ class UtunConnection(
|
|
|
|
|
override fun onCallAudioStateChanged(audioState: CallAudioState?) { |
|
|
|
|
LogManager.addLog("DEBUG", "UtunConn", |
|
|
|
|
"audioState route=%s muted=%s".format(audioState?.route ?: -1, audioState?.isMuted ?: false)) |
|
|
|
|
if (Build.VERSION.SDK_INT < 34 && audioState != null) { |
|
|
|
|
val route = when (audioState.route) { |
|
|
|
|
CallAudioState.ROUTE_EARPIECE -> AudioRoute.EARPIECE |
|
|
|
|
CallAudioState.ROUTE_SPEAKER -> AudioRoute.SPEAKER |
|
|
|
|
CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_WIRED_HEADSET -> AudioRoute.HEADSET |
|
|
|
|
else -> null |
|
|
|
|
} |
|
|
|
|
if (route != null) confirmRoute(route) |
|
|
|
|
else LogManager.addLog("WARN", "UtunConn", "unrecognized audio route=${audioState.route}") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override fun onCallEndpointChanged(endpoint: CallEndpoint) { |
|
|
|
|
val route = when (endpoint.endpointType) { |
|
|
|
|
CallEndpoint.TYPE_EARPIECE -> AudioRoute.EARPIECE |
|
|
|
|
CallEndpoint.TYPE_SPEAKER -> AudioRoute.SPEAKER |
|
|
|
|
CallEndpoint.TYPE_BLUETOOTH, CallEndpoint.TYPE_WIRED_HEADSET -> AudioRoute.HEADSET |
|
|
|
|
else -> null |
|
|
|
|
} |
|
|
|
|
if (route != null) confirmRoute(route, endpoint.identifier) |
|
|
|
|
else LogManager.addLog("WARN", "UtunConn", "unrecognized endpoint type=${endpoint.endpointType}") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun confirmRoute(route: AudioRoute, endpoint: android.os.ParcelUuid? = null) { |
|
|
|
|
confirmedRoute = route |
|
|
|
|
if (wantedRoute == route && (requestedEndpoint == null || requestedEndpoint == endpoint)) { |
|
|
|
|
wantedRoute = null; requestedEndpoint = null |
|
|
|
|
} |
|
|
|
|
LogManager.addLog("INFO", "UtunConn", "route confirmed id=$callId route=$route pending=$wantedRoute") |
|
|
|
|
onRouteChanged?.invoke(route) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override fun onAvailableCallEndpointsChanged(availableEndpoints: MutableList<CallEndpoint>) { |
|
|
|
|
@ -67,6 +103,7 @@ class UtunConnection(
|
|
|
|
|
this.availableEndpoints.addAll(availableEndpoints) |
|
|
|
|
LogManager.addLog("DEBUG", "UtunConn", |
|
|
|
|
"availableEndpoints -> %s".format(availableEndpoints.joinToString { "${it.endpointName}/${it.endpointType}" })) |
|
|
|
|
wantedRoute?.let { routeTo(it) } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** Перевести вызов в звонящее состояние (входящий). */ |
|
|
|
|
@ -88,10 +125,19 @@ class UtunConnection(
|
|
|
|
|
|
|
|
|
|
/** Маршрутизация аудио через Telecom (CallEndpoint API на Android 14+, иначе setAudioRoute). */ |
|
|
|
|
fun routeTo(route: AudioRoute) { |
|
|
|
|
wantedRoute = route |
|
|
|
|
if (confirmedRoute == route && requestedEndpoint == null) { wantedRoute = null; return } |
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { |
|
|
|
|
val type = endpointTypeFor(route) |
|
|
|
|
val ep = availableEndpoints.firstOrNull { it.endpointType == type } |
|
|
|
|
val ep = when (route) { |
|
|
|
|
AudioRoute.EARPIECE -> availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_EARPIECE } |
|
|
|
|
AudioRoute.SPEAKER -> availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_SPEAKER } |
|
|
|
|
AudioRoute.HEADSET -> availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_BLUETOOTH } |
|
|
|
|
?: availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_WIRED_HEADSET } |
|
|
|
|
} |
|
|
|
|
if (ep != null) { |
|
|
|
|
if (requestedEndpoint == ep.identifier) return |
|
|
|
|
requestedEndpoint = ep.identifier |
|
|
|
|
val serial = ++requestSerial |
|
|
|
|
try { |
|
|
|
|
val executor = ContextCompat.getMainExecutor(ChatApplication.instance) |
|
|
|
|
requestCallEndpointChange(ep, executor, object : OutcomeReceiver<Void, CallEndpointException> { |
|
|
|
|
@ -99,21 +145,25 @@ class UtunConnection(
|
|
|
|
|
LogManager.addLog("INFO", "UtunConn", "endpoint change ok -> %d".format(ep.endpointType)) |
|
|
|
|
} |
|
|
|
|
override fun onError(error: CallEndpointException) { |
|
|
|
|
if (requestSerial == serial) { requestedEndpoint = null; wantedRoute = null } |
|
|
|
|
LogManager.addLog("WARN", "UtunConn", "endpoint change failed: %s".format(error.message)) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
return |
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
requestedEndpoint = null; wantedRoute = null |
|
|
|
|
LogManager.addLog("WARN", "UtunConn", "requestCallEndpointChange threw: %s".format(e.message)) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
LogManager.addLog("DEBUG", "UtunConn", "no endpoint of type %d, fallback to setAudioRoute".format(type)) |
|
|
|
|
LogManager.addLog("WARN", "UtunConn", "route waiting for endpoint id=$callId requested=$route") |
|
|
|
|
} |
|
|
|
|
return // На API 34+ один путь управления: CallEndpoint, без параллельного legacy-запроса. |
|
|
|
|
} |
|
|
|
|
val stateRoute = when (route) { |
|
|
|
|
AudioRoute.EARPIECE -> CallAudioState.ROUTE_EARPIECE |
|
|
|
|
AudioRoute.SPEAKER -> CallAudioState.ROUTE_SPEAKER |
|
|
|
|
AudioRoute.HEADSET -> CallAudioState.ROUTE_WIRED_OR_EARPIECE |
|
|
|
|
AudioRoute.HEADSET -> if ((callAudioState?.supportedRouteMask ?: 0) and CallAudioState.ROUTE_BLUETOOTH != 0) |
|
|
|
|
CallAudioState.ROUTE_BLUETOOTH else CallAudioState.ROUTE_WIRED_HEADSET |
|
|
|
|
} |
|
|
|
|
try { |
|
|
|
|
setAudioRoute(stateRoute) |
|
|
|
|
@ -123,9 +173,4 @@ class UtunConnection(
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun endpointTypeFor(route: AudioRoute): Int = when (route) { |
|
|
|
|
AudioRoute.EARPIECE -> CallEndpoint.TYPE_EARPIECE |
|
|
|
|
AudioRoute.SPEAKER -> CallEndpoint.TYPE_SPEAKER |
|
|
|
|
AudioRoute.HEADSET -> CallEndpoint.TYPE_WIRED_HEADSET |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|