Keep cancelled media preparation inside the native teardown barrier

This commit is contained in:
evgeny committed 2026-10-29 13:34:23 +01:00
1 parent ca7b23909b
commit aefca81444
3 files changed
+18 -1

No files matched your search

@@ -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"
}
}
}
@@ -105,5 +105,5 @@ class RadioAudioEngine {
data class HostMediaState(val ongoing: Boolean)
object StreamController { val state = MutableStateFlow<HostMediaState?>(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 }
@@ -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<CallService>()
private val gates = mutableListOf<CountDownLatch>()
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() }
}
}