diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRepository.kt index e3c0f5ae7c..81bbf06834 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRepository.kt @@ -43,7 +43,8 @@ class CallLogRepository( fun markAllCallEventsRead() { SignalExecutors.BOUNDED_IO.execute { - SignalDatabase.messages.markAllCallEventsRead() + SignalDatabase.calls.markAllCallEventsRead() + ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forMarkedAsRead(System.currentTimeMillis())) } } 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 be05e03fc4..22ed8a52ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt @@ -7,6 +7,7 @@ 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.count import org.signal.core.util.delete import org.signal.core.util.deleteAll import org.signal.core.util.flatten @@ -60,9 +61,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl const val TIMESTAMP = "timestamp" const val RINGER = "ringer" const val DELETION_TIMESTAMP = "deletion_timestamp" + const val READ = "read" //language=sql - val CREATE_TABLE = """ + const val CREATE_TABLE = """ CREATE TABLE $TABLE_NAME ( $ID INTEGER PRIMARY KEY, $CALL_ID INTEGER NOT NULL, @@ -74,6 +76,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl $TIMESTAMP INTEGER NOT NULL, $RINGER INTEGER DEFAULT NULL, $DELETION_TIMESTAMP INTEGER DEFAULT 0, + $READ INTEGER DEFAULT 1, UNIQUE ($CALL_ID, $PEER) ON CONFLICT FAIL ) """ @@ -85,12 +88,29 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl ) } + fun markAllCallEventsRead(timestamp: Long = Long.MAX_VALUE) { + writableDatabase.update(TABLE_NAME) + .values(READ to ReadState.serialize(ReadState.READ)) + .where("$TIMESTAMP <= ?", timestamp) + .run() + + notifyConversationListListeners() + } + + fun getUnreadMissedCallCount(): Long { + return readableDatabase + .count() + .from(TABLE_NAME) + .where("$EVENT = ? AND $READ = ?", Event.serialize(Event.MISSED), ReadState.serialize(ReadState.UNREAD)) + .run() + .readToSingleLong() + } + fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event) { val messageType: Long = Call.getMessageType(type, direction, event) writableDatabase.withinTransaction { val result = SignalDatabase.messages.insertCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING) - val values = contentValuesOf( CALL_ID to callId, MESSAGE_ID to result.messageId, @@ -98,7 +118,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl TYPE to Type.serialize(type), DIRECTION to Direction.serialize(direction), EVENT to Event.serialize(event), - TIMESTAMP to timestamp + TIMESTAMP to timestamp, + READ to ReadState.serialize(ReadState.UNREAD) ) writableDatabase.insert(TABLE_NAME, null, values) @@ -114,7 +135,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl return writableDatabase.withinTransaction { writableDatabase .update(TABLE_NAME) - .values(EVENT to Event.serialize(event)) + .values( + EVENT to Event.serialize(event), + READ to ReadState.serialize(ReadState.UNREAD) + ) .where("$CALL_ID = ?", callId) .run() @@ -1361,6 +1385,21 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl } } + enum class ReadState(private val code: Int) { + UNREAD(0), + READ(1); + + companion object Serializer : IntSerializer { + override fun serialize(data: ReadState): Int { + return data.code + } + + override fun deserialize(data: Int): ReadState { + return ReadState.values().first { it.code == data } + } + } + } + enum class Event(private val code: Int) { /** * 1:1 Calls only. 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 b0c0697559..323947e0fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -774,7 +774,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } fun insertCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean): InsertResult { - val unread = MessageTypes.isMissedAudioCall(type) || MessageTypes.isMissedVideoCall(type) val recipient = Recipient.resolved(recipientId) val threadIdResult = threads.getOrCreateThreadIdResultFor(recipient.id, recipient.isGroup) val threadId = threadIdResult.threadId @@ -785,17 +784,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat TO_RECIPIENT_ID to if (outgoing) recipientId.serialize() else Recipient.self().id.serialize(), DATE_RECEIVED to System.currentTimeMillis(), DATE_SENT to timestamp, - READ to if (unread) 0 else 1, + READ to 1, TYPE to type, THREAD_ID to threadId ) val messageId = writableDatabase.insert(TABLE_NAME, null, values) - if (unread) { - threads.incrementUnread(threadId, 1, 0) - } - threads.update(threadId, true) notifyConversationListeners(threadId) @@ -809,23 +804,17 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } fun updateCallLog(messageId: Long, type: Long) { - val unread = MessageTypes.isMissedAudioCall(type) || MessageTypes.isMissedVideoCall(type) - writableDatabase .update(TABLE_NAME) .values( TYPE to type, - READ to if (unread) 0 else 1 + READ to 1 ) .where("$ID = ?", messageId) .run() val threadId = getThreadIdForMessage(messageId) - if (unread) { - threads.incrementUnread(threadId, 1, 0) - } - threads.update(threadId, true) notifyConversationListeners(threadId) @@ -1281,7 +1270,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat return MmsReader(rawQueryWithAttachments(query, args, false, limit.toLong())) } - fun getUnreadMisedCallCount(): Long { + fun getUnreadMissedCallCount(): Long { return readableDatabase .select("COUNT(*)") .from(TABLE_NAME) 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 8ecc56a239..fafd9c7f5f 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 @@ -78,6 +78,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V217_MessageTableEx import org.thoughtcrime.securesms.database.helpers.migration.V218_RecipientPniSignatureVerified import org.thoughtcrime.securesms.database.helpers.migration.V219_PniPreKeyStores import org.thoughtcrime.securesms.database.helpers.migration.V220_PreKeyConstraints +import org.thoughtcrime.securesms.database.helpers.migration.V221_AddReadColumnToCallEventsTable /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -158,10 +159,11 @@ object SignalDatabaseMigrations { 217 to V217_MessageTableExtrasColumn, 218 to V218_RecipientPniSignatureVerified, 219 to V219_PniPreKeyStores, - 220 to V220_PreKeyConstraints + 220 to V220_PreKeyConstraints, + 221 to V221_AddReadColumnToCallEventsTable ) - const val DATABASE_VERSION = 220 + const val DATABASE_VERSION = 221 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V221_AddReadColumnToCallEventsTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V221_AddReadColumnToCallEventsTable.kt new file mode 100644 index 0000000000..3cdca4412c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V221_AddReadColumnToCallEventsTable.kt @@ -0,0 +1,45 @@ +/* + * 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 + +/** + * Adds read state to call events to separately track from the primary messages table. + * Copies the current read state in from the message database, and then clears the message + * database 'read' flag as well as decrements the unread count in the thread databse. + */ +@Suppress("ClassName") +object V221_AddReadColumnToCallEventsTable : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE call ADD COLUMN read INTEGER DEFAULT 1") + + db.execSQL( + """ + UPDATE call + SET read = (SELECT read FROM message WHERE _id = call.message_id) + WHERE event = 3 AND direction = 0 + """.trimIndent() + ) + + db.execSQL( + """ + UPDATE thread + SET unread_count = thread.unread_count - 1 + WHERE _id IN (SELECT thread_id FROM message WHERE (type = 3 OR type = 8) AND read = 0) AND unread_count > 0 + """.trimIndent() + ) + + db.execSQL( + """ + UPDATE message + SET read = 1 + WHERE (type = 3 OR type = 8) AND read = 0 + """.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 78412cb8b1..992b76f93f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt @@ -41,6 +41,21 @@ class CallLogEventSendJob private constructor( type = SyncMessage.CallLogEvent.Type.CLEAR ) ) + + fun forMarkedAsRead( + timestamp: Long + ) = CallLogEventSendJob( + Parameters.Builder() + .setQueue("CallLogEventSendJob") + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .addConstraint(NetworkConstraint.KEY) + .build(), + SyncMessage.CallLogEvent( + timestamp = timestamp, + type = SyncMessage.CallLogEvent.Type.MARKED_AS_READ + ) + ) } 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 ed57963347..602ab61527 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -1197,16 +1197,23 @@ object SyncMessageProcessor { } private fun handleSynchronizeCallLogEvent(callLogEvent: CallLogEvent, envelopeTimestamp: Long) { - if (callLogEvent.type != CallLogEvent.Type.CLEAR) { - log(envelopeTimestamp, "Synchronize call log event has an invalid type ${callLogEvent.type}, ignoring.") - return - } else if (callLogEvent.timestamp == null) { + if (callLogEvent.timestamp == null) { log(envelopeTimestamp, "Synchronize call log event has null timestamp") return } - SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(callLogEvent.timestamp!!) - SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(callLogEvent.timestamp!!) + when (callLogEvent.type) { + CallLogEvent.Type.CLEAR -> { + SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(callLogEvent.timestamp!!) + SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(callLogEvent.timestamp!!) + } + + CallLogEvent.Type.MARKED_AS_READ -> { + SignalDatabase.calls.markAllCallEventsRead(callLogEvent.timestamp!!) + } + + else -> log(envelopeTimestamp, "Synchronize call log event has an invalid type ${callLogEvent.type}, ignoring.") + } } private fun handleSynchronizeCallLink(callLinkUpdate: CallLinkUpdate, envelopeTimestamp: Long) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt index e0ea88505e..9c6993f78b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/tabs/ConversationListTabRepository.kt @@ -28,6 +28,6 @@ class ConversationListTabRepository { } fun getNumberOfUnseenCalls(): Flowable { - return RxDatabaseObserver.conversationList.map { SignalDatabase.messages.getUnreadMisedCallCount() } + return RxDatabaseObserver.conversationList.map { SignalDatabase.calls.getUnreadMissedCallCount() } } } diff --git a/libsignal-service/src/main/protowire/SignalService.proto b/libsignal-service/src/main/protowire/SignalService.proto index fe19d3efb1..49ef328a6b 100644 --- a/libsignal-service/src/main/protowire/SignalService.proto +++ b/libsignal-service/src/main/protowire/SignalService.proto @@ -633,7 +633,8 @@ message SyncMessage { message CallLogEvent { enum Type { - CLEAR = 0; + CLEAR = 0; + MARKED_AS_READ = 1; } optional Type type = 1;