Use Telecom as the sole route owner for Android calls
This commit is contained in:
1 parent
a439b222da
commit
9c4219d837
3 files changed
+126
-31
No files matched your search
@@ -84,6 +84,21 @@ class CallAudioEngine {
|
||||
|
||||
fun isActive(): Boolean = active
|
||||
|
||||
/** Позднее создание Telecom Connection: сначала снимаем собственную заявку на audio mode. */
|
||||
fun bindTelecom(connection: UtunConnection) {
|
||||
if (!active || router.telecom === connection) return
|
||||
if (router.telecom == null) {
|
||||
try {
|
||||
val am = ChatApplication.instance.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.mode = AudioManager.MODE_NORMAL
|
||||
if (Build.VERSION.SDK_INT >= 31) am.clearCommunicationDevice()
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudio", "release fallback routing failed id=$callId: ${e.message}")
|
||||
}
|
||||
}
|
||||
router.bindTelecom(connection)
|
||||
}
|
||||
|
||||
val diagnostics: String get() = synchronized(recordLock) {
|
||||
"active=$active ready=$captureReady/$playReady capture=$capFramesRead play=$playFramesWritten " +
|
||||
"workers=${captureThread?.isAlive}/${playThread?.isAlive} " +
|
||||
@@ -111,6 +126,7 @@ class CallAudioEngine {
|
||||
val nativeMs = SystemClock.elapsedRealtime() - startElapsedMs
|
||||
|
||||
callId = id
|
||||
router.bindTelecom(TelecomCallManager.connectionFor(id))
|
||||
muted = false
|
||||
failed.set(false)
|
||||
captureReady = false; playReady = false; duplexReady = false
|
||||
@@ -121,7 +137,7 @@ class CallAudioEngine {
|
||||
prevSpeaker = am.isSpeakerphoneOn
|
||||
var stageAt = SystemClock.elapsedRealtime()
|
||||
try {
|
||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
if (router.telecom == null) am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudio", "set audio mode failed: ${e.message}")
|
||||
}
|
||||
@@ -297,8 +313,9 @@ class CallAudioEngine {
|
||||
NativeLib.callAudioStop()
|
||||
router.onOutputDevice = null
|
||||
router.onInputDevice = null
|
||||
val restoreMode = router.telecom == null
|
||||
router.stop()
|
||||
restoreAudioMode()
|
||||
if (restoreMode) restoreAudioMode()
|
||||
callId = 0L
|
||||
muted = false
|
||||
LogManager.addLog("INFO", "CallAudio", "stopped id=$id capture=$capFramesRead play=$playFramesWritten; workers joined")
|
||||
@@ -649,12 +666,12 @@ class CallAudioEngine {
|
||||
private fun applyTrackOutputDevice(dev: AudioDeviceInfo?) {
|
||||
synchronized(trackLock) {
|
||||
val track = audioTrack ?: return
|
||||
if (!active || dev == null) return
|
||||
if (!active) return
|
||||
val startedAt = SystemClock.elapsedRealtime()
|
||||
try {
|
||||
val ok = track.setPreferredDevice(dev)
|
||||
LogManager.addLog(if (ok) "INFO" else "WARN", "CallAudio",
|
||||
"track preferred device type=${dev.type} accepted=$ok totalMs=${SystemClock.elapsedRealtime() - startedAt}")
|
||||
"track preferred device type=${dev?.type ?: "default"} accepted=$ok totalMs=${SystemClock.elapsedRealtime() - startedAt}")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudio", "setPreferredDevice failed: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -81,6 +81,30 @@ class CallAudioRouter {
|
||||
|
||||
private var preferWiredHeadset = false
|
||||
private var started = false
|
||||
private var desiredRoute = AudioRoute.SPEAKER
|
||||
var telecom: UtunConnection? = null
|
||||
private set
|
||||
|
||||
/** Для звонка Telecom — единственный владелец маршрута; рация продолжает использовать AudioManager. */
|
||||
fun bindTelecom(connection: UtunConnection?) {
|
||||
if (telecom === connection) return
|
||||
telecom?.onRouteChanged = null
|
||||
telecom = connection
|
||||
connection?.onRouteChanged = { actual ->
|
||||
if (started && telecom === connection) {
|
||||
route = actual
|
||||
onChanged?.invoke(route, headsetAvailable)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
private var deviceCallback: AudioDeviceCallback? = null
|
||||
private var btHeadset: BluetoothHeadset? = null
|
||||
private var btProfileProxyBound = false
|
||||
@@ -109,9 +133,9 @@ class CallAudioRouter {
|
||||
devices = am.getDevices(AudioManager.GET_DEVICES_ALL)
|
||||
val devicesMs = SystemClock.elapsedRealtime() - devicesAt
|
||||
hasEarpiece = findBuiltinEarpiece() != null
|
||||
route = if (autoHeadset) (if (isHeadsetConnected()) AudioRoute.HEADSET else fallbackRoute) else preferredRoute
|
||||
desiredRoute = if (autoHeadset) (if (isHeadsetConnected()) AudioRoute.HEADSET else fallbackRoute) else preferredRoute
|
||||
stageAt = SystemClock.elapsedRealtime()
|
||||
applyRoute(route)
|
||||
applyRoute(desiredRoute)
|
||||
val applyMs = SystemClock.elapsedRealtime() - stageAt
|
||||
stageAt = SystemClock.elapsedRealtime()
|
||||
refresh()
|
||||
@@ -131,13 +155,16 @@ class CallAudioRouter {
|
||||
unbindBtHeadsetProfile()
|
||||
|
||||
try {
|
||||
stopBluetoothSco()
|
||||
am.isBluetoothScoOn = false
|
||||
clearCommunicationDevice()
|
||||
if (telecom == null) {
|
||||
stopBluetoothSco()
|
||||
am.isBluetoothScoOn = false
|
||||
clearCommunicationDevice()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudioRouter", "stop cleanup failed: ${e.message}")
|
||||
}
|
||||
devices = null
|
||||
bindTelecom(null)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "stopped")
|
||||
}
|
||||
|
||||
@@ -149,12 +176,12 @@ class CallAudioRouter {
|
||||
if (!automatic) autoHeadset = false
|
||||
val preferenceChanged = preferWiredHeadset != preferWired
|
||||
preferWiredHeadset = preferWired
|
||||
if (route == r && !preferenceChanged) return
|
||||
route = r
|
||||
if (desiredRoute == r && route == r && !preferenceChanged) return
|
||||
desiredRoute = r
|
||||
preferredRoute = r
|
||||
applyRoute(r)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "route -> $r (headsetAvailable=$headsetAvailable automatic=$automatic)")
|
||||
onChanged?.invoke(route, headsetAvailable)
|
||||
if (telecom == null) onChanged?.invoke(route, headsetAvailable)
|
||||
}
|
||||
|
||||
/** Инжекция сохранённого источника аудио (из DataStore) перед стартом звонка. */
|
||||
@@ -181,6 +208,12 @@ class CallAudioRouter {
|
||||
|
||||
/* ── применение маршрута ── */
|
||||
private fun applyRoute(r: AudioRoute) {
|
||||
telecom?.let { connection ->
|
||||
connection.routeTo(r)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "route requested=$r confirmed=$route owner=telecom")
|
||||
return
|
||||
}
|
||||
route = r
|
||||
val startedAt = SystemClock.elapsedRealtime()
|
||||
try {
|
||||
var dev: AudioDeviceInfo? = null
|
||||
@@ -294,7 +327,7 @@ class CallAudioRouter {
|
||||
devices = am.getDevices(AudioManager.GET_DEVICES_ALL)
|
||||
/* если в режиме HEADSET появилось SCO-устройство — пере-применяем маршрут,
|
||||
* иначе SCO не замаршрутизируется автоматически */
|
||||
if (route == AudioRoute.HEADSET) applyRoute(route)
|
||||
if (telecom == null && route == AudioRoute.HEADSET) applyRoute(route)
|
||||
refresh()
|
||||
}
|
||||
|
||||
@@ -308,22 +341,22 @@ class CallAudioRouter {
|
||||
/* авторежим: маршрут следует за гарнитурой */
|
||||
if (autoHeadset) {
|
||||
val want = if (now) AudioRoute.HEADSET else fallbackRoute
|
||||
if (route != want) {
|
||||
route = want
|
||||
applyRoute(route)
|
||||
if (desiredRoute != want) {
|
||||
desiredRoute = want
|
||||
applyRoute(want)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "auto route -> $want")
|
||||
}
|
||||
} else {
|
||||
/* гарнитуру отключили — откатываемся на fallback-маршрут */
|
||||
if (!now && route == AudioRoute.HEADSET) {
|
||||
route = fallbackRoute
|
||||
applyRoute(route)
|
||||
if (!now && desiredRoute == AudioRoute.HEADSET) {
|
||||
desiredRoute = fallbackRoute
|
||||
applyRoute(desiredRoute)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "headset lost, fallback route -> $fallbackRoute")
|
||||
}
|
||||
/* гарнитура появилась, а пользователь ранее выбрал её — восстанавливаем */
|
||||
if (now && !prev && preferredRoute == AudioRoute.HEADSET && route != AudioRoute.HEADSET) {
|
||||
route = AudioRoute.HEADSET
|
||||
applyRoute(route)
|
||||
if (now && !prev && preferredRoute == AudioRoute.HEADSET && desiredRoute != AudioRoute.HEADSET) {
|
||||
desiredRoute = AudioRoute.HEADSET
|
||||
applyRoute(desiredRoute)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "headset available, restore preferred route -> HEADSET")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user