Add support for group call disposition.

Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
Alex Hart 2023-04-03 10:44:14 -03:00
parent e94a84d4ec
commit f9548dcffe
40 changed files with 2165 additions and 340 deletions

View file

@ -0,0 +1,750 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.ringrtc.CallId
import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.testing.SignalActivityRule
@RunWith(AndroidJUnit4::class)
class CallTableTest {
@get:Rule
val harness = SignalActivityRule()
@Test
fun givenACall_whenISetTimestamp_thenIExpectUpdatedTimestamp() {
val callId = 1L
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
SignalDatabase.calls.setTimestamp(callId, -1L)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(-1L, call?.timestamp)
val messageRecord = SignalDatabase.messages.getMessageRecord(call!!.messageId!!)
assertEquals(-1L, messageRecord.dateReceived)
assertEquals(-1L, messageRecord.dateSent)
}
@Test
fun givenPreExistingEvent_whenIDeleteGroupCall_thenIMarkDeletedAndSetTimestamp() {
val callId = 1L
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
val call = SignalDatabase.calls.getCallById(callId)
SignalDatabase.calls.deleteGroupCall(call!!)
val deletedCall = SignalDatabase.calls.getCallById(callId)
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
assertEquals(CallTable.Event.DELETE, deletedCall?.event)
assertNotEquals(0L, oldestDeletionTimestamp)
assertNull(deletedCall!!.messageId)
}
@Test
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
val callId = 1L
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
callId,
harness.others[0],
CallTable.Direction.OUTGOING,
System.currentTimeMillis()
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
assertEquals(CallTable.Event.DELETE, call?.event)
assertNotEquals(oldestDeletionTimestamp, 0)
assertNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenIInsertAcceptedOutgoingGroupCall_thenIExpectLocalRingerAndOutgoingRing() {
val callId = 1L
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.OUTGOING,
System.currentTimeMillis()
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
assertEquals(harness.self.id, call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenIInsertAcceptedIncomingGroupCall_thenIExpectJoined() {
val callId = 1L
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
System.currentTimeMillis()
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.JOINED, call?.event)
assertNull(call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenARingingCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = harness.others[0],
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.RINGING, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId)
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenAMissedCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = harness.others[0],
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.EXPIRED_REQUEST
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId)
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenADeclinedCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = harness.others[0],
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.DECLINED, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId)
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenAGenericGroupCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = System.currentTimeMillis(),
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId)
assertEquals(CallTable.Event.JOINED, acceptedCall?.event)
}
@Test
fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = System.currentTimeMillis(),
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
}
@Test
fun givenAPriorCallEventWithNewerTimestamp_whenIReceiveAGroupCallUpdateMessage_thenIExpectAnUpdatedTimestamp() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.getCallById(callId).let {
assertNotNull(it)
assertEquals(now, it?.timestamp)
}
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = 1L,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
assertEquals(1L, call?.timestamp)
}
@Test
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
val callId = 1L
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
callId = callId,
recipientId = harness.others[0],
direction = CallTable.Direction.INCOMING,
timestamp = System.currentTimeMillis()
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = harness.others[0],
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.DELETE, call?.event)
}
@Test
fun givenAGenericCallEvent_whenRingRequested_thenISetRingerAndMoveToRingingState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.RINGING, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
}
@Test
fun givenAJoinedCallEvent_whenRingRequested_thenISetRingerAndMoveToRingingState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
}
@Test
fun givenAGenericCallEvent_whenRingExpired_thenISetRingerAndMoveToMissedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.EXPIRED_REQUEST
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
}
@Test
fun givenARingingCallEvent_whenRingExpired_thenISetRingerAndMoveToMissedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.EXPIRED_REQUEST
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
}
@Test
fun givenAJoinedCallEvent_whenRingIsCancelledBecauseUserIsBusyLocally_thenIMoveToAcceptedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_LOCALLY
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
}
@Test
fun givenAJoinedCallEvent_whenRingIsCancelledBecauseUserIsBusyOnAnotherDevice_thenIMoveToAcceptedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
}
@Test
fun givenARingingCallEvent_whenRingCancelledBecauseUserIsBusyLocally_thenIMoveToMissedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_LOCALLY
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
}
@Test
fun givenARingingCallEvent_whenRingCancelledBecauseUserIsBusyOnAnotherDevice_thenIMoveToMissedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
}
@Test
fun givenACallEvent_whenRingIsAcceptedOnAnotherDevice_thenIMoveToAcceptedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
}
@Test
fun givenARingingCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.DECLINED, call?.event)
}
@Test
fun givenAMissedCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.EXPIRED_REQUEST
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.DECLINED, call?.event)
}
@Test
fun givenAnOutgoingRingCallEvent_whenRingDeclinedOnAnotherDevice_thenIDoNotChangeState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.OUTGOING,
System.currentTimeMillis()
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
}
@Test
fun givenNoPriorEvent_whenRingRequested_thenICreateAnEventInTheRingingStateAndSetRinger() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.RINGING, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingExpired_thenICreateAnEventInTheMissedStateAndSetRinger() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.EXPIRED_REQUEST
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingCancelledByRinger_thenICreateAnEventInTheMissedStateAndSetRinger() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.CANCELLED_BY_RINGER
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyLocally_thenICreateAnEventInTheMissedState() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_LOCALLY
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyOnAnotherDevice_thenICreateAnEventInTheMissedState() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingAcceptedOnAnotherDevice_thenICreateAnEventInTheAcceptedState() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingDeclinedOnAnotherDevice_thenICreateAnEventInTheDeclinedState() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId)
assertNotNull(call)
assertEquals(CallTable.Event.DECLINED, call?.event)
assertNotNull(call?.messageId)
}
}

View file

@ -197,6 +197,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(this::ensureProfileUploaded)
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
@ -214,7 +215,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
.addPostRender(() -> SignalDatabase.groupCallRings().removeOldRings())
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
.execute();

View file

@ -186,6 +186,10 @@ class CallLogAdapter(
binding.callType.setImageResource(R.drawable.symbol_video_24)
binding.callType.setOnClickListener { onStartVideoCallClicked(peer) }
}
CallTable.Type.GROUP_CALL, CallTable.Type.AD_HOC_CALL -> {
// TODO [alex] -- Group call button
}
}
binding.callType.visible = true

View file

@ -72,7 +72,7 @@ class CallLogContextMenu(
iconRes = R.drawable.symbol_info_24,
title = fragment.getString(R.string.CallContextMenu__info)
) {
val intent = ConversationSettingsActivity.forCall(fragment.requireContext(), call.peer, longArrayOf(call.call.messageId))
val intent = ConversationSettingsActivity.forCall(fragment.requireContext(), call.peer, longArrayOf(call.call.messageId!!))
fragment.startActivity(intent)
}
}

View file

@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFi
import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterLerp
import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterPullState
import org.thoughtcrime.securesms.databinding.CallLogFragmentBinding
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
import org.thoughtcrime.securesms.main.SearchBinder
import org.thoughtcrime.securesms.recipients.Recipient
@ -167,6 +168,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
override fun onResume() {
super.onResume()
initializeSearchAction()
ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
}
private fun initializeTapToScrollToTop() {
@ -270,7 +272,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
if (viewModel.selectionStateSnapshot.isNotEmpty(binding.recycler.adapter!!.itemCount)) {
viewModel.toggleSelected(callLogRow.id)
} else {
val intent = ConversationSettingsActivity.forCall(requireContext(), callLogRow.peer, longArrayOf(callLogRow.call.messageId))
val intent = ConversationSettingsActivity.forCall(requireContext(), callLogRow.peer, longArrayOf(callLogRow.call.messageId!!))
startActivity(intent)
}
}

View file

@ -41,18 +41,18 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
}
fun deleteSelectedCallLogs(
selectedMessageIds: Set<Long>
selectedCallIds: Set<Long>
): Completable {
return Completable.fromAction {
SignalDatabase.messages.deleteCallUpdates(selectedMessageIds)
SignalDatabase.calls.deleteCallEvents(selectedCallIds)
}.observeOn(Schedulers.io())
}
fun deleteAllCallLogsExcept(
selectedMessageIds: Set<Long>
selectedCallIds: Set<Long>
): Completable {
return Completable.fromAction {
SignalDatabase.messages.deleteAllCallUpdatesExcept(selectedMessageIds)
SignalDatabase.calls.deleteAllCallEventsExcept(selectedCallIds)
}.observeOn(Schedulers.io())
}
}

View file

