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.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);
});
});
}

View file

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

View file

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

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.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) {

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

View file

@ -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.")
}
}

View file

@ -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<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) {
if (expirationInfo.size() > 0) {
long now = System.currentTimeMillis();

View file

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

View file

@ -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;