diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index 53e3a73d..738c478e 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -73,6 +73,10 @@ object NativeLib { RadioCallController.reset() StreamController.reset(); CallController.reset(); ChatApplication.instance.avatars.stop(); ChatApplication.instance.memberFiles.cancel() CallService.requireReleased() // Не останавливать native при неподтверждённой очистке его media-владельца. + check(!CallController.blocksMedia()) { + LogManager.addLog("ERROR", "Call", "Core teardown blocked: call media preparation still running") + "Call media preparation still running" + } } } diff --git a/tools/chatgui-android/call-service-tests/src/main/kotlin/com/utun/chat/data/CallServiceHost.kt b/tools/chatgui-android/call-service-tests/src/main/kotlin/com/utun/chat/data/CallServiceHost.kt index 7031bbd8..d0f00ec7 100644 --- a/tools/chatgui-android/call-service-tests/src/main/kotlin/com/utun/chat/data/CallServiceHost.kt +++ b/tools/chatgui-android/call-service-tests/src/main/kotlin/com/utun/chat/data/CallServiceHost.kt @@ -105,5 +105,5 @@ class RadioAudioEngine { data class HostMediaState(val ongoing: Boolean) object StreamController { val state = MutableStateFlow(null); fun stopForCall() { state.value = null } } object StreamPlayerEngine { val viewing = MutableStateFlow(false); fun stopForCall() { viewing.value = false } } -object MediaAudioController { fun awaitIdle() {} } +object MediaAudioController { var beforeIdle: () -> Unit = {}; fun awaitIdle() { beforeIdle() } } enum class MediaAudioProfile { AUTO, PHONE, WIRED, BLUETOOTH } 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 9c6d0573..c0df98fe 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 @@ -13,6 +13,7 @@ import com.utun.chat.ChatApplication import java.nio.ByteBuffer import java.nio.ByteOrder import java.util.concurrent.TimeUnit +import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicReference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -30,6 +31,7 @@ class CallServiceLifecycleTest { private lateinit var context: Context private val dispatcher = StandardTestDispatcher() private val services = mutableListOf() + private val gates = mutableListOf() private val main get() = Looper.getMainLooper() private val power get() = requireNotNull(context.powerManager) private val manager get() = requireNotNull(context.notificationManager) @@ -41,6 +43,7 @@ class CallServiceLifecycleTest { NativeLib.ready = true; NativeLib.failActivityRequest = false; NativeLib.acceptActivityRequest = true AppLanguage.failText = false; AppLanguage.language = "en"; NotificationCompat.failBuild = false Handler.rejectImmediate = false; Handler.rejectDelayed = false; PendingIntent.reset() + MediaAudioController.beforeIdle = {} CallVideoEngine.failCreate = false; CallVideoEngine.created.clear() CallController.reset(); RadioAudioEngine.instance().activeGroup.value = null; RadioAudioEngine.instance().restored.clear() NativeLib.callCommands.clear(); LogManager.entries.clear(); LogManager.console = true @@ -48,6 +51,7 @@ class CallServiceLifecycleTest { } @After fun release() { + gates.forEach { it.countDown() }; MediaAudioController.beforeIdle = {} NativeLib.failActivityRequest = false Handler.rejectImmediate = false; Handler.rejectDelayed = false AppLanguage.failText = false; NotificationCompat.failBuild = false; CallVideoEngine.failCreate = false @@ -335,4 +339,13 @@ class CallServiceLifecycleTest { assertEquals(CallPhase.ENDED, state?.phase); assertTrue(service.stops > 0) assertFalse(manager.active.containsKey(1002)); diagnostic("start failed operation=wake lock") } + + @Test fun aCancelledCallStillBlocksCoreTeardownUntilMediaPreparationLeavesNative() { + val entered = CountDownLatch(1); val gate = CountDownLatch(1).also { gates.add(it) } + MediaAudioController.beforeIdle = { entered.countDown(); check(gate.await(5, TimeUnit.SECONDS)) } + request(); dispatcher.scheduler.runCurrent(); assertTrue(entered.await(5, TimeUnit.SECONDS)) + CallController.reset() + assertNull(state); assertFalse(CallService.cleanupPending()); assertTrue(CallController.blocksMedia()) + gate.countDown(); await("cancelled media preparation finished") { !CallController.blocksMedia() } + } }