@ -17,7 +17,7 @@ sealed class CallLogRow {
val call: CallTable.Call,
val peer: Recipient,
val date: Long,
override val id: Id = Id.Call(call.messageId)
override val id: Id = Id.Call(call.callId)
) : CallLogRow()
/**
@ -28,7 +28,7 @@ sealed class CallLogRow {
}
sealed class Id {
data class Call(val messageId: Long) : Id()
data class Call(val callId: Long) : Id()
object ClearFilter : Id()
}
}

View file

@ -28,15 +28,15 @@ class CallLogStagedDeletion(
}
isCommitted = true
val messageIds = stateSnapshot.selected()
val callIds = stateSnapshot.selected()
.filterIsInstance<CallLogRow.Id.Call>()
.map { it.messageId }
.map { it.callId }
.toSet()
if (stateSnapshot.isExclusionary()) {
repository.deleteAllCallLogsExcept(messageIds).subscribe()
repository.deleteAllCallLogsExcept(callIds).subscribe()
} else {
repository.deleteSelectedCallLogs(messageIds).subscribe()
repository.deleteSelectedCallLogs(callIds).subscribe()
}
}
}

View file

@ -12,6 +12,7 @@ import org.signal.core.util.logging.Log
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.GroupRecord
@ -39,12 +40,16 @@ class ConversationSettingsRepository(
private val groupManagementRepository: GroupManagementRepository = GroupManagementRepository(context)
) {
fun getCallEvents(callMessageIds: LongArray): Single<List<MessageRecord>> {
fun getCallEvents(callMessageIds: LongArray): Single<List<Pair<CallTable.Call, MessageRecord>>> {
return if (callMessageIds.isEmpty()) {
Single.just(emptyList())
} else {
Single.fromCallable {
SignalDatabase.messages.getMessages(callMessageIds.toList()).iterator().asSequence().toList()
val callMap = SignalDatabase.calls.getCalls(callMessageIds.toList())
SignalDatabase.messages.getMessages(callMessageIds.toList()).iterator().asSequence()
.filter { callMap.containsKey(it.id) }
.map { callMap[it.id]!! to it }
.toList()
}
}
}

View file

@ -67,7 +67,7 @@ sealed class ConversationSettingsViewModel(
}
store.update(repository.getCallEvents(callMessageIds).toObservable()) { callRecords, state ->
state.copy(calls = callRecords.map { CallPreference.Model(it) })
state.copy(calls = callRecords.map { (call, messageRecord) -> CallPreference.Model(call, messageRecord) })
}
store.update(sharedMedia) { cursor, state ->

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.conversation.preferences
import androidx.annotation.DrawableRes
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.databinding.ConversationSettingsCallPreferenceItemBinding
@ -21,12 +22,14 @@ object CallPreference {
}
class Model(
val call: CallTable.Call,
val record: MessageRecord
) : MappingModel<Model> {
override fun areItemsTheSame(newItem: Model): Boolean = record.id == newItem.record.id
override fun areContentsTheSame(newItem: Model): Boolean {
return record.type == newItem.record.type &&
return call == newItem.call &&
record.type == newItem.record.type &&
record.isOutgoing == newItem.record.isOutgoing &&
record.timestamp == newItem.record.timestamp &&
record.id == newItem.record.id
@ -35,30 +38,42 @@ object CallPreference {
private class ViewHolder(binding: ConversationSettingsCallPreferenceItemBinding) : BindingViewHolder<Model, ConversationSettingsCallPreferenceItemBinding>(binding) {
override fun bind(model: Model) {
binding.callIcon.setImageResource(getCallIcon(model.record))
binding.callType.text = getCallType(model.record)
binding.callIcon.setImageResource(getCallIcon(model.call))
binding.callType.text = getCallType(model.call)
binding.callTime.text = getCallTime(model.record)
}
@DrawableRes
private fun getCallIcon(messageRecord: MessageRecord): Int {
return when (messageRecord.type) {
private fun getCallIcon(call: CallTable.Call): Int {
return when (call.messageType) {
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_24
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_24
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_24
else -> error("Unexpected type ${messageRecord.type}")
MessageTypes.GROUP_CALL_TYPE -> when {
call.event == CallTable.Event.MISSED -> R.drawable.symbol_missed_incoming_24
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_24
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_24
else -> throw AssertionError()
}
else -> error("Unexpected type ${call.type}")
}
}
private fun getCallType(messageRecord: MessageRecord): String {
val id = when (messageRecord.type) {
private fun getCallType(call: CallTable.Call): String {
val id = when (call.messageType) {
MessageTypes.MISSED_VIDEO_CALL_TYPE -> R.string.MessageRecord_missed_voice_call
MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.string.MessageRecord_missed_video_call
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.MessageRecord_incoming_voice_call
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.MessageRecord_incoming_video_call
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.MessageRecord_outgoing_voice_call
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.MessageRecord_outgoing_video_call
else -> error("Unexpected type ${messageRecord.type}")
MessageTypes.GROUP_CALL_TYPE -> when {
call.event == CallTable.Event.MISSED -> R.string.CallPreference__missed_group_call
call.direction == CallTable.Direction.INCOMING -> R.string.CallPreference__incoming_group_call
call.direction == CallTable.Direction.OUTGOING -> R.string.CallPreference__outgoing_group_call
else -> throw AssertionError()
}
else -> error("Unexpected type ${call.messageType}")
}
return context.getString(id)

View file

@ -2,24 +2,34 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.database.Cursor
import androidx.annotation.Discouraged
import androidx.core.content.contentValuesOf
import org.signal.core.util.IntSerializer
import org.signal.core.util.Serializer
import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.readToSingleLong
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireLong
import org.signal.core.util.requireObject
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.ringrtc.CallId
import org.signal.ringrtc.CallManager.RingUpdate
import org.thoughtcrime.securesms.calls.log.CallLogFilter
import org.thoughtcrime.securesms.calls.log.CallLogRow
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.CallSyncEventJob
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent
import java.util.UUID
/**
* Contains details for each 1:1 call.
@ -37,16 +47,23 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
private const val TYPE = "type"
private const val DIRECTION = "direction"
private const val EVENT = "event"
private const val TIMESTAMP = "timestamp"
private const val RINGER = "ringer"
private const val DELETION_TIMESTAMP = "deletion_timestamp"
//language=sql
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$CALL_ID INTEGER NOT NULL UNIQUE,
$MESSAGE_ID INTEGER NOT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE CASCADE,
$PEER INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
$MESSAGE_ID INTEGER DEFAULT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE SET NULL,
$PEER INTEGER DEFAULT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
$TYPE INTEGER NOT NULL,
$DIRECTION INTEGER NOT NULL,
$EVENT INTEGER NOT NULL
$EVENT INTEGER NOT NULL,
$TIMESTAMP INTEGER NOT NULL,
$RINGER INTEGER DEFAULT NULL,
$DELETION_TIMESTAMP INTEGER DEFAULT 0
)
""".trimIndent()
@ -56,7 +73,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
)
}
fun insertCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event) {
fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event) {
val messageType: Long = Call.getMessageType(type, direction, event)
writableDatabase.withinTransaction {
@ -68,7 +85,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
PEER to peer.serialize(),
TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(direction),
EVENT to Event.serialize(event)
EVENT to Event.serialize(event),
TIMESTAMP to timestamp
)
writableDatabase.insert(TABLE_NAME, null, values)
@ -79,7 +97,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
Log.i(TAG, "Inserted call: $callId type: $type direction: $direction event:$event")
}
fun updateCall(callId: Long, event: Event): Call? {
fun updateOneToOneCall(callId: Long, event: Event): Call? {
return writableDatabase.withinTransaction {
writableDatabase
.update(TABLE_NAME)
@ -97,7 +115,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
if (call != null) {
Log.i(TAG, "Updated call: $callId event: $event")
SignalDatabase.messages.updateCallLog(call.messageId, call.messageType)
SignalDatabase.messages.updateCallLog(call.messageId!!, call.messageType)
ApplicationDependencies.getMessageNotifier().updateNotification(context)
}
@ -131,7 +149,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
val cursor = readableDatabase
.select()
.from(TABLE_NAME)
.where(query.where, query.whereArgs)
.where("$EVENT != ${Event.serialize(Event.DELETE)} AND ${query.where}", query.whereArgs)
.run()
calls.putAll(cursor.readToList { c -> c.requireLong(MESSAGE_ID) to Call.deserialize(c) })
@ -139,9 +157,536 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
return calls
}
fun getOldestDeletionTimestamp(): Long {
return writableDatabase
.select(DELETION_TIMESTAMP)
.from(TABLE_NAME)
.where("$DELETION_TIMESTAMP > 0")
.orderBy("$DELETION_TIMESTAMP DESC")
.limit(1)
.run()
.readToSingleLong(0L)
}
fun deleteCallEventsDeletedBefore(threshold: Long) {
writableDatabase
.delete(TABLE_NAME)
.where("$DELETION_TIMESTAMP <= ?", threshold)
.run()
}
/**
* If a non-ad-hoc call has been deleted from the message database, then we need to
* set its deletion_timestamp to now.
*/
fun updateCallEventDeletionTimestamps() {
val where = "$TYPE != ? AND $DELETION_TIMESTAMP = 0 AND $MESSAGE_ID IS NULL"
val type = Type.serialize(Type.AD_HOC_CALL)
val toSync = writableDatabase.withinTransaction { db ->
val result = db
.select()
.from(TABLE_NAME)
.where(where, type)
.run()
.readToList {
Call.deserialize(it)
}
.toSet()
db
.update(TABLE_NAME)
.values(DELETION_TIMESTAMP to System.currentTimeMillis())
.where(where, type)
.run()
result
}
CallSyncEventJob.enqueueDeleteSyncEvents(toSync)
ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
}
// region Group / Ad-Hoc Calling
fun deleteGroupCall(call: Call) {
checkIsGroupOrAdHocCall(call)
writableDatabase.withinTransaction { db ->
db
.update(TABLE_NAME)
.values(
EVENT to Event.serialize(Event.DELETE),
DELETION_TIMESTAMP to System.currentTimeMillis()
)
.where("$CALL_ID = ?", call.callId)
.run()
if (call.messageId != null) {
SignalDatabase.messages.deleteMessage(call.messageId)
}
}
ApplicationDependencies.getMessageNotifier().updateNotification(context)
Log.d(TAG, "Marked group call event for deletion: ${call.callId}")
}
fun insertDeletedGroupCallFromSyncEvent(
callId: Long,
recipientId: RecipientId?,
direction: Direction,
timestamp: Long
) {
val type = if (recipientId != null) Type.GROUP_CALL else Type.AD_HOC_CALL
writableDatabase
.insertInto(TABLE_NAME)
.values(
CALL_ID to callId,
MESSAGE_ID to null,
PEER to recipientId?.toLong(),
EVENT to Event.serialize(Event.DELETE),
TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
DELETION_TIMESTAMP to System.currentTimeMillis()
)
.run()
ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
}
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")
return
}
}
writableDatabase
.update(TABLE_NAME)
.values(EVENT to Event.serialize(newEvent))
.run()
ApplicationDependencies.getMessageNotifier().updateNotification(context)
Log.d(TAG, "Transitioned group call ${call.callId} from ${call.event} to $newEvent")
}
fun insertAcceptedGroupCall(
callId: Long,
recipientId: RecipientId?,
direction: Direction,
timestamp: Long
) {
val type = if (recipientId != null) Type.GROUP_CALL else Type.AD_HOC_CALL
val event = if (direction == Direction.OUTGOING) Event.OUTGOING_RING else Event.JOINED
val ringer = if (direction == Direction.OUTGOING) Recipient.self().id.toLong() else null
writableDatabase.withinTransaction { db ->
val messageId: MessageId? = if (recipientId != null) {
SignalDatabase.messages.insertGroupCall(
groupRecipientId = recipientId,
sender = Recipient.self().id,
timestamp,
"",
emptyList(),
false
)
} else {
null
}
db
.insertInto(TABLE_NAME)
.values(
CALL_ID to callId,
MESSAGE_ID to messageId?.id,
PEER to recipientId?.toLong(),
EVENT to Event.serialize(event),
TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
RINGER to ringer
)
.run()
}
}
fun insertOrUpdateGroupCallFromExternalEvent(
groupRecipientId: RecipientId,
sender: RecipientId,
timestamp: Long,
messageGroupCallEraId: String?
) {
insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId,
sender,
timestamp,
messageGroupCallEraId,
emptyList(),
false
)
}
fun insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId: RecipientId,
sender: RecipientId,
timestamp: Long,
peekGroupCallEraId: String?,
peekJoinedUuids: Collection<UUID>,
isCallFull: Boolean
) {
writableDatabase.withinTransaction {
if (peekGroupCallEraId.isNullOrEmpty()) {
Log.w(TAG, "Dropping local call event with null era id.")
return@withinTransaction
}
val callId = CallId.fromEra(peekGroupCallEraId).longValue()
val call = getCallById(callId)
val messageId: MessageId = if (call != null) {
if (call.event == Event.DELETE) {
Log.d(TAG, "Dropping group call update for deleted call.")
return@withinTransaction
}
if (call.type != Type.GROUP_CALL) {
Log.d(TAG, "Dropping unsupported update message for non-group-call call.")
return@withinTransaction
}
if (call.messageId == null) {
Log.d(TAG, "Dropping group call update for call without an attached message.")
return@withinTransaction
}
SignalDatabase.messages.updateGroupCall(
call.messageId,
peekGroupCallEraId,
peekJoinedUuids,
isCallFull
)
} else {
SignalDatabase.messages.insertGroupCall(
groupRecipientId,
sender,
timestamp,
peekGroupCallEraId,
peekJoinedUuids,
isCallFull
)
}
insertCallEventFromGroupUpdate(
callId,
messageId,
sender,
groupRecipientId,
timestamp
)
}
}
private fun insertCallEventFromGroupUpdate(
callId: Long,
messageId: MessageId?,
sender: RecipientId,
groupRecipientId: RecipientId,
timestamp: Long
) {
if (messageId != null) {
val call = getCallById(callId)
if (call == null) {
val direction = if (sender == Recipient.self().id) Direction.OUTGOING else Direction.INCOMING
writableDatabase
.insertInto(TABLE_NAME)
.values(
CALL_ID to callId,
MESSAGE_ID to messageId.id,
PEER to groupRecipientId.toLong(),
EVENT to Event.serialize(Event.GENERIC_GROUP_CALL),
TYPE to Type.serialize(Type.GROUP_CALL),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
RINGER to null
)
.run()
Log.d(TAG, "Inserted new call event from group call update message. Call Id: $callId")
} else {
if (timestamp < call.timestamp) {
setTimestamp(callId, timestamp)
Log.d(TAG, "Updated call event timestamp for call id $callId")
}
if (call.messageId == null) {
setMessageId(callId, messageId)
Log.d(TAG, "Updated call event message id for newly inserted group call state: $callId")
}
}
} else {
Log.d(TAG, "Skipping call event processing for null era id.")
}
}
/**
* Since this does not alter the call table, we can simply pass this directly through to the old handler.
*/
fun updateGroupCallFromPeek(
threadId: Long,
peekGroupCallEraId: String?,
peekJoinedUuids: Collection<UUID>,
isCallFull: Boolean
): Boolean {
return SignalDatabase.messages.updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull)
}
fun insertOrUpdateGroupCallFromRingState(
ringId: Long,
groupRecipientId: RecipientId,
ringerRecipient: RecipientId,
dateReceived: Long,
ringState: RingUpdate
) {
handleGroupRingState(ringId, groupRecipientId, ringerRecipient, dateReceived, ringState)
}
fun insertOrUpdateGroupCallFromRingState(
ringId: Long,
groupRecipientId: RecipientId,
ringerUUID: UUID,
dateReceived: Long,
ringState: RingUpdate
) {
val ringerRecipient = Recipient.externalPush(ServiceId.from(ringerUUID))
handleGroupRingState(ringId, groupRecipientId, ringerRecipient.id, dateReceived, ringState)
}
fun isRingCancelled(ringId: Long): Boolean {
val call = getCallById(ringId) ?: return false
return call.event != Event.RINGING
}
private fun handleGroupRingState(
ringId: Long,
groupRecipientId: RecipientId,
ringerRecipient: RecipientId,
dateReceived: Long,
ringState: RingUpdate
) {
val call = getCallById(ringId)
if (call != null) {
if (call.event == Event.DELETE) {
Log.d(TAG, "Ignoring ring request for $ringId since its event has been deleted.")
return
}
when (ringState) {
RingUpdate.REQUESTED -> {
when (call.event) {
Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.RINGING, ringerRecipient)
Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED, ringerRecipient)
else -> Log.w(TAG, "Received a REQUESTED ring event while in ${call.event}. Ignoring.")
}
}
RingUpdate.EXPIRED_REQUEST, RingUpdate.CANCELLED_BY_RINGER -> {
when (call.event) {
Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED, 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 -> {
when (call.event) {
Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED)
Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED)
else -> Log.w(TAG, "Received a busy event we can't process. Ignoring.")
}
}
RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE -> {
updateEventFromRingState(ringId, Event.ACCEPTED)
}
RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> {
when (call.event) {
Event.RINGING, Event.MISSED -> updateEventFromRingState(ringId, Event.DECLINED)
Event.OUTGOING_RING -> Log.w(TAG, "Received DECLINED_ON_ANOTHER_DEVICE while in OUTGOING_RING state.")
else -> Unit
}
}
}
} else {
val event: Event = when (ringState) {
RingUpdate.REQUESTED -> Event.RINGING
RingUpdate.EXPIRED_REQUEST -> Event.MISSED
RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE -> {
Log.w(TAG, "Missed original ring request for $ringId")
Event.ACCEPTED
}
RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> {
Log.w(TAG, "Missed original ring request for $ringId")
Event.DECLINED
}
RingUpdate.BUSY_LOCALLY, RingUpdate.BUSY_ON_ANOTHER_DEVICE -> {
Log.w(TAG, "Missed original ring request for $ringId")
Event.MISSED
}
RingUpdate.CANCELLED_BY_RINGER -> {
Log.w(TAG, "Missed original ring request for $ringId")
Event.MISSED
}
}
createEventFromRingState(ringId, groupRecipientId, ringerRecipient, event, dateReceived)
}
}
private fun updateEventFromRingState(
callId: Long,
event: Event,
ringerRecipient: RecipientId
) {
writableDatabase
.update(TABLE_NAME)
.values(
EVENT to Event.serialize(event),
RINGER to ringerRecipient.serialize()
)
.where("$CALL_ID = ?", callId)
.run()
Log.d(TAG, "Updated ring state to $event")
}
private fun updateEventFromRingState(
callId: Long,
event: Event
) {
writableDatabase
.update(TABLE_NAME)
.values(
EVENT to Event.serialize(event)
)
.where("$CALL_ID = ?", callId)
.run()
Log.d(TAG, "Updated ring state to $event")
}
private fun createEventFromRingState(
callId: Long,
groupRecipientId: RecipientId,
ringerRecipient: RecipientId,
event: Event,
timestamp: Long
) {
val direction = if (ringerRecipient == Recipient.self().id) Direction.OUTGOING else Direction.INCOMING
writableDatabase.withinTransaction { db ->
val messageId = SignalDatabase.messages.insertGroupCall(
groupRecipientId = groupRecipientId,
sender = ringerRecipient,
timestamp = timestamp,
eraId = "",
joinedUuids = emptyList(),
isCallFull = false
)
db
.insertInto(TABLE_NAME)
.values(
CALL_ID to callId,
MESSAGE_ID to messageId.id,
PEER to groupRecipientId.toLong(),
EVENT to Event.serialize(event),
TYPE to Type.serialize(Type.GROUP_CALL),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
RINGER to ringerRecipient.toLong()
)
.run()
}
Log.d(TAG, "Inserted a new call event for $callId with event $event")
}
fun setTimestamp(callId: Long, timestamp: Long) {
writableDatabase.withinTransaction { db ->
val call = getCallById(callId)
if (call == null || call.event == Event.DELETE) {
Log.d(TAG, "Refusing to update deleted call event.")
return@withinTransaction
}
db
.update(TABLE_NAME)
.values(TIMESTAMP to timestamp)
.where("$CALL_ID = ?", callId)
.run()
if (call.messageId != null) {
SignalDatabase.messages.updateCallTimestamps(call.messageId, timestamp)
}
}
}
private fun setMessageId(callId: Long, messageId: MessageId) {
writableDatabase
.update(TABLE_NAME)
.values(MESSAGE_ID to messageId.id)
.where("$CALL_ID = ?", callId)
.run()
}
fun deleteCallEvents(callIds: Set<Long>) {
val messageIds = getMessageIds(callIds)
SignalDatabase.messages.deleteCallUpdates(messageIds)
updateCallEventDeletionTimestamps()
}
fun deleteAllCallEventsExcept(callIds: Set<Long>) {
val messageIds = getMessageIds(callIds)
SignalDatabase.messages.deleteAllCallUpdatesExcept(messageIds)
updateCallEventDeletionTimestamps()
}
@Discouraged("Using this method is generally considered an error. Utilize other deletion methods instead of this.")
fun deleteAllCalls() {
Log.w(TAG, "Deleting all calls from the local database.")
writableDatabase
.delete(TABLE_NAME)
.run()
}
private fun getMessageIds(callIds: Set<Long>): Set<Long> {
val queries = SqlUtil.buildCollectionQuery(
CALL_ID,
callIds,
"$MESSAGE_ID NOT NULL AND"
)
return queries.map { query ->
readableDatabase.select(MESSAGE_ID).from(TABLE_NAME).where(query.where, query.whereArgs).run().readToList {
it.requireLong(MESSAGE_ID)
}
}.flatten().toSet()
}
private fun checkIsGroupOrAdHocCall(call: Call) {
check(call.type == Type.GROUP_CALL || call.type == Type.AD_HOC_CALL)
}
// endregion
private fun getCallsCursor(isCount: Boolean, offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): Cursor {
val filterClause = when (filter) {
CallLogFilter.ALL -> SqlUtil.buildQuery("")
CallLogFilter.ALL -> SqlUtil.buildQuery("$EVENT != ${Event.serialize(Event.DELETE)}")
CallLogFilter.MISSED -> SqlUtil.buildQuery("$EVENT == ${Event.serialize(Event.MISSED)}")
}
@ -233,12 +778,22 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
val type: Type,
val direction: Direction,
val event: Event,
val messageId: Long
val messageId: Long?,
val timestamp: Long,
val ringerRecipient: RecipientId?
) {
val messageType: Long = getMessageType(type, direction, event)
companion object Deserializer : Serializer<Call, Cursor> {
fun getMessageType(type: Type, direction: Direction, event: Event): Long {
if (type == Type.GROUP_CALL) {
return MessageTypes.GROUP_CALL_TYPE
}
if (type == Type.AD_HOC_CALL) {
error("Ad-Hoc calls are not linked to messages.")
}
return if (direction == Direction.INCOMING && event == Event.MISSED) {
if (type == Type.VIDEO_CALL) MessageTypes.MISSED_VIDEO_CALL_TYPE else MessageTypes.MISSED_AUDIO_CALL_TYPE
} else if (direction == Direction.INCOMING) {
@ -259,7 +814,15 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
type = data.requireObject(TYPE, Type.Serializer),
direction = data.requireObject(DIRECTION, Direction.Serializer),
event = data.requireObject(EVENT, Event.Serializer),
messageId = data.requireLong(MESSAGE_ID)
messageId = data.requireLong(MESSAGE_ID).takeIf { it > 0L },
timestamp = data.requireLong(TIMESTAMP),
ringerRecipient = data.requireLong(RINGER).let {
if (it > 0) {
RecipientId.from(it)
} else {
null
}
}
)
}
}
@ -267,7 +830,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
enum class Type(private val code: Int) {
AUDIO_CALL(0),
VIDEO_CALL(1);
VIDEO_CALL(1),
GROUP_CALL(3),
AD_HOC_CALL(4);
companion object Serializer : IntSerializer<Type> {
override fun serialize(data: Type): Int = data.code
@ -276,6 +841,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
return when (data) {
AUDIO_CALL.code -> AUDIO_CALL
VIDEO_CALL.code -> VIDEO_CALL
GROUP_CALL.code -> GROUP_CALL
AD_HOC_CALL.code -> AD_HOC_CALL
else -> throw IllegalArgumentException("Unknown type $data")
}
}
@ -286,6 +853,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
CallEvent.Type.UNKNOWN_TYPE -> null
CallEvent.Type.AUDIO_CALL -> AUDIO_CALL
CallEvent.Type.VIDEO_CALL -> VIDEO_CALL
CallEvent.Type.GROUP_CALL -> GROUP_CALL
CallEvent.Type.AD_HOC_CALL -> AD_HOC_CALL
}
}
}
@ -318,22 +887,69 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
}
enum class Event(private val code: Int) {
/**
* 1:1 Calls only.
*/
ONGOING(0),
/**
* 1:1 and Group Calls.
*
* Group calls: You accepted a ring.
*/
ACCEPTED(1),
/**
* 1:1 Calls only.
*/
NOT_ACCEPTED(2),
MISSED(3);
/**
* 1:1 and Group/Ad-Hoc Calls.
*
* Group calls: The remote ring has expired or was cancelled by the ringer.
*/
MISSED(3),
/**
* 1:1 and Group/Ad-Hoc Calls.
*/
DELETE(4),
/**
* Group/Ad-Hoc Calls only.
*
* Initial state.
*/
GENERIC_GROUP_CALL(5),
/**
* Group Calls: User has joined the group call.
*/
JOINED(6),
/**
* Group Calls: If a ring was requested by another user.
*/
RINGING(7),
/**
* Group Calls: If you declined a ring.
*/
DECLINED(8),
/**
* Group Calls: If you are ringing a group.
*/
OUTGOING_RING(9);
companion object Serializer : IntSerializer<Event> {
override fun serialize(data: Event): Int = data.code
override fun deserialize(data: Int): Event {
return when (data) {
ONGOING.code -> ONGOING
ACCEPTED.code -> ACCEPTED
NOT_ACCEPTED.code -> NOT_ACCEPTED
MISSED.code -> MISSED
else -> throw IllegalArgumentException("Unknown type $data")
}
return values().firstOrNull {
it.code == data
} ?: throw IllegalArgumentException("Unknown event $data")
}
@JvmStatic
@ -342,6 +958,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
CallEvent.Event.UNKNOWN_ACTION -> null
CallEvent.Event.ACCEPTED -> ACCEPTED
CallEvent.Event.NOT_ACCEPTED -> NOT_ACCEPTED
CallEvent.Event.DELETE -> DELETE
}
}
}

