Retain retiring Telecom connections and reject duplicate registrations

This commit is contained in:
evgeny committed 2026-10-29 13:34:25 +01:00
1 parent c136ff60c2
commit 01a84c6851
2 files changed
+87 -32

No files matched your search

@@ -20,8 +20,8 @@ import java.util.concurrent.ConcurrentHashMap
* - Хранит Connection по callId и прокидывает state/route из контроллера в Connection.
* - Обратные события (answer/reject/disconnect из системного ввода) — через Listener в контроллер.
*
* Всё «best effort»: если Telecom недоступен (нет прав/стека), методы возвращают false,
* и контроллер продолжает звонок без системной маршрутизации.
* init/изменения реестра — main, lookup — также workers. Отклонённые/удалённые connections сохраняются
* до успешного destroy. Отсутствие Telecom возвращает false из запросов; решение о fallback/завершении принимает controller.
*/
object TelecomCallManager {
@@ -44,6 +44,13 @@ object TelecomCallManager {
@Volatile private var listener: Listener? = null
private val connections = ConcurrentHashMap<Long, UtunConnection>()
private data class Closing(val id: Long, val reason: Int, var disconnected: Boolean = false)
private val closing = linkedMapOf<UtunConnection, Closing>() // Main: сильные refs до ACK destroy, включая late/duplicate connections.
private val reasons = mutableMapOf<UtunConnection, Int>()
private val disconnected = mutableSetOf<UtunConnection>()
private var cleanupQueued = false
private var cleanupDelay = 250L
private val cleanupRetry = Runnable { cleanupQueued = false; cleanupConnections() }
@Volatile private var phoneAccountHandle: PhoneAccountHandle? = null
@Volatile private var ready = false
@@ -55,15 +62,10 @@ object TelecomCallManager {
/** Инициализация из ChatApplication.onCreate: регистрирует PhoneAccount. Идемпотентно. */
fun init() {
if (ready) return
val ctx = ChatApplication.instance
val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager ?: run {
LogManager.addLog("ERROR", TAG, "TelecomManager unavailable")
return
}
val component = ComponentName(ctx, UtunConnectionService::class.java)
val handle = PhoneAccountHandle(component, PHONE_ACCOUNT_ID)
phoneAccountHandle = handle
try {
val ctx = ChatApplication.instance
val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager ?: error("TelecomManager unavailable")
val handle = PhoneAccountHandle(ComponentName(ctx, UtunConnectionService::class.java), PHONE_ACCOUNT_ID)
/* registerPhoneAccount идемпотентен (перезапись по handle) и для self-managed
* аккаунта требует только MANAGE_OWN_CALLS. getPhoneAccount() здесь не зовём —
* он на Android 14+ требует READ_PHONE_NUMBERS, которого у нас нет. */
@@ -71,6 +73,7 @@ object TelecomCallManager {
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
.build()
tm.registerPhoneAccount(account)
phoneAccountHandle = handle
LogManager.addLog("INFO", TAG, "phone account registered")
ready = true
} catch (e: Exception) {
@@ -125,22 +128,32 @@ object TelecomCallManager {
/* ── реестр Connection (вызывается из UtunConnectionService) ── */
fun registerConnection(callId: Long, conn: UtunConnection) {
fun registerConnection(callId: Long, conn: UtunConnection): Boolean {
val st = CallController.state.value
if (st == null || st.callId != callId || !st.ongoing) {
LogManager.addLog("WARN", TAG, "late connection for ended/stale call id=$callId current=${st?.callId}")
conn.markDisconnected(0)
conn.destroy()
return
retire(callId, conn, 0); return false
}
connections[callId] = conn
when (st.phase) {
CallPhase.ACTIVE -> conn.markActive()
CallPhase.INCOMING, CallPhase.PENDING_INCOMING -> conn.markRinging()
else -> conn.setDialing()
val previous = connections.putIfAbsent(callId, conn)
if (previous != null) {
if (previous === conn) return true
LogManager.addLog("WARN", TAG, "duplicate connection rejected id=$callId; original retained")
retire(callId, conn, 0); return false
}
try {
when (st.phase) {
CallPhase.ACTIVE -> conn.markActive()
CallPhase.INCOMING, CallPhase.PENDING_INCOMING -> conn.markRinging()
else -> conn.setDialing()
}
LogManager.addLog("INFO", TAG, "connection registered id=$callId phase=${st.phase}")
listener?.onConnectionAvailable(callId, conn)
return true
} catch (e: Exception) {
LogManager.addLog("ERROR", TAG, "connection registration failed id=$callId; retiring owner: $e")
connections.remove(callId, conn); retire(callId, conn, 7); notifyConnectionFailed(callId)
return false
}
LogManager.addLog("INFO", TAG, "connection registered id=$callId phase=${st.phase}")
listener?.onConnectionAvailable(callId, conn)
}
fun connectionFor(callId: Long): UtunConnection? = connections[callId]
@@ -159,23 +172,57 @@ object TelecomCallManager {
/* ── управление состоянием из контроллера ── */
fun setRinging(callId: Long) = connections[callId]?.markRinging()
fun setRinging(callId: Long) {
try { connections[callId]?.markRinging() } catch (e: Exception) { LogManager.addLog("ERROR", TAG, "ringing state failed id=$callId: $e") }
}
fun setActive(callId: Long) = connections[callId]?.markActive()
fun setActive(callId: Long) {
try { connections[callId]?.markActive() } catch (e: Exception) { LogManager.addLog("ERROR", TAG, "active state failed id=$callId: $e") }
}
fun setDisconnected(callId: Long, reason: Int) = connections[callId]?.markDisconnected(reason)
fun setDisconnected(callId: Long, reason: Int) {
val conn = connections[callId] ?: return
reasons[conn] = reason
try { conn.markDisconnected(reason); disconnected.add(conn) } catch (e: Exception) {
LogManager.addLog("ERROR", TAG, "disconnected state failed id=$callId; owner retained until removal: $e")
}
}
fun removeConnection(callId: Long) {
connections.remove(callId)?.destroy()
connections.remove(callId)?.let { retire(callId, it, reasons[it] ?: 0) }
LogManager.addLog("DEBUG", TAG, "connection removed id=0x%016x".format(callId))
}
private fun retire(callId: Long, conn: UtunConnection, reason: Int) {
closing.putIfAbsent(conn, Closing(callId, reason, conn in disconnected))
reasons.remove(conn); disconnected.remove(conn); cleanupConnections()
}
private fun cleanupConnections() {
for ((conn, owner) in closing.toMap()) {
if (!owner.disconnected) try { conn.markDisconnected(owner.reason); owner.disconnected = true } catch (e: Exception) {
LogManager.addLog("ERROR", TAG, "retiring disconnect failed id=${owner.id}; attempting destroy independently: $e")
}
try { conn.destroy(); closing.remove(conn) } catch (e: Exception) {
LogManager.addLog("ERROR", TAG, "connection destroy failed id=${owner.id}; retained: $e")
}
}
if (closing.isEmpty()) { mainHandler.removeCallbacks(cleanupRetry); cleanupQueued = false; cleanupDelay = 250; return }
if (!cleanupQueued) {
cleanupQueued = mainHandler.postDelayed(cleanupRetry, cleanupDelay)
if (!cleanupQueued) LogManager.addLog("ERROR", TAG, "connection cleanup retry rejected count=${closing.size}; owners retained")
else { LogManager.addLog("DEBUG", TAG, "connection cleanup scheduled count=${closing.size} delayMs=$cleanupDelay"); cleanupDelay = (cleanupDelay * 2).coerceAtMost(30_000) }
}
}
/* ── helpers ── */
private fun telecomManager(): TelecomManager? {
if (!ready) return null
if (!ready) { LogManager.addLog("WARN", TAG, "Telecom request before account registration"); return null }
return try {
ChatApplication.instance.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager
(ChatApplication.instance.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager).also {
if (it == null) LogManager.addLog("ERROR", TAG, "TelecomManager unavailable after account registration")
}
} catch (e: Exception) {
LogManager.addLog("WARN", TAG, "getTelecomManager failed: %s".format(e.message))
null
@@ -183,6 +230,8 @@ object TelecomCallManager {
}
private fun post(r: () -> Unit) {
mainHandler.post(r)
if (!mainHandler.post {
try { r() } catch (e: Exception) { LogManager.addLog("ERROR", TAG, "Telecom listener callback failed: $e") }
}) LogManager.addLog("ERROR", TAG, "Telecom callback post rejected")
}
}
@@ -4,6 +4,7 @@ import android.telecom.Connection
import android.telecom.ConnectionRequest
import android.telecom.ConnectionService
import android.telecom.PhoneAccountHandle
import android.telecom.DisconnectCause
/**
* Self-managed ConnectionService для P2P-звонков uTun.
@@ -52,10 +53,15 @@ class UtunConnectionService : ConnectionService() {
return createConnection(callId, peerNodeId, name)
}
private fun createConnection(callId: Long, peerNodeId: Long, displayName: String): UtunConnection {
val conn = UtunConnection(callId, peerNodeId, displayName)
TelecomCallManager.registerConnection(callId, conn)
return conn
private fun createConnection(callId: Long, peerNodeId: Long, displayName: String): Connection {
return try {
val conn = UtunConnection(callId, peerNodeId, displayName)
if (TelecomCallManager.registerConnection(callId, conn)) conn else Connection.createCanceledConnection()
} catch (e: Exception) {
LogManager.addLog("ERROR", "UtunConnSvc", "connection creation failed id=$callId peer=$peerNodeId: $e")
TelecomCallManager.notifyConnectionFailed(callId)
Connection.createFailedConnection(DisconnectCause(DisconnectCause.ERROR))
}
}
private fun parseRequest(request: ConnectionRequest?): Triple<Long, Long, String>? {