mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 05:14:50 +01:00
Fix issue with vanity interlacing frames.
This commit is contained in:
+13
-3
@@ -182,7 +182,7 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
|
||||
boolean remotePeerIsActive = remotePeer.callIdEquals(activePeer);
|
||||
boolean outgoingBeforeAccept = remotePeer.getState() == CallState.DIALING || remotePeer.getState() == CallState.REMOTE_RINGING;
|
||||
boolean incomingBeforeAccept = remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING;
|
||||
boolean incomingBeforeAccept = isIncomingBeforeAccept(currentState, remotePeer);
|
||||
|
||||
if (remotePeerIsActive && ENDED_REMOTE_END_REASON_TO_STATE.containsKey(callEndReason)) {
|
||||
state = Objects.requireNonNull(ENDED_REMOTE_END_REASON_TO_STATE.get(callEndReason));
|
||||
@@ -235,7 +235,7 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
webRtcInteractor.postStateUpdate(currentState);
|
||||
}
|
||||
|
||||
if (remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING) {
|
||||
if (isIncomingBeforeAccept(currentState, remotePeer)) {
|
||||
webRtcInteractor.insertMissedCall(remotePeer, remotePeer.getCallStartTimestamp(), currentState.getCallSetupState(remotePeer).isRemoteVideoOffer());
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
|
||||
webRtcInteractor.postStateUpdate(currentState);
|
||||
|
||||
if (activePeer.getState() == CallState.ANSWERING || activePeer.getState() == CallState.LOCAL_RINGING) {
|
||||
if (isIncomingBeforeAccept(currentState, activePeer)) {
|
||||
webRtcInteractor.insertMissedCall(activePeer, activePeer.getCallStartTimestamp(), currentState.getCallSetupState(activePeer).isRemoteVideoOffer());
|
||||
}
|
||||
|
||||
@@ -280,4 +280,14 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given peer represents an incoming call that the user never picked up. The peer stays in
|
||||
* {@link CallState#LOCAL_RINGING} from accept until the call connects, so the peer state alone cannot
|
||||
* distinguish a missed call from one that was answered but ended before connecting.
|
||||
*/
|
||||
private static boolean isIncomingBeforeAccept(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
|
||||
boolean isIncoming = remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING;
|
||||
return isIncoming && !currentState.getCallSetupState(remotePeer).isAccepted();
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -50,6 +50,11 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
|
||||
activePeer.connected();
|
||||
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().getRouter();
|
||||
if (router != null) {
|
||||
router.setVanitySink(null);
|
||||
}
|
||||
|
||||
boolean localVideoEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled();
|
||||
boolean remoteVideoEnabled = currentState.getCallSetupState(activePeer).isRemoteVideoOffer();
|
||||
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context, localVideoEnabled, remoteVideoEnabled));
|
||||
|
||||
+14
-3
@@ -152,6 +152,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
currentState = currentState.builder()
|
||||
.changeCallSetupState(activePeer.getCallId())
|
||||
.acceptWithVideo(answerWithVideo)
|
||||
.accepted(true)
|
||||
.build();
|
||||
|
||||
try {
|
||||
@@ -171,6 +172,11 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
if (currentState.getCallSetupState(activePeer).isAccepted()) {
|
||||
Log.w(TAG, "Cannot deny after call has been accepted!");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
Log.i(TAG, "handleDenyCall():");
|
||||
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().getRouter();
|
||||
@@ -193,10 +199,15 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleSetIncomingRingingVanity(@NonNull WebRtcServiceState currentState, boolean enabled) {
|
||||
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
|
||||
boolean isVideoOffer = currentState.getCallSetupState(activePeer).isRemoteVideoOffer();
|
||||
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
|
||||
CallSetupState callSetupState = currentState.getCallSetupState(activePeer);
|
||||
|
||||
if (!isVideoOffer) {
|
||||
if (!callSetupState.isRemoteVideoOffer()) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
if (callSetupState.isAccepted()) {
|
||||
Log.w(TAG, "handleSetIncomingRingingVanity(): call has already been accepted, ignoring");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ data class CallSetupState(
|
||||
var isEnableVideoOnCreate: Boolean = false,
|
||||
var isRemoteVideoOffer: Boolean = false,
|
||||
var isAcceptWithVideo: Boolean = false,
|
||||
var isAccepted: Boolean = false,
|
||||
@get:JvmName("hasSentJoinedMessage") var sentJoinedMessage: Boolean = false,
|
||||
@get:JvmName("shouldRingGroup") var ringGroup: Boolean = true,
|
||||
var ringId: Long = NO_RING,
|
||||
|
||||
+5
@@ -206,6 +206,11 @@ public class WebRtcServiceStateBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull CallSetupStateBuilder accepted(boolean accepted) {
|
||||
toBuild.setAccepted(accepted);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull CallSetupStateBuilder sentJoinedMessage(boolean sentJoinedMessage) {
|
||||
toBuild.setSentJoinedMessage(sentJoinedMessage);
|
||||
return this;
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import android.app.Application
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isSameInstanceAs
|
||||
import assertk.assertions.isTrue
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.ringrtc.CallId
|
||||
import org.signal.ringrtc.CallManager
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState
|
||||
import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
||||
|
||||
/**
|
||||
* State transition tests for the incoming 1:1 call processor, focused on the
|
||||
* accepted-but-not-yet-connected window: once a call is accepted, late vanity
|
||||
* toggles and deny requests (which can arrive off stale CALL_INCOMING state)
|
||||
* must be ignored.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class, instrumentedPackages = ["org.signal.ringrtc"])
|
||||
class IncomingCallActionProcessorTest {
|
||||
|
||||
companion object {
|
||||
private val CALL_ID = CallId(42L)
|
||||
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
fun setUpClass() {
|
||||
Log.initialize(SystemOutLogger())
|
||||
}
|
||||
}
|
||||
|
||||
private val callManager: CallManager = mockk(relaxed = true)
|
||||
private val webRtcInteractor: WebRtcInteractor = mockk(relaxed = true)
|
||||
private val router: OutgoingVideoSourceRouter = mockk(relaxed = true)
|
||||
private val localSink: BroadcastVideoSink = mockk()
|
||||
|
||||
private val processor = IncomingCallActionProcessor(webRtcInteractor)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
every { webRtcInteractor.callManager } returns callManager
|
||||
every { router.cameraState } returns CameraState(CameraState.Direction.FRONT, 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a ringing video call, when I handleAcceptCall, then I expect the call setup state to be accepted`() {
|
||||
val state = incomingRingingCall()
|
||||
|
||||
val result = processor.handleAcceptCall(state, true)
|
||||
|
||||
assertThat(result.getCallSetupState(CALL_ID).isAccepted).isTrue()
|
||||
verify { callManager.acceptCall(CALL_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a ringing unaccepted video call, when I enable vanity, then I expect the vanity camera to start`() {
|
||||
val state = incomingRingingCall(accepted = false, cameraEnabled = false)
|
||||
|
||||
processor.handleSetIncomingRingingVanity(state, true)
|
||||
|
||||
verify { router.setVanitySink(localSink) }
|
||||
verify { router.setEnabled(true) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an accepted call, when I enable vanity, then I expect no change`() {
|
||||
val state = incomingRingingCall(accepted = true, cameraEnabled = false)
|
||||
|
||||
val result = processor.handleSetIncomingRingingVanity(state, true)
|
||||
|
||||
assertThat(result).isSameInstanceAs(state)
|
||||
verify(exactly = 0) { router.setVanitySink(any()) }
|
||||
verify(exactly = 0) { router.setEnabled(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an accepted call with the camera on, when I disable vanity, then I expect the camera to stay on`() {
|
||||
val state = incomingRingingCall(accepted = true, cameraEnabled = true)
|
||||
|
||||
val result = processor.handleSetIncomingRingingVanity(state, false)
|
||||
|
||||
assertThat(result).isSameInstanceAs(state)
|
||||
verify(exactly = 0) { router.setEnabled(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an accepted call, when I handleDenyCall, then I expect the deny to be ignored`() {
|
||||
val state = incomingRingingCall(accepted = true)
|
||||
|
||||
val result = processor.handleDenyCall(state)
|
||||
|
||||
assertThat(result).isSameInstanceAs(state)
|
||||
verify(exactly = 0) { webRtcInteractor.sendNotAcceptedCallEventSyncMessage(any(), any(), any()) }
|
||||
verify(exactly = 0) { webRtcInteractor.rejectIncomingCall(any()) }
|
||||
verify(exactly = 0) { callManager.hangup() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a ringing unaccepted call, when I handleDenyCall, then I expect the call to be rejected and terminated`() {
|
||||
val state = incomingRingingCall(accepted = false)
|
||||
val activePeer = state.callInfoState.requireActivePeer()
|
||||
|
||||
val result = processor.handleDenyCall(state)
|
||||
|
||||
verify { webRtcInteractor.sendNotAcceptedCallEventSyncMessage(activePeer, false, true) }
|
||||
verify { webRtcInteractor.rejectIncomingCall(activePeer.id) }
|
||||
verify { callManager.hangup() }
|
||||
assertThat(result.callInfoState.activePeer).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an accepted call, when the remote hangs up before connecting, then I expect no missed call`() {
|
||||
val state = incomingRingingCall(accepted = true)
|
||||
val activePeer = state.callInfoState.requireActivePeer()
|
||||
|
||||
processor.handleEndedRemote(state, CallManager.CallEndReason.REMOTE_HANGUP, activePeer)
|
||||
|
||||
verify(exactly = 0) { webRtcInteractor.insertMissedCall(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an unaccepted ringing call, when the remote hangs up, then I expect a missed call`() {
|
||||
val state = incomingRingingCall(accepted = false)
|
||||
val activePeer = state.callInfoState.requireActivePeer()
|
||||
|
||||
processor.handleEndedRemote(state, CallManager.CallEndReason.REMOTE_HANGUP, activePeer)
|
||||
|
||||
verify { webRtcInteractor.insertMissedCall(activePeer, activePeer.callStartTimestamp, true) }
|
||||
}
|
||||
|
||||
private fun incomingRingingCall(
|
||||
accepted: Boolean = false,
|
||||
cameraEnabled: Boolean = false
|
||||
): WebRtcServiceState {
|
||||
val peer = RemotePeer(RecipientId.from(1L), CALL_ID)
|
||||
peer.answering()
|
||||
peer.localRinging()
|
||||
|
||||
return WebRtcServiceState(processor)
|
||||
.builder()
|
||||
.changeCallInfoState()
|
||||
.callState(WebRtcViewModel.State.CALL_INCOMING)
|
||||
.activePeer(peer)
|
||||
.commit()
|
||||
.changeCallSetupState(CALL_ID)
|
||||
.isRemoteVideoOffer(true)
|
||||
.accepted(accepted)
|
||||
.commit()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(if (cameraEnabled) CameraState(CameraState.Direction.FRONT, 2) else CameraState.UNKNOWN)
|
||||
.commit()
|
||||
.changeVideoState()
|
||||
.router(router)
|
||||
.localSink(localSink)
|
||||
.commit()
|
||||
.build()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user