diff --git a/tools/chatgui-android/call-service-tests/src/test/kotlin/com/utun/chat/data/CallServiceLifecycleTest.kt b/tools/chatgui-android/call-service-tests/src/test/kotlin/com/utun/chat/data/CallServiceLifecycleTest.kt index 4f248d98..86a41b54 100644 --- a/tools/chatgui-android/call-service-tests/src/test/kotlin/com/utun/chat/data/CallServiceLifecycleTest.kt +++ b/tools/chatgui-android/call-service-tests/src/test/kotlin/com/utun/chat/data/CallServiceLifecycleTest.kt @@ -45,6 +45,7 @@ class CallServiceLifecycleTest { AppLanguage.failText = false; AppLanguage.language = "en"; NotificationCompat.failBuild = false Handler.rejectImmediate = false; Handler.rejectDelayed = false; PendingIntent.reset() MediaAudioController.beforeIdle = {} + AudioRecorderManager.stopped = java.util.concurrent.CompletableFuture.completedFuture(Unit) TelecomCallManager.pending = false CallVideoEngine.failCreate = false; CallVideoEngine.created.clear() CallController.reset(); RadioAudioEngine.instance().activeGroup.value = null; RadioAudioEngine.instance().restored.clear() @@ -53,6 +54,7 @@ class CallServiceLifecycleTest { } @After fun release() { + AudioRecorderManager.stopped.complete(Unit) gates.forEach { it.countDown() }; MediaAudioController.beforeIdle = {} TelecomCallManager.pending = false NativeLib.failActivityRequest = false @@ -486,4 +488,16 @@ class CallServiceLifecycleTest { assertEquals(CallPhase.ENDED, state?.phase); assertEquals(7, state?.reason) diagnostic("service request") } + @Test fun voiceCleanupBlocksSignallingAndRemainsABarrierAfterCallCancellation() { + val voice = java.util.concurrent.CompletableFuture() + AudioRecorderManager.stopped = voice + request(); dispatcher.scheduler.runCurrent() + assertTrue(state?.preparing == true) + assertFalse(NativeLib.callCommands.any { it.first == "start" }) + CallController.reset() + assertTrue(CallController.blocksMedia()); assertFalse(voice.isCancelled) + voice.complete(Unit) + await("voice release after cancelled call") { !CallController.blocksMedia() } + assertFalse(NativeLib.callCommands.any { it.first == "start" }) + } } diff --git a/tools/chatgui-android/doc/ARCHITECTURE.md b/tools/chatgui-android/doc/ARCHITECTURE.md index bfe23b08..20473a8a 100644 --- a/tools/chatgui-android/doc/ARCHITECTURE.md +++ b/tools/chatgui-android/doc/ARCHITECTURE.md @@ -692,10 +692,26 @@ true/false — ответ ОС, null — ошибка чтения сервис Android HeadlessService — foreground service с уведомлением и общим native-ядром, а не Linux control-loop. Он запускает NetworkChangeMonitor и NetworkActivityMonitor. +Foreground подтверждается до запуска native; отказ завершает ожидание startup ошибкой. +Уведомления и независимые части onDestroy логируют ошибки отдельно. CoreShutdown +переживает Service и повторяет отказ stop с backoff 250 мс..30 с; новый init ждёт успешный stop. Переход UI в фон передаёт client_activity в uasync. Standby чередует ACTIVE/SLEEP; сам event loop продолжает работать. Ожидания SLEEP будятся после перехода в ACTIVE, сетевой activity может разбудить после min_sleep. Контракт отмены: `libutun_lite/standby.h`. +AppEventHandler передаёт каждый native event одним main-заданием и независимо вызывает +обработчики владельцев. Сбой одного обработчика не отменяет остальные и доставку ViewModel. +SharedFlow не повторяет старые команды/ACK новой ViewModel; начальный снимок читается +при DB_READY или проверке уже готового ядра. Emit при заполнении буфера ждёт на main coroutine, +без блокировки native-потока. Отказ main-поста и ошибка обработки конкретного события логируются. + +AudioRecorderManager сохраняется в process registry с начала захвата либо playback до release ACK. +ViewModel.onCleared запрашивает асинхронную очистку, рабочий поток переживает UI. +Native cancel и Android release повторяются независимо; неудачный AudioTrack/decoder close +остаётся у playback worker также после completion callback. Capture cleanup не держит playback lock. +NativeLib запрещает новый voice capture во время core teardown/звонка; stop ядра и подготовка +звонка ждут освобождения этих владельцев до доступа к native recorder/звонковому I/O. + DEBUG_* фильтруются общими уровнями/категориями. Bridge переводит их в logcat и, при включённой подписке, Kotlin LogManager. Debug hook также пишет в UDP-буфер: 16KiB threshold либо timer 200мс, неблокирующая отправка без повторов. ERROR вызывает flush.