Honor Telecom incoming UI permission and report phone busy outcomes

This commit is contained in:
evgeny committed 2026-10-19 15:20:25 +02:00
1 parent 4cebd0be54
commit bd2dd056d6
3 files changed
+72 -6

No files matched your search

@@ -1,6 +1,10 @@
package com.utun.chat.data
import android.content.Intent
import android.app.NotificationManager
import android.widget.Toast
import androidx.core.app.NotificationCompat
import com.utun.chat.R
import android.os.Looper
import android.os.SystemClock
import com.utun.chat.ChatApplication
@@ -76,7 +80,7 @@ object CallController : TelecomCallManager.Listener {
val now = SystemClock.elapsedRealtime()
if (st.phase == CallPhase.ACTIVE) {
_state.value = st.copy(durationSeconds = ((now - st.startedAtElapsedRealtime) / 1000).toInt())
} else if (st.phase == CallPhase.INCOMING && now - incomingAt > 50_000) {
} else if (st.phase == CallPhase.INCOMING && now - incomingAt > 60_000) {
LogManager.addLog("WARN", "Call", "incoming timeout id=${st.callId}; declining")
end(st.callId)
}
@@ -238,6 +242,17 @@ object CallController : TelecomCallManager.Listener {
connectionType = CallConnectionType.NONE, connectionNextHop = 0)
syncCoreActivity()
LogManager.addLog("INFO", "Call", "finished id=$id reason=$reason; audio/service/proximity released")
if (reason == 8 && st.phase != CallPhase.INCOMING && st.phase != CallPhase.PENDING_INCOMING) {
val app = ChatApplication.instance
val text = AppLanguage.text(R.string.phone_busy_warning)
if (activityVisible) Toast.makeText(app, text, Toast.LENGTH_LONG).show()
else try {
app.getSystemService(NotificationManager::class.java).notify(1003,
NotificationCompat.Builder(app, IncomingCallManager.CHANNEL_CALL)
.setSmallIcon(android.R.drawable.ic_dialog_info).setContentTitle(st.peerName)
.setContentText(text).setAutoCancel(true).build())
} catch (e: SecurityException) { LogManager.addLog("WARN", "Call", "Busy outcome notification denied id=$id: $e") }
}
scope.launch {
delay(2500)
if (_state.value?.callId == id && _state.value?.phase == CallPhase.ENDED) _state.value = null
@@ -320,7 +335,7 @@ object CallController : TelecomCallManager.Listener {
return
}
if (type == 13) { reset(); return }
if (type !in 34..40 && type != 42 && type != 44) return
if (type !in 34..40 && type != 42 && type != 44 && type != 66) return
if (!coreReady || !NativeLib.isReady()) {
LogManager.addLog("WARN", "Call", "ignore event=$type while core stopped/restarting")
return
@@ -345,10 +360,12 @@ object CallController : TelecomCallManager.Listener {
// Однократное чтение на main завершается до reset; фоновый JNI-запрос не переживает ядро.
val name = ChatRepository().getMembers(channel).firstOrNull { it.nodeId == peer }?.name
?.takeIf { it.isNotEmpty() } ?: "0x%016x".format(peer)
_state.value = CallState(id, channel, peer, name, CallPhase.INCOMING)
_state.value = CallState(id, channel, peer, name,
if (TelecomCallManager.isReady()) CallPhase.PENDING_INCOMING else CallPhase.INCOMING)
syncCoreActivity()
IncomingCallManager.show(id, channel, peer)
TelecomCallManager.addIncomingCall(id, peer, name)
if (TelecomCallManager.isReady()) {
if (!TelecomCallManager.addIncomingCall(id, peer, name)) end(id, 7)
} else IncomingCallManager.show(id, channel, peer)
return
}
if (data == null || data.size < 8 || ((type == 37 || type == 38 || type == 40 || type == 42) && data.size < 9) ||
@@ -361,6 +378,12 @@ object CallController : TelecomCallManager.Listener {
return
}
when (type) {
66 -> if (st.phase == CallPhase.OUTGOING) {
CallRingback.stop()
_state.value = st.copy(phase = CallPhase.WAIT_DECISION)
service?.updateNotification()
LogManager.addLog("INFO", "Call", "Phone busy; waiting for caller choice id=$id")
}
35 -> if (st.phase == CallPhase.OUTGOING) {
_state.value = st.copy(phase = CallPhase.RINGING)
CallRingback.startRinging()
@@ -427,6 +450,9 @@ object CallController : TelecomCallManager.Listener {
}
override fun onAnswer(callId: Long) {
val pending = _state.value
if (pending?.callId == callId && pending.phase == CallPhase.PENDING_INCOMING)
_state.value = pending.copy(phase = CallPhase.INCOMING)
if (_state.value?.callId != callId || _state.value?.phase != CallPhase.INCOMING) return
if (activityVisible) { accept(); return }
// Ответ гарнитурой в фоне: сначала видимый UI, затем разрешённый запуск microphone FGS.
@@ -445,5 +471,29 @@ object CallController : TelecomCallManager.Listener {
override fun onReject(callId: Long) { end(callId, 2) }
override fun onDisconnect(callId: Long) { end(callId) }
override fun onIncomingUiAllowed(callId: Long) {
val st = _state.value ?: return
if (st.callId != callId || st.phase != CallPhase.PENDING_INCOMING) return
_state.value = st.copy(phase = CallPhase.INCOMING)
LogManager.addLog("INFO", "Call", "Telecom permits incoming UI id=$callId")
IncomingCallManager.show(callId, st.channelId, st.peerNodeId)
}
override fun onConnectionFailed(callId: Long) {
LogManager.addLog("WARN", "Call", "Telecom cannot handle call id=$callId")
end(callId, 7)
}
fun continueCall(id: Long) {
checkMain()
val st = _state.value ?: return
if (st.callId != id || st.phase != CallPhase.WAIT_DECISION) return
LogManager.addLog("INFO", "Call", "Caller overrides phone busy id=$id")
_state.value = st.copy(phase = CallPhase.OUTGOING)
NativeLib.callContinue(id)
CallRingback.startConnecting()
service?.updateNotification()
}
private fun checkMain() = check(Looper.myLooper() == Looper.getMainLooper()) { "CallController requires main thread" }
}
@@ -30,6 +30,8 @@ object TelecomCallManager {
fun onReject(callId: Long)
fun onDisconnect(callId: Long)
fun onConnectionAvailable(callId: Long, connection: UtunConnection)
fun onIncomingUiAllowed(callId: Long)
fun onConnectionFailed(callId: Long)
}
const val EXTRA_CALL_ID = "com.utun.chat.extra.CALL_ID"
@@ -134,7 +136,7 @@ object TelecomCallManager {
connections[callId] = conn
when (st.phase) {
CallPhase.ACTIVE -> conn.markActive()
CallPhase.INCOMING -> conn.markRinging()
CallPhase.INCOMING, CallPhase.PENDING_INCOMING -> conn.markRinging()
else -> conn.setDialing()
}
LogManager.addLog("INFO", TAG, "connection registered id=$callId phase=${st.phase}")
@@ -152,6 +154,8 @@ object TelecomCallManager {
fun notifyDisconnect(callId: Long) = post { listener?.onDisconnect(callId) }
fun notifyAbort(callId: Long) = post { listener?.onDisconnect(callId) }
fun notifyIncomingUiAllowed(callId: Long) = post { listener?.onIncomingUiAllowed(callId) }
fun notifyConnectionFailed(callId: Long) = post { listener?.onConnectionFailed(callId) }
/* ── управление состоянием из контроллера ── */
@@ -14,6 +14,18 @@ import android.telecom.PhoneAccountHandle
*/
class UtunConnectionService : ConnectionService() {
override fun onCreateIncomingConnectionFailed(account: PhoneAccountHandle?, request: ConnectionRequest?) {
val id = request?.extras?.getLong(TelecomCallManager.EXTRA_CALL_ID, 0) ?: 0
LogManager.addLog("WARN", "UtunConnSvc", "Telecom rejected incoming call id=$id")
TelecomCallManager.notifyConnectionFailed(id)
}
override fun onCreateOutgoingConnectionFailed(account: PhoneAccountHandle?, request: ConnectionRequest?) {
val id = request?.extras?.getLong(TelecomCallManager.EXTRA_CALL_ID, 0) ?: 0
LogManager.addLog("WARN", "UtunConnSvc", "Telecom rejected outgoing call id=$id")
TelecomCallManager.notifyConnectionFailed(id)
}
override fun onCreateIncomingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: ConnectionRequest?