View file

@ -1,98 +0,0 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil
import org.signal.ringrtc.CallManager
import java.util.concurrent.TimeUnit
/**
* Track state of Group Call ring cancellations.
*/
class GroupCallRingTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
companion object {
private val VALID_RING_DURATION = TimeUnit.MINUTES.toMillis(30)
private const val TABLE_NAME = "group_call_ring"
private const val ID = "_id"
private const val RING_ID = "ring_id"
private const val DATE_RECEIVED = "date_received"
private const val RING_STATE = "ring_state"
@JvmField
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$RING_ID INTEGER UNIQUE,
$DATE_RECEIVED INTEGER,
$RING_STATE INTEGER
)
""".trimIndent()
@JvmField
val CREATE_INDEXES = arrayOf(
"CREATE INDEX date_received_index on $TABLE_NAME ($DATE_RECEIVED)"
)
}
fun isCancelled(ringId: Long): Boolean {
val db = databaseHelper.signalReadableDatabase
db.query(TABLE_NAME, null, "$RING_ID = ?", SqlUtil.buildArgs(ringId), null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
return CursorUtil.requireInt(cursor, RING_STATE) != 0
}
}
return false
}
fun insertGroupRing(ringId: Long, dateReceived: Long, ringState: CallManager.RingUpdate) {
val db = databaseHelper.signalWritableDatabase
val values = ContentValues().apply {
put(RING_ID, ringId)
put(DATE_RECEIVED, dateReceived)
put(RING_STATE, ringState.toCode())
}
db.insert(TABLE_NAME, null, values)
removeOldRings()
}
fun insertOrUpdateGroupRing(ringId: Long, dateReceived: Long, ringState: CallManager.RingUpdate) {
val db = databaseHelper.signalWritableDatabase
val values = ContentValues().apply {
put(RING_ID, ringId)
put(DATE_RECEIVED, dateReceived)
put(RING_STATE, ringState.toCode())
}
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE)
removeOldRings()
}
fun removeOldRings() {
val db = databaseHelper.signalWritableDatabase
db.delete(TABLE_NAME, "$DATE_RECEIVED < ?", SqlUtil.buildArgs(System.currentTimeMillis() - VALID_RING_DURATION))
}
fun deleteAll() {
databaseHelper.signalWritableDatabase.delete(TABLE_NAME, null, null)
}
}
private fun CallManager.RingUpdate.toCode(): Int {
return when (this) {
CallManager.RingUpdate.REQUESTED -> 0
CallManager.RingUpdate.EXPIRED_REQUEST -> 1
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE -> 2
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> 3
CallManager.RingUpdate.BUSY_LOCALLY -> 4
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE -> 5
CallManager.RingUpdate.CANCELLED_BY_RINGER -> 6
}
}

View file

@ -38,7 +38,6 @@ import org.signal.core.util.SqlUtil.buildSingleCollectionQuery
import org.signal.core.util.SqlUtil.buildTrueUpdateQuery
import org.signal.core.util.SqlUtil.getNextAutoIncrementId
import org.signal.core.util.delete
import org.signal.core.util.emptyIfNull
import org.signal.core.util.exists
import org.signal.core.util.forEach
import org.signal.core.util.insertInto
@ -71,6 +70,7 @@ import org.thoughtcrime.securesms.conversation.MessageStyler
import org.thoughtcrime.securesms.database.EarlyReceiptCache.Receipt
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.distributionLists
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groupReceipts
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
@ -400,6 +400,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
($TYPE = ${MessageTypes.MISSED_AUDIO_CALL_TYPE})
OR
($TYPE = ${MessageTypes.MISSED_VIDEO_CALL_TYPE})
OR
($TYPE = ${MessageTypes.GROUP_CALL_TYPE})
)""".toSingleLine()
@JvmStatic
@ -802,122 +804,111 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
}
fun insertOrUpdateGroupCall(
fun insertGroupCall(
groupRecipientId: RecipientId,
sender: RecipientId,
timestamp: Long,
peekGroupCallEraId: String?,
peekJoinedUuids: Collection<UUID>,
eraId: String,
joinedUuids: Collection<UUID>,
isCallFull: Boolean
) {
): MessageId {
val recipient = Recipient.resolved(groupRecipientId)
val threadId = threads.getOrCreateThreadIdFor(recipient)
val peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull)
val messageId: MessageId = writableDatabase.withinTransaction { db ->
val self = Recipient.self()
val markRead = joinedUuids.contains(self.requireServiceId().uuid()) || self.id == sender
val updateDetails: ByteArray = GroupCallUpdateDetails.newBuilder()
.setEraId(eraId)
.setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString())
.setStartedCallTimestamp(timestamp)
.addAllInCallUuids(joinedUuids.map { it.toString() })
.setIsCallFull(isCallFull)
.build()
.toByteArray()
writableDatabase.withinTransaction { db ->
if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) {
val self = Recipient.self()
val markRead = peekJoinedUuids.contains(self.requireServiceId().uuid()) || self.id == sender
val updateDetails = GroupCallUpdateDetails.newBuilder()
.setEraId(peekGroupCallEraId.emptyIfNull())
.setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString())
.setStartedCallTimestamp(timestamp)
.addAllInCallUuids(peekJoinedUuids.map { it.toString() }.toList())
.setIsCallFull(isCallFull)
.build()
.toByteArray()
val values = contentValuesOf(
RECIPIENT_ID to sender.serialize(),
RECIPIENT_DEVICE_ID to 1,
DATE_RECEIVED to timestamp,
DATE_SENT to timestamp,
READ to if (markRead) 1 else 0,
BODY to Base64.encodeBytes(updateDetails),
TYPE to MessageTypes.GROUP_CALL_TYPE,
THREAD_ID to threadId
)
db.insert(TABLE_NAME, null, values)
threads.incrementUnread(threadId, 1, 0)
}
val values = contentValuesOf(
RECIPIENT_ID to sender.serialize(),
RECIPIENT_DEVICE_ID to 1,
DATE_RECEIVED to timestamp,
DATE_SENT to timestamp,
READ to if (markRead) 1 else 0,
BODY to Base64.encodeBytes(updateDetails),
TYPE to MessageTypes.GROUP_CALL_TYPE,
THREAD_ID to threadId
)
val messageId = MessageId(db.insert(TABLE_NAME, null, values))
threads.incrementUnread(threadId, 1, 0)
threads.update(threadId, true)
messageId
}
notifyConversationListeners(threadId)
TrimThreadJob.enqueueAsync(threadId)
return messageId
}
fun insertOrUpdateGroupCall(
groupRecipientId: RecipientId,
sender: RecipientId,
timestamp: Long,
messageGroupCallEraId: String?
) {
val threadId = writableDatabase.withinTransaction { db ->
val recipient = Recipient.resolved(groupRecipientId)
val threadId = threads.getOrCreateThreadIdFor(recipient)
val cursor = db
.select(*MMS_PROJECTION)
.from(TABLE_NAME)
.where("$TYPE = ? AND $THREAD_ID = ?", MessageTypes.GROUP_CALL_TYPE, threadId)
.orderBy("$DATE_RECEIVED DESC")
.limit(1)
.run()
var sameEraId = false
MmsReader(cursor).use { reader ->
val record: MessageRecord? = reader.firstOrNull()
if (record != null) {
val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body)
sameEraId = groupCallUpdateDetails.eraId == messageGroupCallEraId && !Util.isEmpty(messageGroupCallEraId)
if (!sameEraId) {
db.update(TABLE_NAME)
.values(BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, emptyList(), false))
.where("$ID = ?", record.id)
.run()
}
}
}
if (!sameEraId && !Util.isEmpty(messageGroupCallEraId)) {
val updateDetails = GroupCallUpdateDetails.newBuilder()
.setEraId(Util.emptyIfNull(messageGroupCallEraId))
.setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString())
.setStartedCallTimestamp(timestamp)
.addAllInCallUuids(emptyList())
.setIsCallFull(false)
.build()
.toByteArray()
val values = contentValuesOf(
RECIPIENT_ID to sender.serialize(),
RECIPIENT_DEVICE_ID to 1,
DATE_RECEIVED to timestamp,
DATE_SENT to timestamp,
READ to 0,
BODY to Base64.encodeBytes(updateDetails),
TYPE to MessageTypes.GROUP_CALL_TYPE,
THREAD_ID to threadId
)
db.insert(TABLE_NAME, null, values)
threads.incrementUnread(threadId, 1, 0)
}
threads.update(threadId, true)
threadId
/**
* Updates the timestamps associated with the given message id to the given ts
*/
fun updateCallTimestamps(messageId: Long, timestamp: Long) {
val message = try {
getMessageRecord(messageId = messageId)
} catch (e: NoSuchMessageException) {
error("Message $messageId does not exist")
}
notifyConversationListeners(threadId)
TrimThreadJob.enqueueAsync(threadId)
val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body)
val contentValues = contentValuesOf(
BODY to Base64.encodeBytes(updateDetail.toBuilder().setStartedCallTimestamp(timestamp).build().toByteArray()),
DATE_SENT to timestamp,
DATE_RECEIVED to timestamp
)
val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(messageId), contentValues)
val updated = writableDatabase.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0
if (updated) {
notifyConversationListeners(message.threadId)
}
}
fun updateGroupCall(
messageId: Long,
eraId: String,
joinedUuids: Collection<UUID>,
isCallFull: Boolean
): MessageId {
writableDatabase.withinTransaction { db ->
val message = try {
getMessageRecord(messageId = messageId)
} catch (e: NoSuchMessageException) {
error("Message $messageId does not exist.")
}
val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body)
val containsSelf = joinedUuids.contains(SignalStore.account().requireAci().uuid())
val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId)
val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList()
val contentValues = contentValuesOf(
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull)
)
if (sameEraId && containsSelf) {
contentValues.put(READ, 1)
}
val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(messageId), contentValues)
val updated = db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0
if (updated) {
notifyConversationListeners(message.threadId)
}
}
return MessageId(messageId)
}
fun updatePreviousGroupCall(threadId: Long, peekGroupCallEraId: String?, peekJoinedUuids: Collection<UUID>, isCallFull: Boolean): Boolean {
@ -3099,6 +3090,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.where("$ID = ?", messageId)
.run()
calls.updateCallEventDeletionTimestamps()
threads.setLastScrolled(threadId, 0)
val threadDeleted = threads.update(threadId, false)
@ -3356,6 +3348,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (deletes > 0) {
Log.i(TAG, "Deleted $deletes abandoned messages")
calls.updateCallEventDeletionTimestamps()
}
return deletes
@ -3385,6 +3378,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
groupReceipts.deleteAllRows()
mentions.deleteAllMentions()
writableDatabase.delete(TABLE_NAME).run()
calls.updateCallEventDeletionTimestamps()
OptimizeMessageSearchIndexJob.enqueue()
}

