Add support for call log mark as read.

This commit is contained in:
Alex Hart 2024-03-04 15:20:34 -04:00
parent 690608cdf3
commit 9f197b12ed
9 changed files with 128 additions and 29 deletions

View file

@ -43,7 +43,8 @@ class CallLogRepository(
fun markAllCallEventsRead() { fun markAllCallEventsRead() {
SignalExecutors.BOUNDED_IO.execute { SignalExecutors.BOUNDED_IO.execute {
SignalDatabase.messages.markAllCallEventsRead() SignalDatabase.calls.markAllCallEventsRead()
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forMarkedAsRead(System.currentTimeMillis()))
} }
} }

View file

@ -7,6 +7,7 @@ import androidx.core.content.contentValuesOf
import org.signal.core.util.IntSerializer import org.signal.core.util.IntSerializer
import org.signal.core.util.Serializer import org.signal.core.util.Serializer
import org.signal.core.util.SqlUtil import org.signal.core.util.SqlUtil
import org.signal.core.util.count
import org.signal.core.util.delete import org.signal.core.util.delete
import org.signal.core.util.deleteAll import org.signal.core.util.deleteAll
import org.signal.core.util.flatten import org.signal.core.util.flatten
@ -60,9 +61,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
const val TIMESTAMP = "timestamp" const val TIMESTAMP = "timestamp"
const val RINGER = "ringer" const val RINGER = "ringer"
const val DELETION_TIMESTAMP = "deletion_timestamp" const val DELETION_TIMESTAMP = "deletion_timestamp"
const val READ = "read"
//language=sql //language=sql
val CREATE_TABLE = """ const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME ( CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY, $ID INTEGER PRIMARY KEY,
$CALL_ID INTEGER NOT NULL, $CALL_ID INTEGER NOT NULL,
@ -74,6 +76,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
$TIMESTAMP INTEGER NOT NULL, $TIMESTAMP INTEGER NOT NULL,
$RINGER INTEGER DEFAULT NULL, $RINGER INTEGER DEFAULT NULL,
$DELETION_TIMESTAMP INTEGER DEFAULT 0, $DELETION_TIMESTAMP INTEGER DEFAULT 0,
$READ INTEGER DEFAULT 1,
UNIQUE ($CALL_ID, $PEER) ON CONFLICT FAIL 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) { fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event) {
val messageType: Long = Call.getMessageType(type, direction, event) val messageType: Long = Call.getMessageType(type, direction, event)
writableDatabase.withinTransaction { writableDatabase.withinTransaction {
val result = SignalDatabase.messages.insertCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING) val result = SignalDatabase.messages.insertCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING)
val values = contentValuesOf( val values = contentValuesOf(
CALL_ID to callId, CALL_ID to callId,
MESSAGE_ID to result.messageId, MESSAGE_ID to result.messageId,
@ -98,7 +118,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
TYPE to Type.serialize(type), TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(direction), DIRECTION to Direction.serialize(direction),
EVENT to Event.serialize(event), EVENT to Event.serialize(event),
TIMESTAMP to timestamp TIMESTAMP to timestamp,
READ to ReadState.serialize(ReadState.UNREAD)
) )
writableDatabase.insert(TABLE_NAME, null, values) writableDatabase.insert(TABLE_NAME, null, values)
@ -114,7 +135,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
return writableDatabase.withinTransaction { return writableDatabase.withinTransaction {
writableDatabase writableDatabase
.update(TABLE_NAME) .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) .where("$CALL_ID = ?", callId)
.run() .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<ReadState> {
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) { enum class Event(private val code: Int) {
/** /**
* 1:1 Calls only. * 1:1 Calls only.

View file

@ -774,7 +774,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
} }
fun insertCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean): InsertResult { 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 recipient = Recipient.resolved(recipientId)
val threadIdResult = threads.getOrCreateThreadIdResultFor(recipient.id, recipient.isGroup) val threadIdResult = threads.getOrCreateThreadIdResultFor(recipient.id, recipient.isGroup)
val threadId = threadIdResult.threadId 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(), TO_RECIPIENT_ID to if (outgoing) recipientId.serialize() else Recipient.self().id.serialize(),
DATE_RECEIVED to System.currentTimeMillis(), DATE_RECEIVED to System.currentTimeMillis(),
DATE_SENT to timestamp, DATE_SENT to timestamp,
READ to if (unread) 0 else 1, READ to 1,
TYPE to type, TYPE to type,
THREAD_ID to threadId THREAD_ID to threadId
) )
val messageId = writableDatabase.insert(TABLE_NAME, null, values) val messageId = writableDatabase.insert(TABLE_NAME, null, values)
if (unread) {
threads.incrementUnread(threadId, 1, 0)
}
threads.update(threadId, true) threads.update(threadId, true)
notifyConversationListeners(threadId) notifyConversationListeners(threadId)
@ -809,23 +804,17 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
} }
fun updateCallLog(messageId: Long, type: Long) { fun updateCallLog(messageId: Long, type: Long) {
val unread = MessageTypes.isMissedAudioCall(type) || MessageTypes.isMissedVideoCall(type)
writableDatabase writableDatabase
.update(TABLE_NAME) .update(TABLE_NAME)
.values( .values(
TYPE to type, TYPE to type,
READ to if (unread) 0 else 1 READ to 1
) )
.where("$ID = ?", messageId) .where("$ID = ?", messageId)
.run() .run()
val threadId = getThreadIdForMessage(messageId) val threadId = getThreadIdForMessage(messageId)
if (unread) {
threads.incrementUnread(threadId, 1, 0)
}
threads.update(threadId, true) threads.update(threadId, true)
notifyConversationListeners(threadId) notifyConversationListeners(threadId)
@ -1281,7 +1270,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return MmsReader(rawQueryWithAttachments(query, args, false, limit.toLong())) return MmsReader(rawQueryWithAttachments(query, args, false, limit.toLong()))
} }
fun getUnreadMisedCallCount(): Long { fun getUnreadMissedCallCount(): Long {
return readableDatabase return readableDatabase
.select("COUNT(*)") .select("COUNT(*)")
.from(TABLE_NAME) .from(TABLE_NAME)

View file

@ -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.V218_RecipientPniSignatureVerified
import org.thoughtcrime.securesms.database.helpers.migration.V219_PniPreKeyStores 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.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. * 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, 217 to V217_MessageTableExtrasColumn,
218 to V218_RecipientPniSignatureVerified, 218 to V218_RecipientPniSignatureVerified,
219 to V219_PniPreKeyStores, 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 @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,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()
)
}
}

View file

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

View file

@ -1197,16 +1197,23 @@ object SyncMessageProcessor {
} }
private fun handleSynchronizeCallLogEvent(callLogEvent: CallLogEvent, envelopeTimestamp: Long) { private fun handleSynchronizeCallLogEvent(callLogEvent: CallLogEvent, envelopeTimestamp: Long) {
if (callLogEvent.type != CallLogEvent.Type.CLEAR) { if (callLogEvent.timestamp == null) {
log(envelopeTimestamp, "Synchronize call log event has an invalid type ${callLogEvent.type}, ignoring.")
return
} else if (callLogEvent.timestamp == null) {
log(envelopeTimestamp, "Synchronize call log event has null timestamp") log(envelopeTimestamp, "Synchronize call log event has null timestamp")
return return
} }
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(callLogEvent.timestamp!!) when (callLogEvent.type) {
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(callLogEvent.timestamp!!) 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) { private fun handleSynchronizeCallLink(callLinkUpdate: CallLinkUpdate, envelopeTimestamp: Long) {

View file

@ -28,6 +28,6 @@ class ConversationListTabRepository {
} }
fun getNumberOfUnseenCalls(): Flowable<Long> { fun getNumberOfUnseenCalls(): Flowable<Long> {
return RxDatabaseObserver.conversationList.map { SignalDatabase.messages.getUnreadMisedCallCount() } return RxDatabaseObserver.conversationList.map { SignalDatabase.calls.getUnreadMissedCallCount() }
} }
} }

View file

@ -633,7 +633,8 @@ message SyncMessage {
message CallLogEvent { message CallLogEvent {
enum Type { enum Type {
CLEAR = 0; CLEAR = 0;
MARKED_AS_READ = 1;
} }
optional Type type = 1; optional Type type = 1;