Handle headset routing and cancel pending endpoint requests on shutdown

This commit is contained in:
evgeny committed 2026-10-15 03:16:03 +02:00
1 parent d944418676
commit 6765cf186d
3 files changed
+48 -12

No files matched your search

@@ -157,10 +157,10 @@ class CallAudioEngine {
capFramesRead = 0L capFramesRead = 0L
syncAecForRoute(router.route) syncAecForRoute(router.route)
wiredActive = router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null wiredActive = router.telecom == null && router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null
wantedWired = wiredActive wantedWired = wiredActive
router.onInputDevice = { _ -> router.onInputDevice = { _ ->
wantedWired = router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null wantedWired = router.telecom == null && router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null
} }
router.onOutputDevice = { dev -> applyTrackOutputDevice(dev) } router.onOutputDevice = { dev -> applyTrackOutputDevice(dev) }
@@ -89,17 +89,19 @@ class CallAudioRouter {
fun bindTelecom(connection: UtunConnection?) { fun bindTelecom(connection: UtunConnection?) {
if (telecom === connection) return if (telecom === connection) return
telecom?.onRouteChanged = null telecom?.onRouteChanged = null
telecom?.onEndpointsChanged = null
telecom = connection telecom = connection
currentOutputDevice = null; currentInputDevice = null
connection?.onRouteChanged = { actual -> connection?.onRouteChanged = { actual ->
if (started && telecom === connection) { if (started && telecom === connection) {
route = actual route = actual
onChanged?.invoke(route, headsetAvailable) onChanged?.invoke(route, headsetAvailable)
} }
} }
connection?.onEndpointsChanged = { if (started && telecom === connection) refresh() }
connection?.confirmedRoute?.let { route = it } connection?.confirmedRoute?.let { route = it }
LogManager.addLog("INFO", "CallAudioRouter", "routing owner=${if (connection != null) "telecom" else "audio-manager"}") LogManager.addLog("INFO", "CallAudioRouter", "routing owner=${if (connection != null) "telecom" else "audio-manager"}")
if (started) { if (started) {
currentOutputDevice = null; currentInputDevice = null
onOutputDevice?.invoke(null); onInputDevice?.invoke(null) onOutputDevice?.invoke(null); onInputDevice?.invoke(null)
applyRoute(desiredRoute) applyRoute(desiredRoute)
onChanged?.invoke(route, headsetAvailable) onChanged?.invoke(route, headsetAvailable)
@@ -147,7 +149,7 @@ class CallAudioRouter {
} }
fun stop() { fun stop() {
if (!started) return if (!started) { bindTelecom(null); return }
started = false started = false
deviceCallback?.let { am.unregisterAudioDeviceCallback(it) } deviceCallback?.let { am.unregisterAudioDeviceCallback(it) }
@@ -180,7 +182,7 @@ class CallAudioRouter {
desiredRoute = r desiredRoute = r
preferredRoute = r preferredRoute = r
applyRoute(r) applyRoute(r)
LogManager.addLog("INFO", "CallAudioRouter", "route -> $r (headsetAvailable=$headsetAvailable automatic=$automatic)") LogManager.addLog("INFO", "CallAudioRouter", "requested route=$r actual=$route headsetAvailable=$headsetAvailable automatic=$automatic")
if (telecom == null) onChanged?.invoke(route, headsetAvailable) if (telecom == null) onChanged?.invoke(route, headsetAvailable)
} }
@@ -210,7 +212,7 @@ class CallAudioRouter {
private fun applyRoute(r: AudioRoute) { private fun applyRoute(r: AudioRoute) {
telecom?.let { connection -> telecom?.let { connection ->
connection.routeTo(r) connection.routeTo(r)
LogManager.addLog("INFO", "CallAudioRouter", "route requested=$r confirmed=$route owner=telecom") LogManager.addLog("INFO", "CallAudioRouter", "route requested=$r confirmed=${connection.confirmedRoute} owner=telecom")
return return
} }
route = r route = r
@@ -366,8 +368,8 @@ class CallAudioRouter {
refreshInputDevices() refreshInputDevices()
} }
private fun isHeadsetConnected(): Boolean = private fun isHeadsetConnected(): Boolean = telecom?.headsetAvailable
findWiredDevice() != null || findBtScoDevice() != null || isBtHeadsetProfileConnected() ?: (findWiredDevice() != null || findBtScoDevice() != null || isBtHeadsetProfileConnected())
private fun findWiredDevice(): AudioDeviceInfo? = private fun findWiredDevice(): AudioDeviceInfo? =
(devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull { it.isSink && isWiredType(it.type) } (devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull { it.isSink && isWiredType(it.type) }
@@ -34,11 +34,18 @@ class UtunConnection(
/** Доступные CallEndpoint'ы (для requestCallEndpointChange), обновляются системой. */ /** Доступные CallEndpoint'ы (для requestCallEndpointChange), обновляются системой. */
private val availableEndpoints = CopyOnWriteArrayList<CallEndpoint>() private val availableEndpoints = CopyOnWriteArrayList<CallEndpoint>()
var onRouteChanged: ((AudioRoute) -> Unit)? = null var onRouteChanged: ((AudioRoute) -> Unit)? = null
var onEndpointsChanged: (() -> Unit)? = null
val headsetAvailable: Boolean? get() = if (Build.VERSION.SDK_INT >= 34) {
if (availableEndpoints.isEmpty()) null else availableEndpoints.any {
it.endpointType == CallEndpoint.TYPE_BLUETOOTH || it.endpointType == CallEndpoint.TYPE_WIRED_HEADSET
}
} else callAudioState?.let { it.supportedRouteMask and (CallAudioState.ROUTE_BLUETOOTH or CallAudioState.ROUTE_WIRED_HEADSET) != 0 }
var confirmedRoute: AudioRoute? = null var confirmedRoute: AudioRoute? = null
private set private set
private var wantedRoute: AudioRoute? = null private var wantedRoute: AudioRoute? = null
private var requestedEndpoint: android.os.ParcelUuid? = null private var requestedEndpoint: android.os.ParcelUuid? = null
private var requestSerial = 0L private var requestSerial = 0L
private var requestedLegacyRoute = 0
init { init {
setConnectionProperties(PROPERTY_SELF_MANAGED) setConnectionProperties(PROPERTY_SELF_MANAGED)
@@ -75,6 +82,8 @@ class UtunConnection(
} }
if (route != null) confirmRoute(route) if (route != null) confirmRoute(route)
else LogManager.addLog("WARN", "UtunConn", "unrecognized audio route=${audioState.route}") else LogManager.addLog("WARN", "UtunConn", "unrecognized audio route=${audioState.route}")
onEndpointsChanged?.invoke()
wantedRoute?.let { routeTo(it) }
} }
} }
@@ -92,7 +101,7 @@ class UtunConnection(
private fun confirmRoute(route: AudioRoute, endpoint: android.os.ParcelUuid? = null) { private fun confirmRoute(route: AudioRoute, endpoint: android.os.ParcelUuid? = null) {
confirmedRoute = route confirmedRoute = route
if (wantedRoute == route && (requestedEndpoint == null || requestedEndpoint == endpoint)) { if (wantedRoute == route && (requestedEndpoint == null || requestedEndpoint == endpoint)) {
wantedRoute = null; requestedEndpoint = null wantedRoute = null; requestedLegacyRoute = 0
} }
LogManager.addLog("INFO", "UtunConn", "route confirmed id=$callId route=$route pending=$wantedRoute") LogManager.addLog("INFO", "UtunConn", "route confirmed id=$callId route=$route pending=$wantedRoute")
onRouteChanged?.invoke(route) onRouteChanged?.invoke(route)
@@ -103,6 +112,7 @@ class UtunConnection(
this.availableEndpoints.addAll(availableEndpoints) this.availableEndpoints.addAll(availableEndpoints)
LogManager.addLog("DEBUG", "UtunConn", LogManager.addLog("DEBUG", "UtunConn",
"availableEndpoints -> %s".format(availableEndpoints.joinToString { "${it.endpointName}/${it.endpointType}" })) "availableEndpoints -> %s".format(availableEndpoints.joinToString { "${it.endpointName}/${it.endpointType}" }))
onEndpointsChanged?.invoke()
wantedRoute?.let { routeTo(it) } wantedRoute?.let { routeTo(it) }
} }
@@ -114,6 +124,8 @@ class UtunConnection(
/** Завершение с причиной (коды приложения: 0 — отбой, 2 — отклонено, 7 — ошибка). */ /** Завершение с причиной (коды приложения: 0 — отбой, 2 — отклонено, 7 — ошибка). */
fun markDisconnected(reason: Int) { fun markDisconnected(reason: Int) {
wantedRoute = null; requestedEndpoint = null; requestedLegacyRoute = 0; ++requestSerial
onRouteChanged = null; onEndpointsChanged = null
val code = when (reason) { val code = when (reason) {
2 -> DisconnectCause.REJECTED 2 -> DisconnectCause.REJECTED
7 -> DisconnectCause.ERROR 7 -> DisconnectCause.ERROR
@@ -126,8 +138,9 @@ class UtunConnection(
/** Маршрутизация аудио через Telecom (CallEndpoint API на Android 14+, иначе setAudioRoute). */ /** Маршрутизация аудио через Telecom (CallEndpoint API на Android 14+, иначе setAudioRoute). */
fun routeTo(route: AudioRoute) { fun routeTo(route: AudioRoute) {
wantedRoute = route wantedRoute = route
if (confirmedRoute == route && requestedEndpoint == null) { wantedRoute = null; return } if (confirmedRoute == route && requestedEndpoint == null && requestedLegacyRoute == 0) { wantedRoute = null; return }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (requestedEndpoint != null) return // До ответа сохраняем только последнее желание пользователя.
val ep = when (route) { val ep = when (route) {
AudioRoute.EARPIECE -> availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_EARPIECE } AudioRoute.EARPIECE -> availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_EARPIECE }
AudioRoute.SPEAKER -> availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_SPEAKER } AudioRoute.SPEAKER -> availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_SPEAKER }
@@ -143,10 +156,19 @@ class UtunConnection(
requestCallEndpointChange(ep, executor, object : OutcomeReceiver<Void, CallEndpointException> { requestCallEndpointChange(ep, executor, object : OutcomeReceiver<Void, CallEndpointException> {
override fun onResult(result: Void?) { override fun onResult(result: Void?) {
LogManager.addLog("INFO", "UtunConn", "endpoint change ok -> %d".format(ep.endpointType)) LogManager.addLog("INFO", "UtunConn", "endpoint change ok -> %d".format(ep.endpointType))
if (requestSerial == serial) {
requestedEndpoint = null
wantedRoute?.takeIf { it != route }?.let { routeTo(it) }
}
} }
override fun onError(error: CallEndpointException) { override fun onError(error: CallEndpointException) {
if (requestSerial == serial) { requestedEndpoint = null; wantedRoute = null }
LogManager.addLog("WARN", "UtunConn", "endpoint change failed: %s".format(error.message)) LogManager.addLog("WARN", "UtunConn", "endpoint change failed: %s".format(error.message))
if (requestSerial == serial) {
requestedEndpoint = null
val next = wantedRoute?.takeIf { it != route }
wantedRoute = null
next?.let { routeTo(it) }
}
} }
}) })
return return
@@ -159,16 +181,28 @@ class UtunConnection(
} }
return // На API 34+ один путь управления: CallEndpoint, без параллельного legacy-запроса. return // На API 34+ один путь управления: CallEndpoint, без параллельного legacy-запроса.
} }
val supported = callAudioState?.supportedRouteMask ?: run {
LogManager.addLog("WARN", "UtunConn", "route waiting for audio state id=$callId requested=$route")
return
}
val stateRoute = when (route) { val stateRoute = when (route) {
AudioRoute.EARPIECE -> CallAudioState.ROUTE_EARPIECE AudioRoute.EARPIECE -> CallAudioState.ROUTE_EARPIECE
AudioRoute.SPEAKER -> CallAudioState.ROUTE_SPEAKER AudioRoute.SPEAKER -> CallAudioState.ROUTE_SPEAKER
AudioRoute.HEADSET -> if ((callAudioState?.supportedRouteMask ?: 0) and CallAudioState.ROUTE_BLUETOOTH != 0) AudioRoute.HEADSET -> if (supported and CallAudioState.ROUTE_BLUETOOTH != 0)
CallAudioState.ROUTE_BLUETOOTH else CallAudioState.ROUTE_WIRED_HEADSET CallAudioState.ROUTE_BLUETOOTH else CallAudioState.ROUTE_WIRED_HEADSET
} }
if (supported and stateRoute == 0) {
wantedRoute = null
LogManager.addLog("WARN", "UtunConn", "route unavailable id=$callId requested=$route supported=$supported")
return
}
if (requestedLegacyRoute == stateRoute) return
requestedLegacyRoute = stateRoute
try { try {
setAudioRoute(stateRoute) setAudioRoute(stateRoute)
LogManager.addLog("INFO", "UtunConn", "audio route -> %d (route=%s)".format(stateRoute, route)) LogManager.addLog("INFO", "UtunConn", "audio route -> %d (route=%s)".format(stateRoute, route))
} catch (e: Exception) { } catch (e: Exception) {
requestedLegacyRoute = 0; wantedRoute = null
LogManager.addLog("WARN", "UtunConn", "setAudioRoute failed: %s".format(e.message)) LogManager.addLog("WARN", "UtunConn", "setAudioRoute failed: %s".format(e.message))
} }
} }