diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt index 881dcc51..d9b7fac7 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt @@ -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" } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/TelecomCallManager.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/TelecomCallManager.kt index 2915ad9a..3eb7de19 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/TelecomCallManager.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/TelecomCallManager.kt @@ -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) } /* ── управление состоянием из контроллера ── */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnectionService.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnectionService.kt index 3be3f348..cb4b71b1 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnectionService.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnectionService.kt @@ -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?