Retain retiring Telecom connections and reject duplicate registrations
This commit is contained in:
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")
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -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>? {
|
||||
|
||||
Reference in new issue
Block a user