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 63d52cb9..5902ef84 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
@@ -32,6 +32,8 @@ data class OutgoingCallRequest(val channelId: String, val peerNodeId: Long, val
* Единственный владелец звонковой сессии в процессе приложения. Все операции — main thread.
* Сервис владеет аудио и wake lock, ядро — сигналингом, ViewModel только читает StateFlow.
* События поступают напрямую из AppEventHandler без replay старых звонков новым подписчикам.
+ * Start/accept ждут освобождения чужих media и успешного enqueue; hangup/decline при отказе очереди
+ * повторяются независимо от UI/audio cleanup. Core reset отменяет такие повторы до остановки worker.
*/
object CallController : TelecomCallManager.Listener {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
@@ -229,8 +231,9 @@ object CallController : TelecomCallManager.Listener {
throw e
} catch (e: Exception) {
LogManager.addLog("ERROR", "Call", "media release failed id=$id: $e")
- Toast.makeText(ChatApplication.instance, R.string.call_media_stop_failed, Toast.LENGTH_LONG).show()
end(id, 7)
+ try { Toast.makeText(ChatApplication.instance, R.string.call_media_stop_failed, Toast.LENGTH_LONG).show() }
+ catch (notify: Exception) { LogManager.addLog("ERROR", "Call", "Cannot show media release failure id=$id: $notify") }
}
}
}
@@ -382,17 +385,17 @@ object CallController : TelecomCallManager.Listener {
radioToRestore = null
RadioAudioEngine.instance().restoreAfterCall(group)
}
- if (reason == 8 && st.phase != CallPhase.INCOMING && st.phase != CallPhase.PENDING_INCOMING) {
+ if (reason == 8 && st.phase != CallPhase.INCOMING && st.phase != CallPhase.PENDING_INCOMING) try {
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 {
+ else {
requireNotNull(app.getSystemService(NotificationManager::class.java)) { "notification manager unavailable" }.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: Exception) { LogManager.addLog("ERROR", "Call", "Busy outcome notification failed id=$id: $e") }
- }
+ }
+ } catch (e: Exception) { LogManager.addLog("ERROR", "Call", "Busy outcome notification failed id=$id: $e") }
scope.launch {
delay(2500)
if (_state.value?.callId == id && _state.value?.phase == CallPhase.ENDED) _state.value = null
@@ -429,10 +432,21 @@ object CallController : TelecomCallManager.Listener {
fun setRoute(route: AudioRoute) {
checkMain()
+ if (_state.value?.phase != CallPhase.ACTIVE || service == null) {
+ LogManager.addLog("WARN", "Call", "route ignored outside active call route=$route phase=${_state.value?.phase}")
+ return
+ }
videoSpeakerPending = false // Явный выбор пользователя отменяет ожидающее автоматическое переключение.
service?.audio?.router?.setRoute(route)
syncProximity()
- scope.launch { ChatApplication.instance.configProvider.setCallAudioRoute(route) }
+ scope.launch {
+ try { ChatApplication.instance.configProvider.setCallAudioRoute(route) }
+ catch (e: CancellationException) { throw e }
+ catch (e: Exception) {
+ LogManager.addLog("ERROR", "Call", "Cannot save preferred route=$route: $e")
+ Toast.makeText(ChatApplication.instance, R.string.call_route_save_failed, Toast.LENGTH_LONG).show()
+ }
+ }
}
private fun routeChanged(route: AudioRoute, headset: Boolean) {
@@ -466,7 +480,7 @@ object CallController : TelecomCallManager.Listener {
audio.router.fallbackRoute = AudioRoute.SPEAKER
audio.router.setRoute(AudioRoute.SPEAKER, automatic = true)
syncProximity()
- LogManager.addLog("INFO", "call", "video speaker enabled after confirmed FAR id=${st.callId}")
+ LogManager.addLog("INFO", "call", "video speaker requested after confirmed FAR id=${st.callId}")
}
/** Только живые события ядра, сериализованные AppEventHandler на main thread. */
diff --git a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml
index d2557208..89b75aaa 100644
--- a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml
+++ b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml
@@ -33,6 +33,7 @@
При принятии звонка рация выключится, трансляция или её просмотр завершится. После звонка рация включится снова.
Освобождаем аудиоустройства…
Не удалось освободить аудиоустройства для звонка.
+ Не удалось сохранить предпочтительное аудиоустройство звонков.
Во время звонка звук рации недоступен.
Настройки видео
Не удалось включить передачу видео
diff --git a/tools/chatgui-android/app/src/main/res/values/strings.xml b/tools/chatgui-android/app/src/main/res/values/strings.xml
index b62b3774..b9762f44 100644
--- a/tools/chatgui-android/app/src/main/res/values/strings.xml
+++ b/tools/chatgui-android/app/src/main/res/values/strings.xml
@@ -33,6 +33,7 @@
Answering will turn off the radio and end the broadcast or stream playback. The radio will turn back on after the call.
Releasing audio devices…
Could not release the audio devices for the call.
+ Could not save the preferred call audio device.
Radio audio is unavailable during a call.
Video settings
Unable to send video