View file

@ -65,7 +65,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val emojiSearchTable: EmojiSearchTable = EmojiSearchTable(context, this)
val messageSendLogTables: MessageSendLogTables = MessageSendLogTables(context, this)
val avatarPickerDatabase: AvatarPickerDatabase = AvatarPickerDatabase(context, this)
val groupCallRingTable: GroupCallRingTable = GroupCallRingTable(context, this)
val reactionTable: ReactionTable = ReactionTable(context, this)
val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
val donationReceiptTable: DonationReceiptTable = DonationReceiptTable(context, this)
@ -103,7 +102,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
db.execSQL(ChatColorsTable.CREATE_TABLE)
db.execSQL(EmojiSearchTable.CREATE_TABLE)
db.execSQL(AvatarPickerDatabase.CREATE_TABLE)
db.execSQL(GroupCallRingTable.CREATE_TABLE)
db.execSQL(ReactionTable.CREATE_TABLE)
db.execSQL(DonationReceiptTable.CREATE_TABLE)
db.execSQL(StorySendTable.CREATE_TABLE)
@ -129,7 +127,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
executeStatements(db, MentionTable.CREATE_INDEXES)
executeStatements(db, PaymentTable.CREATE_INDEXES)
executeStatements(db, MessageSendLogTables.CREATE_INDEXES)
executeStatements(db, GroupCallRingTable.CREATE_INDEXES)
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
executeStatements(db, DonationReceiptTable.CREATE_INDEXS)
executeStatements(db, StorySendTable.CREATE_INDEXS)
@ -389,11 +386,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val emojiSearch: EmojiSearchTable
get() = instance!!.emojiSearchTable
@get:JvmStatic
@get:JvmName("groupCallRings")
val groupCallRings: GroupCallRingTable
get() = instance!!.groupCallRingTable
@get:JvmStatic
@get:JvmName("groupReceipts")
val groupReceipts: GroupReceiptTable

View file

