Browse Source

Handle headset routing and cancel pending endpoint requests on shutdown

master
evgeny 4 days ago
parent
commit
6765cf186d
  1. 4
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt
  2. 14
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioRouter.kt
  3. 42
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnection.kt

4
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioEngine.kt

@ -157,10 +157,10 @@ class CallAudioEngine {
capFramesRead = 0L
syncAecForRoute(router.route)
wiredActive = router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null
wiredActive = router.telecom == null && router.route == AudioRoute.HEADSET && router.wiredInputDevice() != null
wantedWired = wiredActive
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) }

14
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioRouter.kt

@ -89,17 +89,19 @@ class CallAudioRouter {
fun bindTelecom(connection: UtunConnection?) {
if (telecom === connection) return
telecom?.onRouteChanged = null
telecom?.onEndpointsChanged = null
telecom = connection
currentOutputDevice = null; currentInputDevice = null
connection?.onRouteChanged = { actual ->
if (started && telecom === connection) {
route = actual
onChanged?.invoke(route, headsetAvailable)
}
}
connection?.onEndpointsChanged = { if (started && telecom === connection) refresh() }
connection?.confirmedRoute?.let { route = it }
LogManager.addLog("INFO", "CallAudioRouter", "routing owner=${if (connection != null) "telecom" else "audio-manager"}")
if (started) {
currentOutputDevice = null; currentInputDevice = null
onOutputDevice?.invoke(null); onInputDevice?.invoke(null)
applyRoute(desiredRoute)
onChanged?.invoke(route, headsetAvailable)
@ -147,7 +149,7 @@ class CallAudioRouter {
}
fun stop() {
if (!started) return
if (!started) { bindTelecom(null); return }
started = false
deviceCallback?.let { am.unregisterAudioDeviceCallback(it) }
@ -180,7 +182,7 @@ class CallAudioRouter {
desiredRoute = r
preferredRoute = 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)
}
@ -210,7 +212,7 @@ class CallAudioRouter {
private fun applyRoute(r: AudioRoute) {
telecom?.let { connection ->
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
}
route = r
@ -366,8 +368,8 @@ class CallAudioRouter {
refreshInputDevices()
}
private fun isHeadsetConnected(): Boolean =
findWiredDevice() != null || findBtScoDevice() != null || isBtHeadsetProfileConnected()
private fun isHeadsetConnected(): Boolean = telecom?.headsetAvailable
?: (findWiredDevice() != null || findBtScoDevice() != null || isBtHeadsetProfileConnected())
private fun findWiredDevice(): AudioDeviceInfo? =
(devices ?: am.getDevices(AudioManager.GET_DEVICES_ALL)).firstOrNull { it.isSink && isWiredType(it.type) }

42
tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnection.kt

@ -34,11 +34,18 @@ class UtunConnection(
/** Доступные CallEndpoint'ы (для requestCallEndpointChange), обновляются системой. */
private val availableEndpoints = CopyOnWriteArrayList<CallEndpoint>()
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
private set
private var wantedRoute: AudioRoute? = null
private var requestedEndpoint: android.os.ParcelUuid? = null
private var requestSerial = 0L
private var requestedLegacyRoute = 0
init {
setConnectionProperties(PROPERTY_SELF_MANAGED)
@ -75,6 +82,8 @@ class UtunConnection(
}
if (route != null) confirmRoute(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) {
confirmedRoute = route
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")
onRouteChanged?.invoke(route)
@ -103,6 +112,7 @@ class UtunConnection(
this.availableEndpoints.addAll(availableEndpoints)
LogManager.addLog("DEBUG", "UtunConn",
"availableEndpoints -> %s".format(availableEndpoints.joinToString { "${it.endpointName}/${it.endpointType}" }))
onEndpointsChanged?.invoke()
wantedRoute?.let { routeTo(it) }
}
@ -114,6 +124,8 @@ class UtunConnection(
/** Завершение с причиной (коды приложения: 0 — отбой, 2 — отклонено, 7 — ошибка). */
fun markDisconnected(reason: Int) {
wantedRoute = null; requestedEndpoint = null; requestedLegacyRoute = 0; ++requestSerial
onRouteChanged = null; onEndpointsChanged = null
val code = when (reason) {
2 -> DisconnectCause.REJECTED
7 -> DisconnectCause.ERROR
@ -126,8 +138,9 @@ class UtunConnection(
/** Маршрутизация аудио через Telecom (CallEndpoint API на Android 14+, иначе setAudioRoute). */
fun routeTo(route: AudioRoute) {
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 (requestedEndpoint != null) return // До ответа сохраняем только последнее желание пользователя.
val ep = when (route) {
AudioRoute.EARPIECE -> availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_EARPIECE }
AudioRoute.SPEAKER -> availableEndpoints.firstOrNull { it.endpointType == CallEndpoint.TYPE_SPEAKER }
@ -143,10 +156,19 @@ class UtunConnection(
requestCallEndpointChange(ep, executor, object : OutcomeReceiver<Void, CallEndpointException> {
override fun onResult(result: Void?) {
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) {
if (requestSerial == serial) { requestedEndpoint = null; wantedRoute = null }
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
@ -159,16 +181,28 @@ class UtunConnection(
}
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) {
AudioRoute.EARPIECE -> CallAudioState.ROUTE_EARPIECE
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
}
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 {
setAudioRoute(stateRoute)
LogManager.addLog("INFO", "UtunConn", "audio route -> %d (route=%s)".format(stateRoute, route))
} catch (e: Exception) {
requestedLegacyRoute = 0; wantedRoute = null
LogManager.addLog("WARN", "UtunConn", "setAudioRoute failed: %s".format(e.message))
}
}

Loading…
Cancel
Save