Reject fresh radio and publisher owners during core cleanup

This commit is contained in:
evgeny committed 2026-10-29 13:35:42 +01:00
1 parent 044ff034a6
commit 0568d263e0
3 files changed
+24 -5

No files matched your search

@@ -59,7 +59,7 @@ object StreamController {
check(Looper.myLooper() == Looper.getMainLooper())
if (mutable.value?.ongoing == true) { LogManager.addLog("WARN", "stream", "publisher already owned"); return }
val id = nextId()
if (!NativeLib.isReady() || group == 0L || CallController.blocksMedia() || CallController.state.value?.ongoing == true ||
if (NativeLib.isCoreStopping() || !NativeLib.isReady() || group == 0L || CallController.blocksMedia() || CallController.state.value?.ongoing == true ||
NativeLib.voiceIsActive()) {
LogManager.addLog("WARN", "stream", "start rejected: core/call/voice busy group=$group")
mutable.value = StreamState(id, group, StreamPhase.FAILED, R.string.stream_resources_busy); return
@@ -29,14 +29,14 @@ class StreamServiceLifecycleTest {
assertSame(main, Looper.myLooper()); Dispatchers.setMain(dispatcher)
context = Context().also { ChatApplication.instance = it }
Handler.rejectImmediate = false; Handler.rejectDelayed = false; PendingIntent.reset(); NotificationCompat.failBuild = false
NativeLib.ready = true; NativeLib.voiceActive = false; NativeLib.streamFailures = emptySet()
NativeLib.ready = true; NativeLib.coreStopping = false; NativeLib.voiceActive = false; NativeLib.streamFailures = emptySet()
NativeLib.streamStops.clear(); NativeLib.streamEvents.clear(); NativeLib.streamStats = null
StreamCaptureEngine.nextFailures = emptySet(); StreamCaptureEngine.created.clear(); AppLanguage.failText = false
CallController.reset(); StreamController.reset(); main.dispatchAll()
}
@After fun release() {
Handler.rejectImmediate = false; Handler.rejectDelayed = false; NativeLib.streamFailures = emptySet()
NativeLib.ready = true; NativeLib.voiceActive = false; AppLanguage.failText = false; NotificationCompat.failBuild = false
NativeLib.ready = true; NativeLib.coreStopping = false; NativeLib.voiceActive = false; AppLanguage.failText = false; NotificationCompat.failBuild = false
StreamCaptureEngine.created.forEach { it.failures = emptySet() }
context.powerManager?.let { power -> power.failures = emptySet(); power.locks.forEach { it.failures = emptySet() } }
context.notificationManager?.failures = emptySet()
@@ -66,6 +66,16 @@ class StreamServiceLifecycleTest {
StreamController.requireReleased()
}
@Test fun coreCleanupRejectsNewPublisherBeforeForegroundOrCameraAcquisition() {
NativeLib.coreStopping = true
StreamController.start(7, StreamCaptureConfig(), true)
assertEquals(StreamPhase.FAILED, state.phase)
assertFalse(StreamController.capturing)
assertTrue(context.serviceStarts.isEmpty())
assertTrue(StreamCaptureEngine.created.isEmpty())
StreamController.requireReleased()
}
@Test fun nullForegroundRequestFailsWithoutLeavingAStartingPublisher() {
context.acceptServiceStart = false; request()
assertEquals(StreamPhase.FAILED, state.phase); assertFalse(StreamController.capturing)
@@ -30,7 +30,7 @@ class RadioAudioLifecycleTest {
AudioManager.shared = AudioManager(); CallController.blocked = false
AndroidMicrophoneCapture.reset(); AudioTrack.created.clear(); AudioTrack.nextFailures = emptySet(); AudioTrack.minimumBuffer = 1920
Handler.rejectDelayed = false; Handler.rejectImmediate = false
NativeLib.radioFailures = emptySet(); NativeLib.radioCommands.clear()
NativeLib.coreStopping = false; NativeLib.radioFailures = emptySet(); NativeLib.radioCommands.clear()
LogManager.entries.clear(); LogManager.console = true
engine = RadioAudioEngine()
io = RadioAudioEngine::class.java.getDeclaredField("io").apply { isAccessible = true }.get(engine) as ExecutorService
@@ -39,7 +39,7 @@ class RadioAudioLifecycleTest {
@After fun release() {
context.receiverFailures = emptySet(); context.failPowerService = false; context.configProvider.failRadioSettings = false
AudioTrack.created.forEach { it.failures = emptySet() }; AudioTrack.nextFailures = emptySet(); AudioTrack.minimumBuffer = 1920
NativeLib.radioFailures = emptySet(); Handler.rejectDelayed = false; Handler.rejectImmediate = false
NativeLib.coreStopping = false; NativeLib.radioFailures = emptySet(); Handler.rejectDelayed = false; Handler.rejectImmediate = false
if (!io.isShutdown) {
try { engine.stop().get(5, TimeUnit.SECONDS) } catch (e: ExecutionException) {
if (e.cause !is IllegalArgumentException || context.receivers.isNotEmpty()) throw e
@@ -80,6 +80,15 @@ class RadioAudioLifecycleTest {
assertTrue("radio state did not settle", condition())
}
@Test fun coreCleanupRejectsNewRadioBeforeAcquiringAudioDevices() {
NativeLib.coreStopping = true
start(expected = false)
assertFalse(engine.isActive())
assertTrue(AudioTrack.created.isEmpty())
assertTrue(AndroidMicrophoneCapture.opened.isEmpty())
diagnostic("core cleanup")
}
@Test fun receiverCleanupFailureDoesNotSkipCaptureOutputOrNativeStopAndCanBeRetried() {
start(); assertTrue(engine.talkBegin()); drain(); await { NativeLib.radioTransmitting() }
val track = AudioTrack.created.single()