Improved missed call state handling.

This commit is contained in:
Alex Hart
2024-04-25 10:18:01 -03:00
committed by Greyson Parrelli
parent 95fbd7a31c
commit e60b32202e
10 changed files with 128 additions and 10 deletions

View File

@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor; import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -67,6 +68,7 @@ public class MarkReadHelper {
ApplicationDependencies.getMessageNotifier().updateNotification(context); ApplicationDependencies.getMessageNotifier().updateNotification(context);
MarkReadReceiver.process(infos); MarkReadReceiver.process(infos);
MarkReadReceiver.processCallEvents(Collections.singletonList(conversationId), timestamp);
}); });
}); });
} }

View File

@@ -118,6 +118,30 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
notifyConversationListListeners() 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 { fun getUnreadMissedCallCount(): Long {
return readableDatabase return readableDatabase
.count() .count()

View File

@@ -433,6 +433,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
($TYPE = ${MessageTypes.GROUP_CALL_TYPE}) ($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 { private val outgoingTypeClause: String by lazy {
MessageTypes.OUTGOING_MESSAGE_TYPES MessageTypes.OUTGOING_MESSAGE_TYPES
.map { "($TABLE_NAME.$TYPE & ${MessageTypes.BASE_TYPE_MASK} = $it)" } .map { "($TABLE_NAME.$TYPE & ${MessageTypes.BASE_TYPE_MASK} = $it)" }
@@ -4647,7 +4653,19 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return readableDatabase return readableDatabase
.select(*MMS_PROJECTION) .select(*MMS_PROJECTION)
.from(TABLE_NAME) .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") .orderBy("$DATE_RECEIVED ASC")
.run() .run()
} }

View File

@@ -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.V226_AddAttachmentMediaIdIndex
import org.thoughtcrime.securesms.database.helpers.migration.V227_AddAttachmentArchiveTransferState 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.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. * 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, 225 to V225_AddLocalUserJoinedStateAndGroupCallActiveState,
226 to V226_AddAttachmentMediaIdIndex, 226 to V226_AddAttachmentMediaIdIndex,
227 to V227_AddAttachmentArchiveTransferState, 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 @JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -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()
)
}
}

View File

@@ -66,6 +66,25 @@ class CallLogEventSendJob private constructor(
type = SyncMessage.CallLogEvent.Type.MARKED_AS_READ 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() override fun serialize(): ByteArray = CallLogEventSendJobData.Builder()

View File

@@ -1246,20 +1246,20 @@ object SyncMessageProcessor {
if (call != null) { if (call != null) {
log(envelopeTimestamp, "Synchronizing call log event with exact call data.") log(envelopeTimestamp, "Synchronizing call log event with exact call data.")
synchronizeCallLogEventViaTimestamp(envelopeTimestamp, callLogEvent.type, call.timestamp) synchronizeCallLogEventViaTimestamp(envelopeTimestamp, callLogEvent.type, call.timestamp, peer)
return return
} }
} }
if (timestamp != null) { if (timestamp != null) {
warn(envelopeTimestamp, "Synchronize call log event using timestamp instead of exact values") warn(envelopeTimestamp, "Synchronize call log event using timestamp instead of exact values")
synchronizeCallLogEventViaTimestamp(envelopeTimestamp, callLogEvent.type, timestamp) synchronizeCallLogEventViaTimestamp(envelopeTimestamp, callLogEvent.type, timestamp, peer)
} else { } else {
log(envelopeTimestamp, "Failed to synchronize call log event, not enough information.") 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) { when (eventType) {
CallLogEvent.Type.CLEAR -> { CallLogEvent.Type.CLEAR -> {
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(timestamp) SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(timestamp)
@@ -1270,6 +1270,15 @@ object SyncMessageProcessor {
SignalDatabase.calls.markAllCallEventsRead(timestamp) 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.") else -> log(envelopeTimestamp, "Synchronize call log event has an invalid type $eventType, ignoring.")
} }
} }

View File

@@ -12,16 +12,17 @@ import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log; 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.ExpirationInfo;
import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.CallLogEventSendJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
@@ -62,6 +63,7 @@ public class MarkReadReceiver extends BroadcastReceiver {
} }
process(messageIdsCollection); process(messageIdsCollection);
processCallEvents(threads, System.currentTimeMillis());
ApplicationDependencies.getMessageNotifier().updateNotification(context); ApplicationDependencies.getMessageNotifier().updateNotification(context);
finisher.finish(); finisher.finish();
@@ -102,6 +104,20 @@ public class MarkReadReceiver extends BroadcastReceiver {
}); });
} }
public static void processCallEvents(@NonNull List<ConversationId> threads, long timestamp) {
List<RecipientId> 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> expirationInfo) { private static void scheduleDeletion(@NonNull List<ExpirationInfo> expirationInfo) {
if (expirationInfo.size() > 0) { if (expirationInfo.size() > 0) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();

View File

@@ -138,6 +138,7 @@ object NotificationStateProvider {
) { ) {
private val isGroupStoryReply: Boolean = thread.groupStoryId != null private val isGroupStoryReply: Boolean = thread.groupStoryId != null
private val isUnreadIncoming: Boolean = isUnreadMessage && !messageRecord.isOutgoing && !isGroupStoryReply private val isUnreadIncoming: Boolean = isUnreadMessage && !messageRecord.isOutgoing && !isGroupStoryReply
private val isIncomingMissedCall: Boolean = !messageRecord.isOutgoing && (messageRecord.isMissedAudioCall || messageRecord.isMissedVideoCall)
private val isNotifiableGroupStoryMessage: Boolean = private val isNotifiableGroupStoryMessage: Boolean =
isUnreadMessage && isUnreadMessage &&
@@ -146,7 +147,7 @@ object NotificationStateProvider {
(isParentStorySentBySelf || messageRecord.hasSelfMention() || (hasSelfRepliedToStory && !messageRecord.isStoryReaction())) (isParentStorySentBySelf || messageRecord.hasSelfMention() || (hasSelfRepliedToStory && !messageRecord.isStoryReaction()))
fun includeMessage(notificationProfile: NotificationProfile?): MessageInclusion { fun includeMessage(notificationProfile: NotificationProfile?): MessageInclusion {
return if (isUnreadIncoming || stickyThread || isNotifiableGroupStoryMessage) { return if (isUnreadIncoming || stickyThread || isNotifiableGroupStoryMessage || isIncomingMissedCall) {
if (threadRecipient.isMuted && (threadRecipient.isDoNotNotifyMentions || !messageRecord.hasSelfMention())) { if (threadRecipient.isMuted && (threadRecipient.isDoNotNotifyMentions || !messageRecord.hasSelfMention())) {
MessageInclusion.MUTE_FILTERED MessageInclusion.MUTE_FILTERED
} else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id) && !(notificationProfile.allowAllMentions && messageRecord.hasSelfMention())) { } else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id) && !(notificationProfile.allowAllMentions && messageRecord.hasSelfMention())) {

View File

@@ -637,8 +637,9 @@ message SyncMessage {
message CallLogEvent { message CallLogEvent {
enum Type { enum Type {
CLEAR = 0; CLEAR = 0;
MARKED_AS_READ = 1; MARKED_AS_READ = 1;
MARKED_AS_READ_IN_CONVERSATION = 2;
} }
optional Type type = 1; optional Type type = 1;