@ -31,8 +31,8 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.drafts
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groupCallRings
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groupReceipts
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.mentions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
@ -380,6 +380,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
setLastScrolled(threadId, 0)
update(threadId, false)
notifyConversationListeners(threadId)
SignalDatabase.calls.updateCallEventDeletionTimestamps()
} else {
Log.i(TAG, "Trimming deleted no messages thread: $threadId")
}
@ -1081,13 +1082,14 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
ConversationUtil.clearShortcuts(context, recipientIds)
}
@SuppressLint("DiscouragedApi")
fun deleteAllConversations() {
writableDatabase.withinTransaction { db ->
messageLog.deleteAll()
messages.deleteAllThreads()
drafts.clearAllDrafts()
groupCallRings.deleteAll()
db.delete(TABLE_NAME, null, null)
calls.deleteAllCalls()
}
notifyConversationListListeners()

View file

@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V178_ReportingToken
import org.thoughtcrime.securesms.database.helpers.migration.V179_CleanupDanglingMessageSendLogMigration
import org.thoughtcrime.securesms.database.helpers.migration.V180_RecipientNicknameMigration
import org.thoughtcrime.securesms.database.helpers.migration.V181_ThreadTableForeignKeyCleanup
import org.thoughtcrime.securesms.database.helpers.migration.V182_CallTableMigration
/**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@ -45,7 +46,7 @@ object SignalDatabaseMigrations {
val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
const val DATABASE_VERSION = 181
const val DATABASE_VERSION = 182
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@ -180,6 +181,10 @@ object SignalDatabaseMigrations {
if (oldVersion < 181) {
V181_ThreadTableForeignKeyCleanup.migrate(context, db, oldVersion, newVersion)
}
if (oldVersion < 182) {
V182_CallTableMigration.migrate(context, db, oldVersion, newVersion)
}
}
@JvmStatic

View file

@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.RecipientTable
/**
* Adds a new 'timestamp' column to CallTable and copies in the date_sent column data from
* the messages database.
*
* Adds a new 'ringer' column to the CallTable setting each entry to NULL. This is safe since up
* to this point we were not using the table for group calls. This is effectively a replacement for
* the GroupCallRing table.
*
* Removes the 'NOT NULL' condition on message_id and peer, as with ad-hoc calling in place, these
* can now be null.
*/
object V182_CallTableMigration : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL(
"""
CREATE TABLE call_tmp (
_id INTEGER PRIMARY KEY,
call_id INTEGER NOT NULL UNIQUE,
message_id INTEGER DEFAULT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE SET NULL,
peer INTEGER DEFAULT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
type INTEGER NOT NULL,
direction INTEGER NOT NULL,
event INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
ringer INTEGER DEFAULT NULL,
deletion_timestamp INTEGER DEFAULT 0
)
""".trimIndent()
)
db.execSQL(
"""
INSERT INTO call_tmp
SELECT
_id,
call_id,
message_id,
peer,
type,
direction,
event,
(SELECT date_sent FROM message WHERE message._id = call.message_id) as timestamp,
NULL as ringer,
0 as deletion_timestamp
FROM call
""".trimIndent()
)
db.execSQL("DROP TABLE group_call_ring")
db.execSQL("DROP TABLE call")
db.execSQL("ALTER TABLE call_tmp RENAME TO call")
}
}

View file

@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.push.SignalServiceTrustStore;
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
import org.thoughtcrime.securesms.service.DeletedCallEventManager;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.ExpiringStoriesManager;
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
@ -112,6 +113,7 @@ public class ApplicationDependencies {
private static volatile ViewOnceMessageManager viewOnceMessageManager;
private static volatile ExpiringStoriesManager expiringStoriesManager;
private static volatile ExpiringMessageManager expiringMessageManager;
private static volatile DeletedCallEventManager deletedCallEventManager;
private static volatile Payments payments;
private static volatile SignalCallManager signalCallManager;
private static volatile ShakeToReport shakeToReport;
@ -430,6 +432,18 @@ public class ApplicationDependencies {
return expiringMessageManager;
}
public static @NonNull DeletedCallEventManager getDeletedCallEventManager() {
if (deletedCallEventManager == null) {
synchronized (LOCK) {
if (deletedCallEventManager == null) {
deletedCallEventManager = provider.provideDeletedCallEventManager();
}
}
}
return deletedCallEventManager;
}
public static @NonNull ScheduledMessageManager getScheduledMessageManager() {
if (scheduledMessagesManager == null) {
synchronized (LOCK) {
@ -691,6 +705,7 @@ public class ApplicationDependencies {
@NonNull ViewOnceMessageManager provideViewOnceMessageManager();
@NonNull ExpiringStoriesManager provideExpiringStoriesManager();
@NonNull ExpiringMessageManager provideExpiringMessageManager();
@NonNull DeletedCallEventManager provideDeletedCallEventManager();
@NonNull TypingStatusRepository provideTypingStatusRepository();
@NonNull TypingStatusSender provideTypingStatusSender();
@NonNull DatabaseObserver provideDatabaseObserver();

View file

@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.push.SecurityEventListener;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
import org.thoughtcrime.securesms.service.DeletedCallEventManager;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.ExpiringStoriesManager;
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
@ -225,6 +226,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
return new ExpiringMessageManager(context);
}
@Override
public @NonNull DeletedCallEventManager provideDeletedCallEventManager() {
return new DeletedCallEventManager(context);
}
@Override
public @NonNull ScheduledMessageManager provideScheduledMessageManager() {
return new ScheduledMessageManager(context);

View file

@ -0,0 +1,128 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallId
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JsonJobData
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.ringrtc.RemotePeer
import org.thoughtcrime.securesms.service.webrtc.CallEventSyncMessageUtil
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
import java.util.Optional
import java.util.concurrent.TimeUnit
/**
* Sends a sync event for the given call when the user first joins.
*/
class CallSyncEventJob private constructor(
parameters: Parameters,
private val conversationRecipientId: RecipientId,
private val callId: Long,
private val direction: CallTable.Direction,
private val event: CallTable.Event
) : BaseJob(parameters) {
companion object {
private val TAG = Log.tag(CallSyncEventJob::class.java)
const val KEY = "CallSyncEventJob"
private const val KEY_CALL_ID = "call_id"
private const val KEY_CONVERSATION_ID = "conversation_id"
private const val KEY_DIRECTION = "direction"
private const val KEY_EVENT = "event"
@JvmStatic
fun createForJoin(conversationRecipientId: RecipientId, callId: Long, isIncoming: Boolean): CallSyncEventJob {
return CallSyncEventJob(
getParameters(conversationRecipientId),
conversationRecipientId,
callId,
if (isIncoming) CallTable.Direction.INCOMING else CallTable.Direction.OUTGOING,
CallTable.Event.ACCEPTED
)
}
@JvmStatic
fun createForDelete(conversationRecipientId: RecipientId, callId: Long, isIncoming: Boolean): CallSyncEventJob {
return CallSyncEventJob(
getParameters(conversationRecipientId),
conversationRecipientId,
callId,
if (isIncoming) CallTable.Direction.INCOMING else CallTable.Direction.OUTGOING,
CallTable.Event.DELETE
)
}
@JvmStatic
fun enqueueDeleteSyncEvents(deletedCalls: Set<CallTable.Call>) {
for (call in deletedCalls) {
ApplicationDependencies.getJobManager().add(
createForDelete(
call.peer,
call.callId,
call.direction == CallTable.Direction.INCOMING
)
)
}
}
private fun getParameters(conversationRecipientId: RecipientId): Parameters {
return Parameters.Builder()
.setQueue(conversationRecipientId.toQueueKey())
.setLifespan(TimeUnit.MINUTES.toMillis(5))
.setMaxAttempts(3)
.addConstraint(NetworkConstraint.KEY)
.build()
}
}
override fun serialize(): ByteArray? {
return JsonJobData.Builder()
.putLong(KEY_CALL_ID, callId)
.putString(KEY_CONVERSATION_ID, conversationRecipientId.serialize())
.putInt(KEY_EVENT, CallTable.Event.serialize(event))
.putInt(KEY_DIRECTION, CallTable.Direction.serialize(direction))
.serialize()
}
override fun getFactoryKey(): String = KEY
override fun onFailure() = Unit
override fun onRun() {
val inputTimestamp = JsonJobData.deserialize(inputData).getLongOrDefault(GroupCallUpdateSendJob.KEY_SYNC_TIMESTAMP, System.currentTimeMillis())
val syncTimestamp = if (inputTimestamp == 0L) System.currentTimeMillis() else inputTimestamp
val syncMessage = CallEventSyncMessageUtil.createAcceptedSyncMessage(
RemotePeer(conversationRecipientId, CallId(callId)),
syncTimestamp,
direction == CallTable.Direction.OUTGOING,
true
)
try {
ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(syncMessage), Optional.empty())
} catch (e: Exception) {
Log.w(TAG, "Unable to send call event sync message for $callId", e)
}
}
override fun onShouldRetry(e: Exception): Boolean = false
class Factory : Job.Factory<CallSyncEventJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): CallSyncEventJob {
val data = JsonJobData.deserialize(serializedData)
return CallSyncEventJob(
parameters,
RecipientId.from(data.getString(KEY_CONVERSATION_ID)),
data.getLong(KEY_CALL_ID),
CallTable.Direction.deserialize(data.getInt(KEY_DIRECTION)),
CallTable.Event.deserialize(data.getInt(KEY_EVENT))
)
}
}
}

View file

@ -42,12 +42,15 @@ public class GroupCallUpdateSendJob extends BaseJob {
private static final String KEY_ERA_ID = "era_id";
private static final String KEY_RECIPIENTS = "recipients";
private static final String KEY_INITIAL_RECIPIENT_COUNT = "initial_recipient_count";
static final String KEY_SYNC_TIMESTAMP = "sync_timestamp";
private final RecipientId recipientId;
private final String eraId;
private final List<RecipientId> recipients;
private final int initialRecipientCount;
private long syncTimestamp;
@WorkerThread
public static @NonNull GroupCallUpdateSendJob create(@NonNull RecipientId recipientId, @Nullable String eraId) {
Recipient conversationRecipient = Recipient.resolved(recipientId);
@ -65,6 +68,7 @@ public class GroupCallUpdateSendJob extends BaseJob {
eraId,
recipientIds,
recipientIds.size(),
0L,
new Parameters.Builder()
.setQueue(conversationRecipient.getId().toQueueKey())
.setLifespan(TimeUnit.MINUTES.toMillis(5))
@ -76,6 +80,7 @@ public class GroupCallUpdateSendJob extends BaseJob {
@Nullable String eraId,
@NonNull List<RecipientId> recipients,
int initialRecipientCount,
long syncTimestamp,
@NonNull Parameters parameters)
{
super(parameters);
@ -84,6 +89,7 @@ public class GroupCallUpdateSendJob extends BaseJob {
this.eraId = eraId;
this.recipients = recipients;
this.initialRecipientCount = initialRecipientCount;
this.syncTimestamp = syncTimestamp;
}
@Override
@ -92,6 +98,7 @@ public class GroupCallUpdateSendJob extends BaseJob {
.putString(KEY_ERA_ID, eraId)
.putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients))
.putInt(KEY_INITIAL_RECIPIENT_COUNT, initialRecipientCount)
.putLong(KEY_SYNC_TIMESTAMP, syncTimestamp)
.serialize();
}
@ -125,6 +132,10 @@ public class GroupCallUpdateSendJob extends BaseJob {
Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying.");
throw new RetryLaterException();
}
setOutputData(new JsonJobData.Builder()
.putLong(KEY_SYNC_TIMESTAMP, syncTimestamp)
.serialize());
}
@Override
@ -166,6 +177,7 @@ public class GroupCallUpdateSendJob extends BaseJob {
if (includesSelf) {
results.add(ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(dataMessage));
syncTimestamp = dataMessage.getTimestamp();
}
return GroupSendJobHelper.getCompletedSends(destinations, results).completed;
@ -182,8 +194,9 @@ public class GroupCallUpdateSendJob extends BaseJob {
String eraId = data.getString(KEY_ERA_ID);
List<RecipientId> recipients = RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS));
int initialRecipientCount = data.getInt(KEY_INITIAL_RECIPIENT_COUNT);
long syncTimestamp = data.getLongOrDefault(KEY_SYNC_TIMESTAMP, 0L);
return new GroupCallUpdateSendJob(recipientId, eraId, recipients, initialRecipientCount, parameters);
return new GroupCallUpdateSendJob(recipientId, eraId, recipients, initialRecipientCount, syncTimestamp, parameters);
}
}
}

View file

