Handle headset routing and cancel pending endpoint requests on shutdown
This commit is contained in:
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user