Test rejected call signalling and deferred hangup recovery

This commit is contained in:
evgeny committed 2026-10-29 13:34:25 +01:00
1 parent 38e2cc4336
commit 7e328b3908
3 files changed
+76 -1

No files matched your search

+6
View File
@@ -231,6 +231,12 @@ static int check_dm_submissions(void) {
struct UTUN_INSTANCE* saved = instance; instance = NULL;
rc = utun_bridge_dm_send(conv, text, sizeof(text) - 1, generation, 17); instance = saved;
REQUIRE(rc == -1);
REQUIRE(utun_bridge_call_start(NULL, node, 1) == -1 && utun_bridge_call_start("7", 0, 1) == -1);
REQUIRE(utun_bridge_call_accept(0) == -1 && utun_bridge_call_decline(0) == -1 && utun_bridge_call_hangup(0) == -1);
instance = NULL;
REQUIRE(utun_bridge_call_start("7", node, 1) == -1 && utun_bridge_call_accept(1) == -1);
REQUIRE(utun_bridge_call_decline(1) == -1 && utun_bridge_call_hangup(1) == -1);
instance = saved;
uasync_poll(async, 0); REQUIRE(!results[15] && !results[16] && !results[17]);
printf("[PASS] Android PM: UTF-8, commit ACK, stale generation, SQL rollback/retry and rejected requests\n");
return 0;
@@ -320,7 +320,7 @@ object CallController : TelecomCallManager.Listener {
/** Ошибка постановки не удерживает FGS/audio, но сохраняет команду и запрещает новый media до queue ACK. */
private fun queueEnd(id: Long, decline: Boolean) {
if (sendEnd(id, decline)) return
if (sendEnd(id, decline)) { pendingEnds.value = pendingEnds.value - id; return }
pendingEnds.value = pendingEnds.value + (id to decline)
if (endRetry?.isActive == true) return
endRetry = scope.launch {
@@ -41,6 +41,7 @@ class CallServiceLifecycleTest {
assertSame(main, Looper.myLooper()); Dispatchers.setMain(dispatcher)
context = Context().also { ChatApplication.instance = it }; AudioManager.shared = AudioManager()
NativeLib.ready = true; NativeLib.failActivityRequest = false; NativeLib.acceptActivityRequest = true
NativeLib.rejectedCallCommands = emptySet()
AppLanguage.failText = false; AppLanguage.language = "en"; NotificationCompat.failBuild = false
Handler.rejectImmediate = false; Handler.rejectDelayed = false; PendingIntent.reset()
MediaAudioController.beforeIdle = {}
@@ -55,6 +56,7 @@ class CallServiceLifecycleTest {
gates.forEach { it.countDown() }; MediaAudioController.beforeIdle = {}
TelecomCallManager.pending = false
NativeLib.failActivityRequest = false
NativeLib.rejectedCallCommands = emptySet()
Handler.rejectImmediate = false; Handler.rejectDelayed = false
AppLanguage.failText = false; NotificationCompat.failBuild = false; CallVideoEngine.failCreate = false
power.failures = emptySet(); power.locks.forEach { it.failures = emptySet() }
@@ -109,6 +111,73 @@ class CallServiceLifecycleTest {
return service
}
@Test fun rejectedOutgoingSignallingEndsAndReleasesReservedService() {
NativeLib.rejectedCallCommands = setOf("start")
val service = create(); val id = request(); start(service, id)
await("rejected call must end") { state?.phase == CallPhase.ENDED }
assertFalse(power.locks.single().held); assertFalse(service.audio.active)
assertFalse(CallController.blocksMedia()); diagnostic("start queue rejected")
}
@Test fun rejectedAcceptDeclinesTheIncomingCallAndReleasesService() {
val id = 17L
CallController.onEvent(34, ByteBuffer.allocate(18).order(ByteOrder.LITTLE_ENDIAN)
.put(1.toByte()).put('7'.code.toByte()).putLong(99).putLong(id).array())
assertEquals(CallPhase.INCOMING, state?.phase)
NativeLib.rejectedCallCommands = setOf("accept")
CallController.accept(); val service = create(); start(service, id)
await("rejected answer must end") { state?.phase == CallPhase.ENDED }
assertTrue(NativeLib.callCommands.contains("decline" to id)); assertFalse(power.locks.single().held)
diagnostic("accept queue rejected")
}
@Test fun rejectedContinueKeepsCallerChoiceAvailableForRetry() {
val service = create(); val id = request(); start(service, id)
await("initial signaling queued") { NativeLib.callCommands.contains("start" to id) }
CallController.onEvent(66, ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(id).array())
assertEquals(CallPhase.WAIT_DECISION, state?.phase)
NativeLib.rejectedCallCommands = setOf("continue"); CallController.continueCall(id)
assertEquals(CallPhase.WAIT_DECISION, state?.phase); diagnostic("continue queue rejected")
NativeLib.rejectedCallCommands = emptySet(); CallController.continueCall(id)
assertEquals(CallPhase.OUTGOING, state?.phase)
}
@Test fun rejectedHangupReleasesAudioButWaitsForQueueAckBeforeRestoringRadio() {
RadioAudioEngine.instance().activeGroup.value = 7
active(); val id = requireNotNull(state).callId
NativeLib.rejectedCallCommands = setOf("hangup"); CallController.end(); dispatcher.scheduler.runCurrent()
assertEquals(CallPhase.ENDED, state?.phase); assertFalse(power.locks.single().held)
assertTrue(CallController.blocksMedia()); assertTrue(RadioAudioEngine.instance().restored.isEmpty())
val starts = context.serviceStarts.size
CallController.start("7", 101, "Other", mediaConfirmed = true)
assertEquals(starts, context.serviceStarts.size); assertEquals(id, state?.callId)
dispatcher.scheduler.advanceTimeBy(250); dispatcher.scheduler.runCurrent()
assertTrue(NativeLib.callCommands.count { it == "hangup" to id } >= 2)
NativeLib.rejectedCallCommands = emptySet()
dispatcher.scheduler.advanceTimeBy(500); dispatcher.scheduler.runCurrent()
await("radio restored after end queue ACK") { RadioAudioEngine.instance().restored == listOf(7L) }
assertFalse(CallController.blocksMedia()); diagnostic("hangup queue rejected")
}
@Test fun nativeEndEventCompletesPendingHangupWithoutAnotherCommand() {
active(); val id = requireNotNull(state).callId
NativeLib.rejectedCallCommands = setOf("hangup"); CallController.end()
assertTrue(CallController.blocksMedia())
CallController.onEvent(38, ByteBuffer.allocate(9).order(ByteOrder.LITTLE_ENDIAN).putLong(id).put(0.toByte()).array())
dispatcher.scheduler.runCurrent(); assertFalse(CallController.blocksMedia())
val attempts = NativeLib.callCommands.size
dispatcher.scheduler.advanceTimeBy(250); dispatcher.scheduler.runCurrent()
assertEquals(attempts, NativeLib.callCommands.size)
}
@Test fun resetCancelsPendingSignallingRetriesBeforeCoreStop() {
active(); NativeLib.rejectedCallCommands = setOf("hangup"); CallController.end()
assertTrue(CallController.blocksMedia()); CallController.reset(); dispatcher.scheduler.runCurrent()
assertFalse(CallController.blocksMedia()); val attempts = NativeLib.callCommands.size
dispatcher.scheduler.advanceTimeBy(30_000); dispatcher.scheduler.runCurrent()
assertEquals(attempts, NativeLib.callCommands.size)
}
@Test fun normalStartupOwnsOneLockReceiverAndForegroundBeforeAudio() {
val service = create(); val id = request(); start(service, id)
assertTrue(power.locks.single().held); assertFalse(power.locks.single().counted)