@ -92,6 +92,7 @@ public final class JobManagerFactories {
put(AvatarGroupsV1DownloadJob.KEY, new AvatarGroupsV1DownloadJob.Factory());
put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory());
put(BoostReceiptRequestResponseJob.KEY, new BoostReceiptRequestResponseJob.Factory());
put(CallSyncEventJob.KEY, new CallSyncEventJob.Factory());
put(CheckServiceReachabilityJob.KEY, new CheckServiceReachabilityJob.Factory());
put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory());
put(ClearFallbackKbsEnclaveJob.KEY, new ClearFallbackKbsEnclaveJob.Factory());

View file

@ -934,7 +934,7 @@ object DataMessageProcessor {
val groupRecipientId = SignalDatabase.recipients.getOrInsertFromPossiblyMigratedGroupId(groupId)
SignalDatabase.messages.insertOrUpdateGroupCall(
SignalDatabase.calls.insertOrUpdateGroupCallFromExternalEvent(
groupRecipientId,
senderRecipientId,
envelope.serverTimestamp,

View file

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.messages;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@ -316,7 +315,7 @@ public class MessageContentProcessor {
else if (message.getGiftBadge().isPresent()) messageId = handleGiftMessage(content, message, senderRecipient, threadRecipient, receivedTime);
else if (isMediaMessage) messageId = handleMediaMessage(content, message, smsMessageId, senderRecipient, threadRecipient, receivedTime);
else if (message.getBody().isPresent()) messageId = handleTextMessage(content, message, smsMessageId, groupId, senderRecipient, threadRecipient, receivedTime);
else if (Build.VERSION.SDK_INT > 19 && message.getGroupCallUpdate().isPresent()) handleGroupCallUpdateMessage(content, message, groupId, senderRecipient);
else if (message.getGroupCallUpdate().isPresent()) handleGroupCallUpdateMessage(content, message, groupId, senderRecipient);
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
handleUnknownGroupMessage(content, message.getGroupContext().get(), senderRecipient);
@ -369,7 +368,14 @@ public class MessageContentProcessor {
else if (syncMessage.getOutgoingPaymentMessage().isPresent()) handleSynchronizeOutgoingPayment(content, syncMessage.getOutgoingPaymentMessage().get());
else if (syncMessage.getKeys().isPresent()) handleSynchronizeKeys(syncMessage.getKeys().get(), content.getTimestamp());
else if (syncMessage.getContacts().isPresent()) handleSynchronizeContacts(syncMessage.getContacts().get(), content.getTimestamp());
else if (syncMessage.getCallEvent().isPresent()) handleSynchronizeCallEvent(syncMessage.getCallEvent().get(), content.getTimestamp());
else if (syncMessage.getCallEvent().isPresent()) {
SyncMessage.CallEvent.Type type = syncMessage.getCallEvent().get().getType();
if (type == SyncMessage.CallEvent.Type.GROUP_CALL || type == SyncMessage.CallEvent.Type.AD_HOC_CALL) {
handleSynchronizeGroupOrAdHocCallEvent(syncMessage.getCallEvent().get(), content.getTimestamp());
} else {
handleSynchronizeCallEvent(syncMessage.getCallEvent().get(), content.getTimestamp());
}
}
else warn(String.valueOf(content.getTimestamp()), "Contains no known sync types...");
} else if (content.getCallMessage().isPresent()) {
log(String.valueOf(content.getTimestamp()), "Got call message...");
@ -753,7 +759,7 @@ public class MessageContentProcessor {
RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromPossiblyMigratedGroupId(groupId.get());
SignalDatabase.messages().insertOrUpdateGroupCall(groupRecipientId,
SignalDatabase.calls().insertOrUpdateGroupCallFromExternalEvent(groupRecipientId,
senderRecipient.getId(),
content.getServerReceivedTimestamp(),
message.getGroupCallUpdate().get().getEraId());
@ -1274,12 +1280,12 @@ public class MessageContentProcessor {
CallTable.Direction direction = CallTable.Direction.from(callEvent.getDirection());
CallTable.Event event = CallTable.Event.from(callEvent.getEvent());
if (timestamp == 0 || type == null || direction == null || event == null || !callEvent.hasPeerUuid()) {
warn(envelopeTimestamp, "Call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasPeerUuid());
if (timestamp == 0 || type == null || direction == null || event == null || !callEvent.hasConversationId()) {
warn(envelopeTimestamp, "Call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId());
return;
}
ServiceId serviceId = ServiceId.fromByteString(callEvent.getPeerUuid());
ServiceId serviceId = ServiceId.fromByteString(callEvent.getConversationId());
RecipientId recipientId = RecipientId.from(serviceId);
log(envelopeTimestamp, "Synchronize call event call: " + callId);
@ -1294,10 +1300,79 @@ public class MessageContentProcessor {
if (typeMismatch || directionMismatch || eventDowngrade || peerMismatch) {
warn(envelopeTimestamp, "Call event sync message is not valid for existing call record, ignoring. type: " + type + " direction: " + direction + " event: " + event + " peerMismatch: " + peerMismatch);
} else {
SignalDatabase.calls().updateCall(callId, event);
SignalDatabase.calls().updateOneToOneCall(callId, event);
}
} else {
SignalDatabase.calls().insertCall(callId, timestamp, recipientId, type, direction, event);
SignalDatabase.calls().insertOneToOneCall(callId, timestamp, recipientId, type, direction, event);
}
}
private void handleSynchronizeGroupOrAdHocCallEvent(@NonNull SyncMessage.CallEvent callEvent, long envelopeTimestamp)
throws BadGroupIdException
{
if (!callEvent.hasId()) {
log(envelopeTimestamp, "Synchronize group/ad-hoc call event missing call id, ignoring.");
return;
}
if (!FeatureFlags.adHocCalling() && callEvent.getType() == SyncMessage.CallEvent.Type.AD_HOC_CALL) {
log(envelopeTimestamp, "Ad-Hoc calling is not currently supported by this client, ignoring.");
return;
}
long callId = callEvent.getId();
long timestamp = callEvent.getTimestamp();
CallTable.Type type = CallTable.Type.from(callEvent.getType());
CallTable.Direction direction = CallTable.Direction.from(callEvent.getDirection());
CallTable.Event event = CallTable.Event.from(callEvent.getEvent());
if (timestamp == 0 || type == null || direction == null || event == null || !callEvent.hasConversationId()) {
warn(envelopeTimestamp, "Group/Ad-hoc call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId());
return;
}
CallTable.Call call = SignalDatabase.calls().getCallById(callId);
if (call != null) {
if (call.getType() != type) {
warn(envelopeTimestamp, "Group/Ad-hoc call event type mismatch, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId());
return;
}
switch (event) {
case DELETE:
SignalDatabase.calls().deleteGroupCall(call);
break;
case ACCEPTED:
if (call.getTimestamp() < callEvent.getTimestamp()) {
SignalDatabase.calls().setTimestamp(call.getCallId(), callEvent.getTimestamp());
}
if (callEvent.getDirection() == SyncMessage.CallEvent.Direction.INCOMING) {
SignalDatabase.calls().acceptIncomingGroupCall(call);
} else {
warn(envelopeTimestamp, "Invalid direction OUTGOING for event ACCEPTED");
}
break;
case NOT_ACCEPTED:
default:
warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId());
}
} else {
GroupId groupId = GroupId.push(callEvent.getConversationId().toByteArray());
RecipientId recipientId = Recipient.externalGroupExact(groupId).getId();
switch (event) {
case DELETE:
SignalDatabase.calls().insertDeletedGroupCallFromSyncEvent(callEvent.getId(), recipientId, direction, timestamp);
break;
case ACCEPTED:
SignalDatabase.calls().insertAcceptedGroupCall(callEvent.getId(), recipientId, direction, timestamp);
break;
case NOT_ACCEPTED:
default:
warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId());
}
}
}
@ -1334,7 +1409,7 @@ public class MessageContentProcessor {
} else if (dataMessage.isGroupV2Update()) {
handleSynchronizeSentGv2Update(content, message);
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
} else if (Build.VERSION.SDK_INT > 19 && dataMessage.getGroupCallUpdate().isPresent()) {
} else if (dataMessage.getGroupCallUpdate().isPresent()) {
handleGroupCallUpdateMessage(content, dataMessage, GroupUtil.idFromGroupContext(dataMessage.getGroupContext()), senderRecipient);
} else if (dataMessage.isEmptyGroupV2Message()) {
warn(content.getTimestamp(), "Empty GV2 message! Doing nothing.");

View file

@ -84,6 +84,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.IdentityUtil
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
@ -980,22 +981,30 @@ object SyncMessageProcessor {
private fun handleSynchronizeCallEvent(callEvent: SyncMessage.CallEvent, envelopeTimestamp: Long) {
if (!callEvent.hasId()) {
log(envelopeTimestamp, "Synchronize call event missing call id, ignoring.")
log(envelopeTimestamp, "Synchronize call event missing call id, ignoring. type: ${callEvent.type}")
return
}
if (callEvent.type == SyncMessage.CallEvent.Type.GROUP_CALL || callEvent.type == SyncMessage.CallEvent.Type.AD_HOC_CALL) {
handleSynchronizeGroupOrAdHocCallEvent(callEvent, envelopeTimestamp)
} else {
handleSynchronizeOneToOneCallEvent(callEvent, envelopeTimestamp)
}
}
private fun handleSynchronizeOneToOneCallEvent(callEvent: SyncMessage.CallEvent, envelopeTimestamp: Long) {
val callId: Long = callEvent.id
val timestamp: Long = callEvent.timestamp
val type: CallTable.Type? = CallTable.Type.from(callEvent.type)
val direction: CallTable.Direction? = CallTable.Direction.from(callEvent.direction)
val event: CallTable.Event? = CallTable.Event.from(callEvent.event)
if (timestamp == 0L || type == null || direction == null || event == null || !callEvent.hasPeerUuid()) {
warn(envelopeTimestamp, "Call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasPeerUuid())
if (timestamp == 0L || type == null || direction == null || event == null || !callEvent.hasConversationId()) {
warn(envelopeTimestamp, "Call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId())
return
}
val serviceId = ServiceId.fromByteString(callEvent.peerUuid)
val serviceId = ServiceId.fromByteString(callEvent.conversationId)
val recipientId = RecipientId.from(serviceId)
log(envelopeTimestamp, "Synchronize call event call: $callId")
@ -1010,10 +1019,62 @@ object SyncMessageProcessor {
if (typeMismatch || directionMismatch || eventDowngrade || peerMismatch) {
warn(envelopeTimestamp, "Call event sync message is not valid for existing call record, ignoring. type: $type direction: $direction event: $event peerMismatch: $peerMismatch")
} else {
SignalDatabase.calls.updateCall(callId, event)
SignalDatabase.calls.updateOneToOneCall(callId, event)
}
} else {
SignalDatabase.calls.insertCall(callId, timestamp, recipientId, type, direction, event)
SignalDatabase.calls.insertOneToOneCall(callId, timestamp, recipientId, type, direction, event)
}
}
@Throws(BadGroupIdException::class)
private fun handleSynchronizeGroupOrAdHocCallEvent(callEvent: SyncMessage.CallEvent, envelopeTimestamp: Long) {
if (!FeatureFlags.adHocCalling() && callEvent.type == SyncMessage.CallEvent.Type.AD_HOC_CALL) {
log(envelopeTimestamp, "Ad-Hoc calling is not currently supported by this client, ignoring.")
return
}
val callId: Long = callEvent.id
val timestamp: Long = callEvent.timestamp
val type: CallTable.Type? = CallTable.Type.from(callEvent.type)
val direction: CallTable.Direction? = CallTable.Direction.from(callEvent.direction)
val event: CallTable.Event? = CallTable.Event.from(callEvent.event)
if (timestamp == 0L || type == null || direction == null || event == null || !callEvent.hasConversationId()) {
warn(envelopeTimestamp, "Group/Ad-hoc call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId())
return
}
val call = SignalDatabase.calls.getCallById(callId)
if (call != null) {
if (call.type !== type) {
warn(envelopeTimestamp, "Group/Ad-hoc call event type mismatch, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId())
return
}
when (event) {
CallTable.Event.DELETE -> SignalDatabase.calls.deleteGroupCall(call)
CallTable.Event.ACCEPTED -> {
if (call.timestamp < callEvent.timestamp) {
SignalDatabase.calls.setTimestamp(call.callId, callEvent.timestamp)
}
if (callEvent.direction == SyncMessage.CallEvent.Direction.INCOMING) {
SignalDatabase.calls.acceptIncomingGroupCall(call)
} else {
warn(envelopeTimestamp, "Invalid direction OUTGOING for event ACCEPTED")
}
}
CallTable.Event.NOT_ACCEPTED -> warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId())
else -> warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId())
}
} else {
val groupId: GroupId = GroupId.push(callEvent.conversationId.toByteArray())
val recipientId = Recipient.externalGroupExact(groupId).id
when (event) {
CallTable.Event.DELETE -> SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(callEvent.id, recipientId, direction, timestamp)
CallTable.Event.ACCEPTED -> SignalDatabase.calls.insertAcceptedGroupCall(callEvent.id, recipientId, direction, timestamp)
CallTable.Event.NOT_ACCEPTED -> warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId())
else -> warn("Unsupported event type " + event + ". Ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasConversationId())
}
}
}
}

View file

@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.service
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.annotation.WorkerThread
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import java.util.concurrent.TimeUnit
/**
* Manages deleting call events 8 hours after they've been marked deleted.
*/
class DeletedCallEventManager(
application: Application
) : TimedEventManager<DeletedCallEventManager.Event>(application, "ExpiringCallEventsManager") {
companion object {
private val TAG = Log.tag(DeletedCallEventManager::class.java)
private val CALL_EVENT_DELETION_LIFESPAN = TimeUnit.HOURS.toMillis(8)
}
init {
scheduleIfNecessary()
}
@WorkerThread
override fun getNextClosestEvent(): Event? {
val oldestTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
if (oldestTimestamp <= 0) return null
val timeSinceSend = System.currentTimeMillis() - oldestTimestamp
val delay = (CALL_EVENT_DELETION_LIFESPAN - timeSinceSend).coerceAtLeast(0)
Log.i(TAG, "The oldest call event needs to be deleted in $delay ms.")
return Event(delay)
}
@WorkerThread
override fun executeEvent(event: Event) {
val threshold = System.currentTimeMillis() - CALL_EVENT_DELETION_LIFESPAN
val deletes = SignalDatabase.calls.deleteCallEventsDeletedBefore(threshold)
Log.i(TAG, "Deleted $deletes call events before $threshold")
}
@WorkerThread
override fun getDelayForEvent(event: Event): Long = event.delay
@WorkerThread
override fun scheduleAlarm(application: Application, event: Event, delay: Long) {
setAlarm(application, delay, DeleteCallEventsAlarm::class.java)
}
data class Event(val delay: Long)
class DeleteCallEventsAlarm : BroadcastReceiver() {
companion object {
private val TAG = Log.tag(DeleteCallEventsAlarm::class.java)
}
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(TAG, "onReceive()")
ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
}
}
}

View file

@ -1,6 +1,9 @@
package org.thoughtcrime.securesms.service.webrtc
import com.google.protobuf.ByteString
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.ringrtc.RemotePeer
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent
@ -10,27 +13,70 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMe
object CallEventSyncMessageUtil {
@JvmStatic
fun createAcceptedSyncMessage(remotePeer: RemotePeer, timestamp: Long, isOutgoing: Boolean, isVideoCall: Boolean): CallEvent {
return CallEvent
.newBuilder()
.setPeerUuid(Recipient.resolved(remotePeer.id).requireServiceId().toByteString())
.setId(remotePeer.callId.longValue())
.setTimestamp(timestamp)
.setType(if (isVideoCall) CallEvent.Type.VIDEO_CALL else CallEvent.Type.AUDIO_CALL)
.setDirection(if (isOutgoing) CallEvent.Direction.OUTGOING else CallEvent.Direction.INCOMING)
.setEvent(CallEvent.Event.ACCEPTED)
.build()
return createCallEvent(
remotePeer.id,
remotePeer.callId.longValue(),
timestamp,
isOutgoing,
isVideoCall,
CallEvent.Event.ACCEPTED
)
}
@JvmStatic
fun createNotAcceptedSyncMessage(remotePeer: RemotePeer, timestamp: Long, isOutgoing: Boolean, isVideoCall: Boolean): CallEvent {
return createCallEvent(
remotePeer.id,
remotePeer.callId.longValue(),
timestamp,
isOutgoing,
isVideoCall,
CallEvent.Event.NOT_ACCEPTED
)
}
@JvmStatic
fun createDeleteCallEvent(remotePeer: RemotePeer, timestamp: Long, isOutgoing: Boolean, isVideoCall: Boolean): CallEvent {
return createCallEvent(
remotePeer.id,
remotePeer.callId.longValue(),
timestamp,
isOutgoing,
isVideoCall,
CallEvent.Event.DELETE
)
}
private fun createCallEvent(
recipientId: RecipientId,
callId: Long,
timestamp: Long,
isOutgoing: Boolean,
isVideoCall: Boolean,
event: CallEvent.Event
): CallEvent {
val recipient = Recipient.resolved(recipientId)
val isGroupCall = recipient.isGroup
val conversationId: ByteString = if (isGroupCall) {
recipient.requireGroupId().decodedId.toProtoByteString()
} else {
recipient.requireServiceId().toByteString()
}
return CallEvent
.newBuilder()
.setPeerUuid(Recipient.resolved(remotePeer.id).requireServiceId().toByteString())
.setId(remotePeer.callId.longValue())
.setConversationId(conversationId)
.setId(callId)
.setTimestamp(timestamp)
.setType(if (isVideoCall) CallEvent.Type.VIDEO_CALL else CallEvent.Type.AUDIO_CALL)
.setType(
when {
isGroupCall -> CallEvent.Type.GROUP_CALL
isVideoCall -> CallEvent.Type.VIDEO_CALL
else -> CallEvent.Type.AUDIO_CALL
}
)
.setDirection(if (isOutgoing) CallEvent.Direction.OUTGOING else CallEvent.Direction.INCOMING)
.setEvent(CallEvent.Event.NOT_ACCEPTED)
.setEvent(event)
.build()
}
}

View file

@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
@ -148,8 +149,9 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
return currentState;
}
String eraId = WebRtcUtil.getGroupCallEraId(groupCall);
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), eraId);
boolean remoteUserRangTheCall = currentState.getCallSetupState(RemotePeer.GROUP_CALL_ID).getRingerRecipient() != Recipient.self();
String eraId = WebRtcUtil.getGroupCallEraId(groupCall);
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), eraId, remoteUserRangTheCall, true);
List<UUID> members = new ArrayList<>(peekInfo.getJoinedMembers());
if (!members.contains(SignalStore.account().requireAci().uuid())) {
@ -176,7 +178,7 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
}
String eraId = WebRtcUtil.getGroupCallEraId(groupCall);
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), eraId);
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), eraId, false, false);
List<UUID> members = Stream.of(currentState.getCallInfoState().getRemoteCallParticipants()).map(p -> p.getRecipient().requireServiceId().uuid()).toList();
webRtcInteractor.updateGroupCallUpdateMessage(currentState.getCallInfoState().getCallRecipient().getId(), eraId, members, false);

View file

@ -102,9 +102,9 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
}
if (ringUpdate != CallManager.RingUpdate.REQUESTED) {
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(ringId, System.currentTimeMillis(), ringUpdate);
SignalDatabase.calls().insertOrUpdateGroupCallFromRingState(ringId, remotePeerGroup.getId(), sender, System.currentTimeMillis(), ringUpdate);
return currentState;
} else if (SignalDatabase.groupCallRings().isCancelled(ringId)) {
} else if (SignalDatabase.calls().isRingCancelled(ringId)) {
try {
Log.i(TAG, "Incoming ring request for already cancelled ring: " + ringId);
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, null);
@ -118,7 +118,7 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
if (activeProfile != null && !(activeProfile.isRecipientAllowed(remotePeerGroup.getId()) || activeProfile.getAllowAllCalls())) {
try {
Log.i(TAG, "Incoming ring request for profile restricted recipient");
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(ringId, System.currentTimeMillis(), CallManager.RingUpdate.EXPIRED_REQUEST);
SignalDatabase.calls().insertOrUpdateGroupCallFromRingState(ringId, remotePeerGroup.getId(), sender, System.currentTimeMillis(), CallManager.RingUpdate.EXPIRED_REQUEST);
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, CallManager.RingCancelReason.DeclinedByUser);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring: " + ringId, e);
@ -135,7 +135,7 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
protected @NonNull WebRtcServiceState handleReceivedGroupCallPeekForRingingCheck(@NonNull WebRtcServiceState currentState, @NonNull GroupCallRingCheckInfo info, @NonNull PeekInfo peekInfo) {
Log.i(tag, "handleReceivedGroupCallPeekForRingingCheck(): recipient: " + info.getRecipientId() + " ring: " + info.getRingId());
if (SignalDatabase.groupCallRings().isCancelled(info.getRingId())) {
if (SignalDatabase.calls().isRingCancelled(info.getRingId())) {
try {
Log.i(TAG, "Ring was cancelled while getting peek info ring: " + info.getRingId());
webRtcInteractor.getCallManager().cancelGroupRing(info.getGroupId().getDecodedId(), info.getRingId(), null);
@ -147,11 +147,11 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
if (peekInfo.getDeviceCount() == 0) {
Log.i(TAG, "No one in the group call, mark as expired and do not ring");
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(info.getRingId(), System.currentTimeMillis(), CallManager.RingUpdate.EXPIRED_REQUEST);
SignalDatabase.calls().insertOrUpdateGroupCallFromRingState(info.getRingId(), info.getRecipientId(), info.getRingerUuid(), System.currentTimeMillis(), CallManager.RingUpdate.EXPIRED_REQUEST);
return currentState;
} else if (peekInfo.getJoinedMembers().contains(Recipient.self().requireServiceId().uuid())) {
Log.i(TAG, "We are already in the call, mark accepted on another device and do not ring");
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(info.getRingId(), System.currentTimeMillis(), CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE);
SignalDatabase.calls().insertOrUpdateGroupCallFromRingState(info.getRingId(), info.getRecipientId(), info.getRingerUuid(), System.currentTimeMillis(), CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE);
return currentState;
}

View file

@ -176,12 +176,12 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
activePeer.localRinging();
SignalDatabase.calls().insertCall(remotePeer.getCallId().longValue(),
System.currentTimeMillis(),
remotePeer.getId(),
SignalDatabase.calls().insertOneToOneCall(remotePeer.getCallId().longValue(),
System.currentTimeMillis(),
remotePeer.getId(),
currentState.getCallSetupState(activePeer).isRemoteVideoOffer() ? CallTable.Type.VIDEO_CALL : CallTable.Type.AUDIO_CALL,
CallTable.Direction.INCOMING,
CallTable.Event.ONGOING);
CallTable.Direction.INCOMING,
CallTable.Event.ONGOING);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.INTERACTIVE);

View file

@ -58,7 +58,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
boolean updateForCurrentRingId = ringId == currentState.getCallSetupState(RemotePeer.GROUP_CALL_ID).getRingId();
boolean isCurrentlyRinging = currentState.getCallInfoState().getGroupCallState().isRinging();
if (SignalDatabase.groupCallRings().isCancelled(ringId)) {
if (SignalDatabase.calls().isRingCancelled(ringId)) {
try {
Log.i(TAG, "Ignoring incoming ring request for already cancelled ring: " + ringId);
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, null);
@ -69,7 +69,11 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
}
if (ringUpdate != CallManager.RingUpdate.REQUESTED) {
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(ringId, System.currentTimeMillis(), ringUpdate);
SignalDatabase.calls().insertOrUpdateGroupCallFromRingState(ringId,
remotePeerGroup.getId(),
sender,
System.currentTimeMillis(),
ringUpdate);
if (updateForCurrentRingId && isCurrentlyRinging) {
Log.i(TAG, "Cancelling current ring: " + ringId);
@ -104,7 +108,14 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
Log.i(TAG, "Requesting new ring: " + ringId);
SignalDatabase.groupCallRings().insertGroupRing(ringId, System.currentTimeMillis(), ringUpdate);
Recipient ringerRecipient = Recipient.externalPush(ServiceId.from(sender));
SignalDatabase.calls().insertOrUpdateGroupCallFromRingState(
ringId,
remotePeerGroup.getId(),
ringerRecipient.getId(),
System.currentTimeMillis(),
ringUpdate
);
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState, RemotePeer.GROUP_CALL_ID.longValue());
@ -138,7 +149,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
.changeCallSetupState(RemotePeer.GROUP_CALL_ID)
.isRemoteVideoOffer(true)
.ringId(ringId)
.ringerRecipient(Recipient.externalPush(ServiceId.from(sender)))
.ringerRecipient(ringerRecipient)
.commit()
.changeCallInfoState()
.activePeer(new RemotePeer(currentState.getCallInfoState().getCallRecipient().getId(), RemotePeer.GROUP_CALL_ID))
@ -226,10 +237,13 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
Optional<GroupId> groupId = recipient.getGroupId();
long ringId = currentState.getCallSetupState(RemotePeer.GROUP_CALL_ID).getRingId();
Recipient ringer = currentState.getCallSetupState(RemotePeer.GROUP_CALL_ID).getRingerRecipient();
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(ringId,
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE);
SignalDatabase.calls().insertOrUpdateGroupCallFromRingState(ringId,
recipient.getId(),
ringer.getId(),
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE);
try {
webRtcInteractor.getCallManager().cancelGroupRing(groupId.get().getDecodedId(),

View file

@ -85,12 +85,12 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, Recipient.resolved(remotePeer.getId()), SignalDatabase.threads().getThreadIdIfExistsFor(remotePeer.getId()));
SignalDatabase.calls().insertCall(remotePeer.getCallId().longValue(),
System.currentTimeMillis(),
remotePeer.getId(),
SignalDatabase.calls().insertOneToOneCall(remotePeer.getCallId().longValue(),
System.currentTimeMillis(),
remotePeer.getId(),
isVideoCall ? CallTable.Type.VIDEO_CALL : CallTable.Type.AUDIO_CALL,
CallTable.Direction.OUTGOING,
CallTable.Event.ONGOING);
CallTable.Direction.OUTGOING,
CallTable.Event.ONGOING);
EglBaseWrapper.replaceHolder(EglBaseWrapper.OUTGOING_PLACEHOLDER, remotePeer.getCallId().longValue());

View file

@ -39,6 +39,8 @@ import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.CallSyncEventJob;
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@ -349,8 +351,8 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
Long threadId = SignalDatabase.threads().getThreadIdFor(group.getId());
if (threadId != null) {
SignalDatabase.messages()
.updatePreviousGroupCall(threadId,
SignalDatabase.calls()
.updateGroupCallFromPeek(threadId,
peekInfo.getEraId(),
peekInfo.getJoinedMembers(),
WebRtcUtil.isCallFull(peekInfo));
@ -831,25 +833,25 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
public void insertMissedCall(@NonNull RemotePeer remotePeer, long timestamp, boolean isVideoOffer) {
CallTable.Call call = SignalDatabase.calls()
.updateCall(remotePeer.getCallId().longValue(), CallTable.Event.MISSED);
.updateOneToOneCall(remotePeer.getCallId().longValue(), CallTable.Event.MISSED);
if (call == null) {
CallTable.Type type = isVideoOffer ? CallTable.Type.VIDEO_CALL : CallTable.Type.AUDIO_CALL;
SignalDatabase.calls()
.insertCall(remotePeer.getCallId().longValue(), timestamp, remotePeer.getId(), type, CallTable.Direction.INCOMING, CallTable.Event.MISSED);
.insertOneToOneCall(remotePeer.getCallId().longValue(), timestamp, remotePeer.getId(), type, CallTable.Direction.INCOMING, CallTable.Event.MISSED);
}
}
public void insertReceivedCall(@NonNull RemotePeer remotePeer, boolean isVideoOffer) {
CallTable.Call call = SignalDatabase.calls()
.updateCall(remotePeer.getCallId().longValue(), CallTable.Event.ACCEPTED);
.updateOneToOneCall(remotePeer.getCallId().longValue(), CallTable.Event.ACCEPTED);
if (call == null) {
CallTable.Type type = isVideoOffer ? CallTable.Type.VIDEO_CALL : CallTable.Type.AUDIO_CALL;
SignalDatabase.calls()
.insertCall(remotePeer.getCallId().longValue(), System.currentTimeMillis(), remotePeer.getId(), type, CallTable.Direction.INCOMING, CallTable.Event.ACCEPTED);
.insertOneToOneCall(remotePeer.getCallId().longValue(), System.currentTimeMillis(), remotePeer.getId(), type, CallTable.Direction.INCOMING, CallTable.Event.ACCEPTED);
}
}
@ -887,17 +889,32 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
});
}
public void sendGroupCallUpdateMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId) {
SignalExecutors.BOUNDED.execute(() -> ApplicationDependencies.getJobManager().add(GroupCallUpdateSendJob.create(recipient.getId(), groupCallEraId)));
public void sendGroupCallUpdateMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId, boolean isIncoming, boolean isJoinEvent) {
SignalExecutors.BOUNDED.execute(() -> {
GroupCallUpdateSendJob updateSendJob = GroupCallUpdateSendJob.create(recipient.getId(), groupCallEraId);
JobManager.Chain chain = ApplicationDependencies.getJobManager().startChain(updateSendJob);
if (isJoinEvent && groupCallEraId != null) {
chain.then(CallSyncEventJob.createForJoin(
recipient.getId(),
CallId.fromEra(groupCallEraId).longValue(),
isIncoming
));
} else if (isJoinEvent) {
Log.w(TAG, "Can't send join event sync message without an era id.");
}
chain.enqueue();
});
}
public void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection<UUID> joinedMembers, boolean isCallFull) {
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.messages().insertOrUpdateGroupCall(groupId,
Recipient.self().getId(),
System.currentTimeMillis(),
groupCallEraId,
joinedMembers,
isCallFull));
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.calls().insertOrUpdateGroupCallFromLocalEvent(groupId,
Recipient.self().getId(),
System.currentTimeMillis(),
groupCallEraId,
joinedMembers,
isCallFull));
}
public void sendCallMessage(@NonNull final RemotePeer remotePeer,
@ -935,7 +952,7 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
public void sendAcceptedCallEventSyncMessage(@NonNull RemotePeer remotePeer, boolean isOutgoing, boolean isVideoCall) {
SignalDatabase
.calls()
.updateCall(remotePeer.getCallId().longValue(), CallTable.Event.ACCEPTED);
.updateOneToOneCall(remotePeer.getCallId().longValue(), CallTable.Event.ACCEPTED);
if (TextSecurePreferences.isMultiDevice(context)) {
networkExecutor.execute(() -> {
@ -952,7 +969,7 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
public void sendNotAcceptedCallEventSyncMessage(@NonNull RemotePeer remotePeer, boolean isOutgoing, boolean isVideoCall) {
SignalDatabase
.calls()
.updateCall(remotePeer.getCallId().longValue(), CallTable.Event.NOT_ACCEPTED);
.updateOneToOneCall(remotePeer.getCallId().longValue(), CallTable.Event.NOT_ACCEPTED);
if (TextSecurePreferences.isMultiDevice(context)) {
networkExecutor.execute(() -> {

View file

@ -54,6 +54,7 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.Collection;
import java.util.List;
@ -798,9 +799,11 @@ public abstract class WebRtcActionProcessor {
if (ringUpdate != RingUpdate.BUSY_LOCALLY && ringUpdate != RingUpdate.BUSY_ON_ANOTHER_DEVICE) {
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, CallManager.RingCancelReason.Busy);
}
SignalDatabase.groupCallRings().insertOrUpdateGroupRing(ringId,
System.currentTimeMillis(),
ringUpdate == RingUpdate.REQUESTED ? RingUpdate.BUSY_LOCALLY : ringUpdate);
SignalDatabase.calls().insertOrUpdateGroupCallFromRingState(ringId,
remotePeerGroup.getId(),
sender,
System.currentTimeMillis(),
ringUpdate == RingUpdate.REQUESTED ? RingUpdate.BUSY_LOCALLY : ringUpdate);
} catch (CallException e) {
Log.w(tag, "Unable to cancel ring", e);
}
@ -826,7 +829,7 @@ public abstract class WebRtcActionProcessor {
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
if (recipient != null && currentState.getCallInfoState().getGroupCallState().isConnected()) {
webRtcInteractor.sendGroupCallMessage(recipient, WebRtcUtil.getGroupCallEraId(groupCall));
webRtcInteractor.sendGroupCallMessage(recipient, WebRtcUtil.getGroupCallEraId(groupCall), false, false);
}
currentState = currentState.builder()

View file

@ -84,8 +84,8 @@ public class WebRtcInteractor {
signalCallManager.sendCallMessage(remotePeer, callMessage);
}
void sendGroupCallMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId) {
signalCallManager.sendGroupCallUpdateMessage(recipient, groupCallEraId);
void sendGroupCallMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId, boolean isIncoming, boolean isJoinEvent) {
signalCallManager.sendGroupCallUpdateMessage(recipient, groupCallEraId, isIncoming, isJoinEvent);
}
void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection<UUID> joinedMembers, boolean isCallFull) {

View file

@ -109,6 +109,7 @@ public final class FeatureFlags {
private static final String CALLS_TAB = "android.calls.tab";
private static final String TEXT_FORMATTING_SPOILER_SEND = "android.textFormatting.spoilerSend";
private static final String EXPORT_ACCOUNT_DATA = "android.exportAccountData";
private static final String AD_HOC_CALLING = "android.calling.ad.hoc";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -173,7 +174,8 @@ public final class FeatureFlags {
@VisibleForTesting
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(
PHONE_NUMBER_PRIVACY
PHONE_NUMBER_PRIVACY,
AD_HOC_CALLING
);
/**
@ -612,6 +614,13 @@ public final class FeatureFlags {
return getBoolean(EXPORT_ACCOUNT_DATA, false);
}
/**
* Whether or not ad-hoc calling is enabled
*/
public static boolean adHocCalling() {
return getBoolean(AD_HOC_CALLING, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View file

@ -5830,5 +5830,13 @@
<!-- Accessibility label describing the continue button on the camera screen -->
<string name="CameraControls_continue_button_accessibility_label">Continue Button</string>
<!-- CallPreference -->
<!-- Missed group call in call info -->
<string name="CallPreference__missed_group_call">Missed group call</string>
<!-- Incoming group call in call info -->
<string name="CallPreference__incoming_group_call">Incoming group call</string>
<!-- Outgoing group call in call info -->
<string name="CallPreference__outgoing_group_call">Outgoing group call</string>
<!-- EOF -->
</resources>

View file

@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.payments.Payments;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
import org.thoughtcrime.securesms.service.DeletedCallEventManager;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.ExpiringStoriesManager;
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
@ -136,6 +137,11 @@ public class MockApplicationDependencyProvider implements ApplicationDependencie
return null;
}
@Override
public @NonNull DeletedCallEventManager provideDeletedCallEventManager() {
return null;
}
@Override
public @NonNull TypingStatusRepository provideTypingStatusRepository() {
return null;

View file

@ -607,6 +607,8 @@ message SyncMessage {
UNKNOWN_TYPE = 0;
AUDIO_CALL = 1;
VIDEO_CALL = 2;
GROUP_CALL = 3;
AD_HOC_CALL = 4;
}
enum Direction {
@ -619,14 +621,15 @@ message SyncMessage {
UNKNOWN_ACTION = 0;
ACCEPTED = 1;
NOT_ACCEPTED = 2;
DELETE = 3;
}
optional bytes peerUuid = 1;
optional uint64 id = 2;
optional uint64 timestamp = 3;
optional Type type = 4;
optional Direction direction = 5;
optional Event event = 6;
optional bytes conversationId = 1;
optional uint64 id = 2;
optional uint64 timestamp = 3;
optional Type type = 4;
optional Direction direction = 5;
optional Event event = 6;
}
optional Sent sent = 1;