diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallTableTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallTableTest.kt index 19254bbfc4..38c63ce8b5 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallTableTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallTableTest.kt @@ -214,6 +214,175 @@ class CallTableTest { assertEquals(CallTable.Event.JOINED, acceptedCall?.event) } + @Test + fun givenAnOutgoingRingCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() { + val callId = 1L + SignalDatabase.calls.insertAcceptedGroupCall( + callId = callId, + recipientId = groupRecipientId, + direction = CallTable.Direction.OUTGOING, + timestamp = 1 + ) + + val call = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertNotNull(call) + assertEquals(CallTable.Event.OUTGOING_RING, call?.event) + + SignalDatabase.calls.acceptOutgoingGroupCall( + call!! + ) + + val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event) + } + + @Test + fun givenARingingCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() { + val callId = 1L + SignalDatabase.calls.insertOrUpdateGroupCallFromRingState( + ringId = callId, + groupRecipientId = groupRecipientId, + ringerRecipient = harness.others[1], + dateReceived = System.currentTimeMillis(), + ringState = CallManager.RingUpdate.REQUESTED + ) + + val call = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertNotNull(call) + assertEquals(CallTable.Event.RINGING, call?.event) + + SignalDatabase.calls.acceptOutgoingGroupCall( + call!! + ) + + val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event) + } + + @Test + fun givenAMissedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() { + val callId = 1L + SignalDatabase.calls.insertOrUpdateGroupCallFromRingState( + ringId = callId, + groupRecipientId = groupRecipientId, + ringerRecipient = harness.others[1], + dateReceived = System.currentTimeMillis(), + ringState = CallManager.RingUpdate.EXPIRED_REQUEST + ) + + val call = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertNotNull(call) + assertEquals(CallTable.Event.MISSED, call?.event) + + SignalDatabase.calls.acceptOutgoingGroupCall( + call!! + ) + + val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event) + } + + @Test + fun givenADeclinedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() { + val callId = 1L + SignalDatabase.calls.insertOrUpdateGroupCallFromRingState( + ringId = callId, + groupRecipientId = groupRecipientId, + ringerRecipient = harness.others[1], + dateReceived = System.currentTimeMillis(), + ringState = CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE + ) + + val call = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertNotNull(call) + assertEquals(CallTable.Event.DECLINED, call?.event) + + SignalDatabase.calls.acceptOutgoingGroupCall( + call!! + ) + + val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event) + } + + @Test + fun givenAnAcceptedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() { + val callId = 1L + SignalDatabase.calls.insertOrUpdateGroupCallFromRingState( + ringId = callId, + groupRecipientId = groupRecipientId, + ringerRecipient = harness.others[1], + dateReceived = System.currentTimeMillis(), + ringState = CallManager.RingUpdate.REQUESTED + ) + + val call = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertNotNull(call) + assertEquals(CallTable.Event.RINGING, call?.event) + + SignalDatabase.calls.acceptIncomingGroupCall( + call!! + ) + + SignalDatabase.calls.acceptOutgoingGroupCall( + SignalDatabase.calls.getCallById(callId, groupRecipientId)!! + ) + + val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event) + } + + @Test + fun givenAGenericGroupCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() { + val era = "aaa" + val callId = CallId.fromEra(era).longValue() + SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent( + groupRecipientId = groupRecipientId, + sender = harness.others[1], + timestamp = System.currentTimeMillis(), + peekGroupCallEraId = "aaa", + peekJoinedUuids = emptyList(), + isCallFull = false + ) + + val call = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertNotNull(call) + assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event) + + SignalDatabase.calls.acceptOutgoingGroupCall( + call!! + ) + + val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event) + } + + @Test + fun givenAJoinedGroupCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() { + val era = "aaa" + val callId = CallId.fromEra(era).longValue() + SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent( + groupRecipientId = groupRecipientId, + sender = harness.others[1], + timestamp = System.currentTimeMillis(), + peekGroupCallEraId = "aaa", + peekJoinedUuids = emptyList(), + isCallFull = false + ) + + val call = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertNotNull(call) + assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event) + + SignalDatabase.calls.acceptIncomingGroupCall( + call!! + ) + + SignalDatabase.calls.acceptOutgoingGroupCall(SignalDatabase.calls.getCallById(callId, groupRecipientId)!!) + val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event) + } + @Test fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() { val era = "aaa" diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt index 054276a2db..257b42c5a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt @@ -346,13 +346,12 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl fun acceptIncomingGroupCall(call: Call) { checkIsGroupOrAdHocCall(call) - check(call.direction == Direction.INCOMING) val newEvent = when (call.event) { Event.RINGING, Event.MISSED, Event.DECLINED -> Event.ACCEPTED Event.GENERIC_GROUP_CALL -> Event.JOINED else -> { - Log.d(TAG, "Call in state ${call.event} cannot be transitioned by ACCEPTED") + Log.d(TAG, "[acceptIncomingGroupCall] Call in state ${call.event} cannot be transitioned by ACCEPTED") return } } @@ -364,7 +363,32 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl ApplicationDependencies.getMessageNotifier().updateNotification(context) ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers() - Log.d(TAG, "Transitioned group call ${call.callId} from ${call.event} to $newEvent") + Log.d(TAG, "[acceptIncomingGroupCall] Transitioned group call ${call.callId} from ${call.event} to $newEvent") + } + + fun acceptOutgoingGroupCall(call: Call) { + checkIsGroupOrAdHocCall(call) + + val newEvent = when (call.event) { + Event.GENERIC_GROUP_CALL, Event.JOINED -> Event.OUTGOING_RING + Event.RINGING, Event.MISSED, Event.DECLINED, Event.ACCEPTED -> { + Log.w(TAG, "[acceptOutgoingGroupCall] This shouldn't have been an outgoing ring because the call already existed!") + Event.ACCEPTED + } + else -> { + Log.d(TAG, "[acceptOutgoingGroupCall] Call in state ${call.event} cannot be transitioned by ACCEPTED") + return + } + } + + writableDatabase + .update(TABLE_NAME) + .values(EVENT to Event.serialize(newEvent), DIRECTION to Direction.serialize(Direction.OUTGOING)) + .run() + + ApplicationDependencies.getMessageNotifier().updateNotification(context) + ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers() + Log.d(TAG, "[acceptOutgoingGroupCall] Transitioned group call ${call.callId} from ${call.event} to $newEvent") } fun declineIncomingGroupCall(call: Call) { @@ -372,7 +396,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl check(call.direction == Direction.INCOMING) val newEvent = when (call.event) { - Event.RINGING, Event.MISSED -> Event.DECLINED + Event.GENERIC_GROUP_CALL, Event.RINGING, Event.MISSED -> Event.DECLINED + Event.JOINED -> Event.ACCEPTED else -> { Log.d(TAG, "Call in state ${call.event} cannot be transitioned by DECLINED") return @@ -709,12 +734,24 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl RingUpdate.EXPIRED_REQUEST, RingUpdate.CANCELLED_BY_RINGER -> { when (call.event) { Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED, ringerRecipient) + Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED, ringerRecipient) Event.OUTGOING_RING -> Log.w(TAG, "Received an expiration or cancellation while in OUTGOING_RING state. Ignoring.") else -> Unit } } - RingUpdate.BUSY_LOCALLY, RingUpdate.BUSY_ON_ANOTHER_DEVICE -> { + RingUpdate.BUSY_LOCALLY -> { + when (call.event) { + Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED) + Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED) + else -> { + updateEventFromRingState(ringId, call.event, ringerRecipient) + Log.w(TAG, "Received a busy event we can't process. Updating ringer only.") + } + } + } + + RingUpdate.BUSY_ON_ANOTHER_DEVICE -> { when (call.event) { Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED) Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED) @@ -728,7 +765,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> { when (call.event) { - Event.RINGING, Event.MISSED -> updateEventFromRingState(ringId, Event.DECLINED) + Event.RINGING, Event.MISSED, Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.DECLINED) + Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED) Event.OUTGOING_RING -> Log.w(TAG, "Received DECLINED_ON_ANOTHER_DEVICE while in OUTGOING_RING state.") else -> Unit } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index 0b3b20cd1c..e2fdbe0355 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -1317,10 +1317,10 @@ object SyncMessageProcessor { if (call.timestamp > timestamp) { SignalDatabase.calls.setTimestamp(call.callId, recipient.id, timestamp) } - if (callEvent.direction == SyncMessage.CallEvent.Direction.INCOMING) { + if (direction == CallTable.Direction.INCOMING) { SignalDatabase.calls.acceptIncomingGroupCall(call) } else { - warn(envelopeTimestamp, "Invalid direction OUTGOING for event ACCEPTED") + SignalDatabase.calls.acceptOutgoingGroupCall(call) } } CallTable.Event.NOT_ACCEPTED -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java index 905f554597..77bf7cfceb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java @@ -256,7 +256,11 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro Log.w(TAG, "Error while trying to cancel ring " + ringId, e); } - webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), null, new CallId(ringId), true, false); + CallId callId = new CallId(ringId); + RemotePeer remotePeer = new RemotePeer(recipient.getId(), callId); + + webRtcInteractor.sendGroupCallNotAcceptedCallEventSyncMessage(remotePeer, false); + webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), null, callId, true, false); webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING); webRtcInteractor.stopAudio(false); webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index 4da906a39a..c10dd3ca2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -64,7 +64,6 @@ import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.util.AppForegroundObserver; -import org.thoughtcrime.securesms.util.BubbleUtil; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.RecipientAccessList; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -1135,6 +1134,19 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. } } + public void sendGroupCallNotAcceptedCallEventSyncMessage(@NonNull RemotePeer remotePeer, boolean isOutgoing) { + if (TextSecurePreferences.isMultiDevice(context)) { + networkExecutor.execute(() -> { + try { + SyncMessage.CallEvent callEvent = CallEventSyncMessageUtil.createNotAcceptedSyncMessage(remotePeer, System.currentTimeMillis(), isOutgoing, true); + ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(callEvent), Optional.empty()); + } catch (IOException | UntrustedIdentityException e) { + Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e); + } + }); + } + } + public @NonNull SignalCallLinkManager getCallLinkManager() { return new SignalCallLinkManager(Objects.requireNonNull(callManager)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java index 8091a92ee4..c113b1d3d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcInteractor.java @@ -204,4 +204,8 @@ public class WebRtcInteractor { public void sendNotAcceptedCallEventSyncMessage(@NonNull RemotePeer remotePeer, boolean isOutgoing, boolean isVideoCall) { signalCallManager.sendNotAcceptedCallEventSyncMessage(remotePeer, isOutgoing, isVideoCall); } + + public void sendGroupCallNotAcceptedCallEventSyncMessage(@NonNull RemotePeer remotePeer, boolean isOutgoing) { + signalCallManager.sendGroupCallNotAcceptedCallEventSyncMessage(remotePeer, isOutgoing); + } }