diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallTableTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallTableTest.kt index 069228f73c..963ee9c499 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallTableTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/CallTableTest.kt @@ -10,9 +10,14 @@ import org.junit.Test import org.junit.runner.RunWith import org.signal.ringrtc.CallId import org.signal.ringrtc.CallManager +import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testing.Flag +import org.thoughtcrime.securesms.testing.RemoteConfigForTest import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.testing.TestRemoteConfigFlag +@RemoteConfigForTest(flags = [Flag(TestRemoteConfigFlag.DISAPPEAR_MORE, "true")]) @RunWith(AndroidJUnit4::class) class CallTableTest { @@ -977,6 +982,80 @@ class CallTableTest { // assertEquals(0, allCallEvents.size) } + @Test + fun givenAMissedOneToOneCall_whenIMarkAllCallEventsRead_thenTimerShouldStart() { + val callId = 1L + val peer = harness.others[0] + insertExpiringThread(peer) + + SignalDatabase.calls.insertOneToOneCall(callId, System.currentTimeMillis(), peer, CallTable.Type.AUDIO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED) + + val readAt = System.currentTimeMillis() + SignalDatabase.calls.markAllCallEventsRead(readAt = readAt) + + val call = SignalDatabase.calls.getCallById(callId, peer) + assertEquals(readAt, SignalDatabase.messages.getMessageRecord(call!!.messageId!!).expireStarted) + } + + @Test + fun givenAMissedNotificationProfileOneToOneCall_whenIMarkAllCallEventsRead_thenTimerShouldStart() { + val callId = 1L + val peer = harness.others[0] + insertExpiringThread(peer) + + SignalDatabase.calls.insertOneToOneCall(callId, System.currentTimeMillis(), peer, CallTable.Type.AUDIO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED_NOTIFICATION_PROFILE) + + val readAt = System.currentTimeMillis() + SignalDatabase.calls.markAllCallEventsRead(readAt = readAt) + + val call = SignalDatabase.calls.getCallById(callId, peer) + assertEquals(readAt, SignalDatabase.messages.getMessageRecord(call!!.messageId!!).expireStarted) + } + + @Test + fun givenAMissedGroupCall_whenIMarkAllCallEventsRead_thenTimerShouldStart() { + val callId = 1L + SignalDatabase.recipients.setExpireMessagesForGroup(groupRecipientId, 60) + + SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(callId, groupRecipientId, harness.others[1], System.currentTimeMillis(), CallManager.RingUpdate.EXPIRED_REQUEST) + + val readAt = System.currentTimeMillis() + SignalDatabase.calls.markAllCallEventsRead(readAt = readAt) + + val call = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertEquals(readAt, SignalDatabase.messages.getMessageRecord(call!!.messageId!!).expireStarted) + } + + @Test + fun givenAMissedNotificationProfileGroupCall_whenIMarkAllCallEventsRead_thenTimerShouldStart() { + val callId = 1L + val ringerAci = Recipient.resolved(harness.others[1]).requireAci() + + SignalDatabase.recipients.setExpireMessagesForGroup(groupRecipientId, 60) + + SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(callId, groupRecipientId, ringerAci, System.currentTimeMillis(), CallManager.RingUpdate.EXPIRED_REQUEST, true) + + val readAt = System.currentTimeMillis() + SignalDatabase.calls.markAllCallEventsRead(readAt = readAt) + + val call = SignalDatabase.calls.getCallById(callId, groupRecipientId) + assertEquals(readAt, SignalDatabase.messages.getMessageRecord(call!!.messageId!!).expireStarted) + } + + @Test + fun givenAnOutgoingOneToOneCallFromSync_whenInserted_thenTimerIsStarted() { + val callId = 1L + val peer = harness.others[0] + val timestamp = System.currentTimeMillis() + + insertExpiringThread(peer) + SignalDatabase.calls.insertOneToOneCall(callId, timestamp, peer, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.OUTGOING_RING, true) + + val call = SignalDatabase.calls.getCallById(callId, peer) + val message = SignalDatabase.messages.getMessageRecord(call!!.messageId!!) + assertNotEquals(0L, message.expireStarted) + } + private fun insertTwoCallEvents() { SignalDatabase.calls.insertAcceptedGroupCall( 1, @@ -992,4 +1071,10 @@ class CallTableTest { 2000 ) } + + private fun insertExpiringThread(recipientId: RecipientId) { + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId)) + MmsHelper.insert(recipient = Recipient.resolved(recipientId), expiresIn = 30_000L, threadId = threadId) + SignalDatabase.threads.update(threadId, false) + } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/RemoteConfigForTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/RemoteConfigForTest.kt index f670d85b6a..2f67eaf8a8 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/RemoteConfigForTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/RemoteConfigForTest.kt @@ -46,7 +46,8 @@ annotation class RawFlag(val key: String, val value: String) */ enum class TestRemoteConfigFlag(private val property: KProperty0<*>) { INTERNAL_USER(RemoteConfig::internalUser), - DEFAULT_MAX_BACKOFF(RemoteConfig::defaultMaxBackoff); + DEFAULT_MAX_BACKOFF(RemoteConfig::defaultMaxBackoff), + DISAPPEAR_MORE(RemoteConfig::disappearMore); val key: String get() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt index 09d571a641..98ec127de1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt @@ -109,13 +109,11 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl "CREATE INDEX $CALL_LOG_INDEX ON $TABLE_NAME ($TIMESTAMP, $PEER, $EVENT, $TYPE, $DELETION_TIMESTAMP)" ) } - fun markAllCallEventsRead(timestamp: Long = Long.MAX_VALUE) { - val proposedExpireStarted = if (timestamp == Long.MAX_VALUE) System.currentTimeMillis() else timestamp - + fun markAllCallEventsRead(timestamp: Long = Long.MAX_VALUE, readAt: Long = System.currentTimeMillis()) { val allUnreadMissedCalls = readableDatabase .select(MESSAGE_ID) .from(TABLE_NAME) - .where("$TIMESTAMP <= ? AND $READ != ? AND $EVENT = ?", timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED)) + .where("$TIMESTAMP <= ? AND $READ != ? AND ($EVENT = ? OR $EVENT = ?)", timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED), Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)) .run() .readToList { cursor -> cursor.requireLong(MESSAGE_ID) @@ -131,9 +129,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl if (expiringCalls.isNotEmpty()) { Log.i(TAG, "Found ${expiringCalls.size} calls that needs expiring.") - SignalDatabase.messages.markExpireStarted(expiringCalls.map { it.key to proposedExpireStarted }) + SignalDatabase.messages.markExpireStarted(expiringCalls.map { it.key to readAt }) for ((messageId, expiresIn) in expiringCalls) { - AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, proposedExpireStarted, expiresIn) + AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, readAt, expiresIn) } } @@ -142,14 +140,13 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl } } - fun markAllCallEventsWithPeerBeforeTimestampRead(peer: RecipientId, timestamp: Long): Call? { - val proposedExpireStarted = if (timestamp == Long.MAX_VALUE) System.currentTimeMillis() else timestamp + fun markAllCallEventsWithPeerBeforeTimestampRead(peer: RecipientId, timestamp: Long, readAt: Long = System.currentTimeMillis()): Call? { val latestCallAsOfTimestamp = writableDatabase.withinTransaction { db -> val unreadMissedCalls = db .select(MESSAGE_ID) .from(TABLE_NAME) - .where("$PEER = ? AND $TIMESTAMP <= ? AND $READ != ? AND $EVENT = ? AND $GROUP_CALL_ACTIVE = 0", peer.toLong(), timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED)) + .where("$PEER = ? AND $TIMESTAMP <= ? AND $READ != ? AND ($EVENT = ? OR $EVENT = ?) AND $GROUP_CALL_ACTIVE = 0", peer.toLong(), timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED), Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)) .run() .readToList { cursor -> cursor.requireLong(MESSAGE_ID) @@ -164,9 +161,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl if (expiring.isNotEmpty()) { Log.i(TAG, "Found ${expiring.size} calls that needs expiring.") - SignalDatabase.messages.markExpireStarted(expiring.map { it.key to proposedExpireStarted }) + SignalDatabase.messages.markExpireStarted(expiring.map { it.key to readAt }) for ((messageId, expiresIn) in expiring) { - AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, proposedExpireStarted, expiresIn) + AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, readAt, expiresIn) } } @@ -191,7 +188,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl return readableDatabase .count() .from(TABLE_NAME) - .where("$EVENT = ? AND $READ = ?", Event.serialize(Event.MISSED), ReadState.serialize(ReadState.UNREAD)) + .where("($EVENT = ? OR $EVENT = ?) AND $READ = ?", Event.serialize(Event.MISSED), Event.serialize(Event.MISSED_NOTIFICATION_PROFILE), ReadState.serialize(ReadState.UNREAD)) .run() .readToSingleLong() } @@ -1057,7 +1054,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> { when (call.event) { - Event.RINGING, Event.MISSED, Event.MISSED_NOTIFICATION_PROFILE, Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.DECLINED) + Event.RINGING, Event.MISSED, Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.DECLINED) Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED) Event.OUTGOING_RING -> Log.w(TAG, "Received DECLINED_ON_ANOTHER_DEVICE while in OUTGOING_RING state.") else -> Unit diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index abe9bf7c48..6138d8a717 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -915,6 +915,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val threadId = threadIdResult.threadId val dateReceived = System.currentTimeMillis() val expiresIn = if (RemoteConfig.disappearMore) threads.getExpiresIn(threadId) else 0 + val missed = MessageTypes.isMissedAudioCall(type) || MessageTypes.isMissedVideoCall(type) val values = contentValuesOf( FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else recipientId.serialize(), @@ -923,7 +924,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat DATE_RECEIVED to dateReceived, DATE_SENT to timestamp, READ to 1, - NOTIFIED to if (MessageTypes.isMissedAudioCall(type) || MessageTypes.isMissedVideoCall(type)) 0 else 1, + NOTIFIED to if (missed) 0 else 1, TYPE to type, THREAD_ID to threadId, EXPIRES_IN to expiresIn @@ -938,8 +939,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat notifyConversationListeners(threadId) TrimThreadJob.enqueueAsync(threadId) - // If inserting an outgoing call from a sync message, automatically start timer - if (expiresIn != 0L && outgoing && fromSync) { + // If inserting a call from a sync message, automatically start timer unless it was missed + if (expiresIn != 0L && !missed && fromSync) { Log.i(TAG, "Starting expiration timer after inserting a call from a sync message.") markExpireStarted(messageId, timestamp) AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, timestamp, expiresIn) @@ -997,7 +998,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val expiresIn = if (RemoteConfig.disappearMore) recipient.expiresInSeconds.seconds.inWholeMilliseconds else 0 val messageId: MessageId = writableDatabase.withinTransaction { db -> val self = Recipient.self() - val markRead = joinedUuids.contains(self.requireServiceId().rawUuid) || self.id == sender + val selfCreated = self.id == sender + val markRead = joinedUuids.contains(self.requireServiceId().rawUuid) || selfCreated val updateDetails: ByteArray = GroupCallUpdateDetails( eraId = eraId, startedCallUuid = Recipient.resolved(sender).requireServiceId().toString(), @@ -1024,8 +1026,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val messageId = MessageId(db.insert(TABLE_NAME, null, values)) - val isActiveCall = joinedUuids.isNotEmpty() || isIncomingGroupCallRingingOnLocalDevice - if (!isActiveCall) { + // Calls ringing from a linked (not local) device could be active + val isPotentialActiveCall = joinedUuids.isNotEmpty() || isIncomingGroupCallRingingOnLocalDevice || selfCreated + if (!isPotentialActiveCall) { maybeCollapseMessage(db = db, messageId = messageId.id, threadId = threadId, dateReceived = timestamp, messageExtras = null, messageType = MessageTypes.GROUP_CALL_TYPE) if (markRead && expiresIn != 0L) { Log.d(TAG, "[insertGroupCall] Starting expiration timer for group call.") @@ -1122,6 +1125,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body) val containsSelf = joinedUuids.contains(SignalStore.account.requireAci().rawUuid) + val selfCreated = updateDetail.startedCallUuid == SignalStore.account.requireAci().rawUuid.toString() // Treat empty eraId from ring requests as matching for updating val sameEraId = (updateDetail.eraId == eraId || updateDetail.eraId.isEmpty()) && !Util.isEmpty(eraId) val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList() @@ -1140,7 +1144,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val updated = db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0 if (inCallUuids.isEmpty()) { - val acknowledgedCall = localJoined || event == Event.DECLINED + val acknowledgedCall = localJoined || event == Event.DECLINED || selfCreated finalizeEndedGroupCallMessage(db, message, acknowledgedCall, logPrefix = "[updateGroupCall]") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index 40e54a00e2..870f96888b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -1376,7 +1376,7 @@ object SyncMessageProcessor { } CallLogEvent.Type.MARKED_AS_READ -> { - SignalDatabase.calls.markAllCallEventsRead(timestamp) + SignalDatabase.calls.markAllCallEventsRead(timestamp, envelopeTimestamp) } CallLogEvent.Type.MARKED_AS_READ_IN_CONVERSATION -> { @@ -1385,7 +1385,7 @@ object SyncMessageProcessor { return } - SignalDatabase.calls.markAllCallEventsWithPeerBeforeTimestampRead(peer, timestamp) + SignalDatabase.calls.markAllCallEventsWithPeerBeforeTimestampRead(peer, timestamp, envelopeTimestamp) } else -> log(envelopeTimestamp, "Synchronize call log event has an invalid type $eventType, ignoring.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index e57af4513f..1f92e2487c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -110,7 +110,7 @@ public class MarkReadReceiver extends BroadcastReceiver { .collect(Collectors.toList())); for (RecipientId peer : peers) { - CallTable.Call lastCallInThread = SignalDatabase.calls().markAllCallEventsWithPeerBeforeTimestampRead(peer, timestamp); + CallTable.Call lastCallInThread = SignalDatabase.calls().markAllCallEventsWithPeerBeforeTimestampRead(peer, timestamp, System.currentTimeMillis()); if (lastCallInThread != null) { AppDependencies.getJobManager().add(CallLogEventSendJob.forMarkedAsReadInConversation(lastCallInThread)); }