Add support for call log mark as read.
This commit is contained in:
parent
690608cdf3
commit
9f197b12ed
9 changed files with 128 additions and 29 deletions
|
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue