From e60b32202efeb17b178f7307e3f62604dfcaccee Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 25 Apr 2024 10:18:01 -0300 Subject: [PATCH] Improved missed call state handling. --- .../conversation/MarkReadHelper.java | 2 ++ .../securesms/database/CallTable.kt | 24 +++++++++++++++++ .../securesms/database/MessageTable.kt | 20 +++++++++++++- .../helpers/SignalDatabaseMigrations.kt | 6 +++-- .../V229_MarkMissedCallEventsNotified.kt | 26 +++++++++++++++++++ .../securesms/jobs/CallLogEventSendJob.kt | 19 ++++++++++++++ .../messages/SyncMessageProcessor.kt | 15 ++++++++--- .../notifications/MarkReadReceiver.java | 18 ++++++++++++- .../v2/NotificationStateProvider.kt | 3 ++- .../src/main/protowire/SignalService.proto | 5 ++-- 10 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V229_MarkMissedCallEventsNotified.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java index a9a85ba131..b2d747cca3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.Executor; @@ -67,6 +68,7 @@ public class MarkReadHelper { ApplicationDependencies.getMessageNotifier().updateNotification(context); MarkReadReceiver.process(infos); + MarkReadReceiver.processCallEvents(Collections.singletonList(conversationId), timestamp); }); }); } 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 3e06fed7a1..3438ab5312 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt @@ -118,6 +118,30 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl notifyConversationListListeners() } + fun markAllCallEventsWithPeerBeforeTimestampRead(peer: RecipientId, timestamp: Long): Call? { + val latestCallAsOfTimestamp = writableDatabase.withinTransaction { db -> + val updated = db.update(TABLE_NAME) + .values(READ to ReadState.serialize(ReadState.READ)) + .where("$PEER = ? AND $TIMESTAMP <= ?", peer.toLong(), timestamp) + .run() + + if (updated == 0) { + null + } else { + db.select() + .from(TABLE_NAME) + .where("$PEER = ? AND $TIMESTAMP <= ?", peer.toLong(), timestamp) + .orderBy("$TIMESTAMP DESC") + .limit(1) + .run() + .readToSingleObject(Call.Deserializer) + } + } + + notifyConversationListListeners() + return latestCallAsOfTimestamp + } + fun getUnreadMissedCallCount(): Long { return readableDatabase .count() 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 e856df7c80..97f088fbe8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -433,6 +433,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat ($TYPE = ${MessageTypes.GROUP_CALL_TYPE}) )""" + private const val IS_MISSED_CALL_TYPE_CLAUSE = """( + ($TYPE = ${MessageTypes.MISSED_AUDIO_CALL_TYPE}) + OR + ($TYPE = ${MessageTypes.MISSED_VIDEO_CALL_TYPE}) + )""" + private val outgoingTypeClause: String by lazy { MessageTypes.OUTGOING_MESSAGE_TYPES .map { "($TABLE_NAME.$TYPE & ${MessageTypes.BASE_TYPE_MASK} = $it)" } @@ -4647,7 +4653,19 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat return readableDatabase .select(*MMS_PROJECTION) .from(TABLE_NAME) - .where("$NOTIFIED = 0 AND $STORY_TYPE = 0 AND $LATEST_REVISION_ID IS NULL AND ($READ = 0 OR $REACTIONS_UNREAD = 1 ${if (stickyQuery.isNotEmpty()) "OR ($stickyQuery)" else ""})") + .where( + """ + $NOTIFIED = 0 + AND $STORY_TYPE = 0 + AND $LATEST_REVISION_ID IS NULL + AND ( + $READ = 0 + OR $REACTIONS_UNREAD = 1 + ${if (stickyQuery.isNotEmpty()) "OR ($stickyQuery)" else ""} + OR ($IS_MISSED_CALL_TYPE_CLAUSE AND EXISTS (SELECT 1 FROM ${CallTable.TABLE_NAME} WHERE ${CallTable.MESSAGE_ID} = $TABLE_NAME.$ID AND ${CallTable.EVENT} = ${CallTable.Event.serialize(CallTable.Event.MISSED)} AND ${CallTable.READ} = 0)) + ) + """.trimIndent() + ) .orderBy("$DATE_RECEIVED ASC") .run() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 4f8fbc0f93..27ba81ff5d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V225_AddLocalUserJo import org.thoughtcrime.securesms.database.helpers.migration.V226_AddAttachmentMediaIdIndex import org.thoughtcrime.securesms.database.helpers.migration.V227_AddAttachmentArchiveTransferState import org.thoughtcrime.securesms.database.helpers.migration.V228_AddNameCollisionTables +import org.thoughtcrime.securesms.database.helpers.migration.V229_MarkMissedCallEventsNotified /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -174,10 +175,11 @@ object SignalDatabaseMigrations { 225 to V225_AddLocalUserJoinedStateAndGroupCallActiveState, 226 to V226_AddAttachmentMediaIdIndex, 227 to V227_AddAttachmentArchiveTransferState, - 228 to V228_AddNameCollisionTables + 228 to V228_AddNameCollisionTables, + 229 to V229_MarkMissedCallEventsNotified ) - const val DATABASE_VERSION = 228 + const val DATABASE_VERSION = 229 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V229_MarkMissedCallEventsNotified.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V229_MarkMissedCallEventsNotified.kt new file mode 100644 index 0000000000..16469407cb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V229_MarkMissedCallEventsNotified.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * In order to both correct how we display missed calls and not spam users, + * we want to mark every missed call event in the database as notified. + */ +@Suppress("ClassName") +object V229_MarkMissedCallEventsNotified : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL( + """ + UPDATE message + SET notified = 1 + WHERE (type = 3) OR (type = 8) + """.trimIndent() + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt index b087aa260a..81de40c107 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt @@ -66,6 +66,25 @@ class CallLogEventSendJob private constructor( type = SyncMessage.CallLogEvent.Type.MARKED_AS_READ ) ) + + @JvmStatic + @WorkerThread + fun forMarkedAsReadInConversation( + call: CallTable.Call + ) = CallLogEventSendJob( + Parameters.Builder() + .setQueue("CallLogEventSendJob") + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .addConstraint(NetworkConstraint.KEY) + .build(), + SyncMessage.CallLogEvent( + timestamp = call.timestamp, + callId = call.callId, + conversationId = Recipient.resolved(call.peer).requireCallConversationId().toByteString(), + type = SyncMessage.CallLogEvent.Type.MARKED_AS_READ_IN_CONVERSATION + ) + ) } override fun serialize(): ByteArray = CallLogEventSendJobData.Builder() 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 3e37771980..5846da2949 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -1246,20 +1246,20 @@ object SyncMessageProcessor { if (call != null) { log(envelopeTimestamp, "Synchronizing call log event with exact call data.") - synchronizeCallLogEventViaTimestamp(envelopeTimestamp, callLogEvent.type, call.timestamp) + synchronizeCallLogEventViaTimestamp(envelopeTimestamp, callLogEvent.type, call.timestamp, peer) return } } if (timestamp != null) { warn(envelopeTimestamp, "Synchronize call log event using timestamp instead of exact values") - synchronizeCallLogEventViaTimestamp(envelopeTimestamp, callLogEvent.type, timestamp) + synchronizeCallLogEventViaTimestamp(envelopeTimestamp, callLogEvent.type, timestamp, peer) } else { log(envelopeTimestamp, "Failed to synchronize call log event, not enough information.") } } - private fun synchronizeCallLogEventViaTimestamp(envelopeTimestamp: Long, eventType: CallLogEvent.Type?, timestamp: Long) { + private fun synchronizeCallLogEventViaTimestamp(envelopeTimestamp: Long, eventType: CallLogEvent.Type?, timestamp: Long, peer: RecipientId?) { when (eventType) { CallLogEvent.Type.CLEAR -> { SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(timestamp) @@ -1270,6 +1270,15 @@ object SyncMessageProcessor { SignalDatabase.calls.markAllCallEventsRead(timestamp) } + CallLogEvent.Type.MARKED_AS_READ_IN_CONVERSATION -> { + if (peer == null) { + warn(envelopeTimestamp, "Cannot synchronize conversation calls, missing peer.") + return + } + + SignalDatabase.calls.markAllCallEventsWithPeerBeforeTimestampRead(peer, timestamp) + } + 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 5df2a5e9d5..2a4761f31c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -12,16 +12,17 @@ import com.annimon.stream.Stream; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.database.CallTable; import org.thoughtcrime.securesms.database.MessageTable.ExpirationInfo; import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobs.CallLogEventSendJob; import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.service.ExpiringMessageManager; import java.util.ArrayList; import java.util.LinkedList; @@ -62,6 +63,7 @@ public class MarkReadReceiver extends BroadcastReceiver { } process(messageIdsCollection); + processCallEvents(threads, System.currentTimeMillis()); ApplicationDependencies.getMessageNotifier().updateNotification(context); finisher.finish(); @@ -102,6 +104,20 @@ public class MarkReadReceiver extends BroadcastReceiver { }); } + public static void processCallEvents(@NonNull List threads, long timestamp) { + List peers = SignalDatabase.threads().getRecipientIdsForThreadIds(threads.stream() + .filter(it -> it.getGroupStoryId() == null) + .map(ConversationId::getThreadId) + .collect(java.util.stream.Collectors.toList())); + + for (RecipientId peer : peers) { + CallTable.Call lastCallInThread = SignalDatabase.calls().markAllCallEventsWithPeerBeforeTimestampRead(peer, timestamp); + if (lastCallInThread != null) { + ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forMarkedAsReadInConversation(lastCallInThread)); + } + } + } + private static void scheduleDeletion(@NonNull List expirationInfo) { if (expirationInfo.size() > 0) { long now = System.currentTimeMillis(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt index 7bf9205f21..433b70178f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt @@ -138,6 +138,7 @@ object NotificationStateProvider { ) { private val isGroupStoryReply: Boolean = thread.groupStoryId != null private val isUnreadIncoming: Boolean = isUnreadMessage && !messageRecord.isOutgoing && !isGroupStoryReply + private val isIncomingMissedCall: Boolean = !messageRecord.isOutgoing && (messageRecord.isMissedAudioCall || messageRecord.isMissedVideoCall) private val isNotifiableGroupStoryMessage: Boolean = isUnreadMessage && @@ -146,7 +147,7 @@ object NotificationStateProvider { (isParentStorySentBySelf || messageRecord.hasSelfMention() || (hasSelfRepliedToStory && !messageRecord.isStoryReaction())) fun includeMessage(notificationProfile: NotificationProfile?): MessageInclusion { - return if (isUnreadIncoming || stickyThread || isNotifiableGroupStoryMessage) { + return if (isUnreadIncoming || stickyThread || isNotifiableGroupStoryMessage || isIncomingMissedCall) { if (threadRecipient.isMuted && (threadRecipient.isDoNotNotifyMentions || !messageRecord.hasSelfMention())) { MessageInclusion.MUTE_FILTERED } else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id) && !(notificationProfile.allowAllMentions && messageRecord.hasSelfMention())) { diff --git a/libsignal-service/src/main/protowire/SignalService.proto b/libsignal-service/src/main/protowire/SignalService.proto index 0707e787a7..c6d482d649 100644 --- a/libsignal-service/src/main/protowire/SignalService.proto +++ b/libsignal-service/src/main/protowire/SignalService.proto @@ -637,8 +637,9 @@ message SyncMessage { message CallLogEvent { enum Type { - CLEAR = 0; - MARKED_AS_READ = 1; + CLEAR = 0; + MARKED_AS_READ = 1; + MARKED_AS_READ_IN_CONVERSATION = 2; } optional Type type = 1;