Improve delete sync coverage for partial expiring threads.

This commit is contained in:
Cody Henthorne
2024-06-18 11:33:04 -04:00
committed by Greyson Parrelli
parent 070174fee6
commit 6659700a1c
13 changed files with 127 additions and 51 deletions

View File

@@ -209,6 +209,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
const val QUOTE_NOT_PRESENT_ID = 0L
const val QUOTE_TARGET_MISSING_ID = -1L
const val ADDRESSABLE_MESSAGE_LIMIT = 5
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -4972,26 +4974,26 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return !hasMessages
}
fun getMostRecentAddressableMessages(threadId: Long): Set<MessageRecord> {
fun getMostRecentAddressableMessages(threadId: Long, excludeExpiring: Boolean): Set<MessageRecord> {
return readableDatabase
.select(*MMS_PROJECTION)
.from(TABLE_NAME)
.where("$IS_ADDRESSABLE_CLAUSE AND $THREAD_ID = ?", threadId)
.where("$IS_ADDRESSABLE_CLAUSE AND $THREAD_ID = ? ${if (excludeExpiring) "AND $EXPIRES_IN = 0" else ""}", threadId)
.orderBy("$DATE_RECEIVED DESC")
.limit(5)
.limit(ADDRESSABLE_MESSAGE_LIMIT)
.run()
.use {
MmsReader(it).toSet()
}
}
fun getAddressableMessagesBefore(threadId: Long, beforeTimestamp: Long): Set<MessageRecord> {
fun getAddressableMessagesBefore(threadId: Long, beforeTimestamp: Long, excludeExpiring: Boolean): Set<MessageRecord> {
return readableDatabase
.select(*MMS_PROJECTION)
.from(TABLE_NAME)
.where("$IS_ADDRESSABLE_CLAUSE AND $THREAD_ID = ? AND $DATE_RECEIVED < ?", threadId, beforeTimestamp)
.where("$IS_ADDRESSABLE_CLAUSE AND $THREAD_ID = ? AND $DATE_RECEIVED < ? ${if (excludeExpiring) "AND $EXPIRES_IN = 0" else ""}", threadId, beforeTimestamp)
.orderBy("$DATE_RECEIVED DESC")
.limit(5)
.limit(ADDRESSABLE_MESSAGE_LIMIT)
.run()
.use {
MmsReader(it).toSet()

View File

@@ -50,7 +50,7 @@ import org.thoughtcrime.securesms.database.model.serialize
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob
import org.thoughtcrime.securesms.jobs.OptimizeMessageSearchIndexJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.SlideDeck
@@ -326,7 +326,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
}
val syncThreadTrimDeletes = SignalStore.settings().shouldSyncThreadTrimDeletes() && Recipient.self().deleteSyncCapability.isSupported
val threadTrimsToSync = mutableListOf<Pair<Long, Set<MessageRecord>>>()
val threadTrimsToSync = mutableListOf<ThreadDeleteSyncInfo>()
readableDatabase
.select(ID)
@@ -358,7 +358,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
}
if (syncThreadTrimDeletes && threadTrimsToSync.isNotEmpty()) {
MultiDeviceDeleteSendSyncJob.enqueueThreadDeletes(threadTrimsToSync, isFullDelete = false)
MultiDeviceDeleteSyncJob.enqueueThreadDeletes(threadTrimsToSync, isFullDelete = false)
}
notifyAttachmentListeners()
@@ -377,7 +377,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
return
}
var threadTrimToSync: Pair<Long, Set<MessageRecord>>? = null
var threadTrimToSync: ThreadDeleteSyncInfo? = null
val deletes = writableDatabase.withinTransaction {
threadTrimToSync = trimThreadInternal(threadId, syncThreadTrimDeletes, length, trimBeforeDate, inclusive)
messages.deleteAbandonedMessages()
@@ -392,7 +392,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
}
if (syncThreadTrimDeletes && threadTrimToSync != null) {
MultiDeviceDeleteSendSyncJob.enqueueThreadDeletes(listOf(threadTrimToSync!!), isFullDelete = false)
MultiDeviceDeleteSyncJob.enqueueThreadDeletes(listOf(threadTrimToSync!!), isFullDelete = false)
}
notifyAttachmentListeners()
@@ -406,7 +406,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
length: Int,
trimBeforeDate: Long,
inclusive: Boolean = false
): Pair<Long, Set<MessageRecord>>? {
): ThreadDeleteSyncInfo? {
if (length == NO_TRIM_MESSAGE_COUNT_SET && trimBeforeDate == NO_TRIM_BEFORE_DATE_SET) {
return null
}
@@ -427,7 +427,18 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
if (finalTrimBeforeDate != NO_TRIM_BEFORE_DATE_SET) {
Log.i(TAG, "Trimming thread: $threadId before: $finalTrimBeforeDate inclusive: $inclusive")
val addressableMessages: Set<MessageRecord> = if (syncThreadTrimDeletes) messages.getAddressableMessagesBefore(threadId, finalTrimBeforeDate) else emptySet()
val addressableMessages: Set<MessageRecord> = if (syncThreadTrimDeletes) {
messages.getAddressableMessagesBefore(threadId, finalTrimBeforeDate, excludeExpiring = false)
} else {
emptySet()
}
val nonExpiringAddressableMessages: Set<MessageRecord> = if (syncThreadTrimDeletes && addressableMessages.size == MessageTable.ADDRESSABLE_MESSAGE_LIMIT && addressableMessages.any { it.expiresIn > 0 }) {
messages.getAddressableMessagesBefore(threadId, finalTrimBeforeDate, excludeExpiring = true)
} else {
emptySet()
}
val deletes = messages.deleteMessagesInThreadBeforeDate(threadId, finalTrimBeforeDate, inclusive)
if (deletes > 0) {
@@ -438,7 +449,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
SignalDatabase.calls.updateCallEventDeletionTimestamps()
return if (syncThreadTrimDeletes && (threadDeleted || addressableMessages.isNotEmpty())) {
threadId to addressableMessages
ThreadDeleteSyncInfo(threadId, addressableMessages, nonExpiringAddressableMessages)
} else {
null
}
@@ -1132,13 +1143,20 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
fun deleteConversations(selectedConversations: Set<Long>, syncThreadDeletes: Boolean = true) {
val recipientIds = getRecipientIdsForThreadIds(selectedConversations)
val addressableMessages = mutableListOf<Pair<Long, Set<MessageRecord>>>()
val addressableMessages = mutableListOf<ThreadDeleteSyncInfo>()
val queries: List<SqlUtil.Query> = SqlUtil.buildCollectionQuery(ID, selectedConversations)
writableDatabase.withinTransaction { db ->
if (syncThreadDeletes && Recipient.self().deleteSyncCapability.isSupported) {
for (threadId in selectedConversations) {
addressableMessages += threadId to messages.getMostRecentAddressableMessages(threadId)
val mostRecentMessages = messages.getMostRecentAddressableMessages(threadId, excludeExpiring = false)
val mostRecentNonExpiring = if (mostRecentMessages.size == MessageTable.ADDRESSABLE_MESSAGE_LIMIT && mostRecentMessages.any { it.expiresIn > 0 }) {
messages.getMostRecentAddressableMessages(threadId, excludeExpiring = true)
} else {
emptySet()
}
addressableMessages += ThreadDeleteSyncInfo(threadId, mostRecentMessages, mostRecentNonExpiring)
}
}
@@ -1160,7 +1178,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
}
if (syncThreadDeletes) {
MultiDeviceDeleteSendSyncJob.enqueueThreadDeletes(addressableMessages, isFullDelete = true)
MultiDeviceDeleteSyncJob.enqueueThreadDeletes(addressableMessages, isFullDelete = true)
}
notifyConversationListListeners()
@@ -2206,4 +2224,6 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
val threadId: Long,
val newlyCreated: Boolean
)
data class ThreadDeleteSyncInfo(val threadId: Long, val addressableMessages: Set<MessageRecord>, val nonExpiringAddressableMessages: Set<MessageRecord>)
}

View File

@@ -164,7 +164,7 @@ public final class JobManagerFactories {
put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory());
put(MultiDeviceContactSyncJob.KEY, new MultiDeviceContactSyncJob.Factory());
put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory());
put(MultiDeviceDeleteSendSyncJob.KEY, new MultiDeviceDeleteSendSyncJob.Factory());
put(MultiDeviceDeleteSyncJob.KEY, new MultiDeviceDeleteSyncJob.Factory());
put(MultiDeviceKeysUpdateJob.KEY, new MultiDeviceKeysUpdateJob.Factory());
put(MultiDeviceMessageRequestResponseJob.KEY, new MultiDeviceMessageRequestResponseJob.Factory());
put(MultiDeviceOutgoingPaymentSyncJob.KEY, new MultiDeviceOutgoingPaymentSyncJob.Factory());

View File

@@ -13,6 +13,7 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
@@ -37,7 +38,7 @@ import kotlin.time.Duration.Companion.days
/**
* Send delete for me sync messages for the various type of delete syncs.
*/
class MultiDeviceDeleteSendSyncJob private constructor(
class MultiDeviceDeleteSyncJob private constructor(
private var data: DeleteSyncJobData,
parameters: Parameters = Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
@@ -48,7 +49,7 @@ class MultiDeviceDeleteSendSyncJob private constructor(
companion object {
const val KEY = "MultiDeviceDeleteSendSyncJob"
private val TAG = Log.tag(MultiDeviceDeleteSendSyncJob::class.java)
private val TAG = Log.tag(MultiDeviceDeleteSyncJob::class.java)
private const val CHUNK_SIZE = 500
private const val THREAD_CHUNK_SIZE = CHUNK_SIZE / 5
@@ -68,7 +69,7 @@ class MultiDeviceDeleteSendSyncJob private constructor(
messageRecords.chunked(CHUNK_SIZE).forEach { chunk ->
val deletes = createMessageDeletes(chunk)
if (deletes.isNotEmpty()) {
AppDependencies.jobManager.add(MultiDeviceDeleteSendSyncJob(messages = deletes))
AppDependencies.jobManager.add(MultiDeviceDeleteSyncJob(messages = deletes))
} else {
Log.i(TAG, "No valid message deletes to sync")
}
@@ -89,14 +90,14 @@ class MultiDeviceDeleteSendSyncJob private constructor(
val delete = createAttachmentDelete(message, attachment)
if (delete != null) {
AppDependencies.jobManager.add(MultiDeviceDeleteSendSyncJob(attachments = listOf(delete)))
AppDependencies.jobManager.add(MultiDeviceDeleteSyncJob(attachments = listOf(delete)))
} else {
Log.i(TAG, "No valid attachment deletes to sync attachment:${attachment.attachmentId}")
}
}
@WorkerThread
fun enqueueThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean) {
fun enqueueThreadDeletes(threads: List<ThreadTable.ThreadDeleteSyncInfo>, isFullDelete: Boolean) {
if (!TextSecurePreferences.isMultiDevice(AppDependencies.application)) {
return
}
@@ -110,7 +111,7 @@ class MultiDeviceDeleteSendSyncJob private constructor(
val threadDeletes = createThreadDeletes(chunk, isFullDelete)
if (threadDeletes.isNotEmpty()) {
AppDependencies.jobManager.add(
MultiDeviceDeleteSendSyncJob(
MultiDeviceDeleteSyncJob(
threads = threadDeletes.filter { it.messages.isNotEmpty() },
localOnlyThreads = threadDeletes.filter { it.messages.isEmpty() }
)
@@ -186,8 +187,8 @@ class MultiDeviceDeleteSendSyncJob private constructor(
}
@WorkerThread
private fun createThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean): List<ThreadDelete> {
return threads.mapNotNull { (threadId, messages) ->
private fun createThreadDeletes(threads: List<ThreadTable.ThreadDeleteSyncInfo>, isFullDelete: Boolean): List<ThreadDelete> {
return threads.mapNotNull { (threadId, messages, nonExpiringMessages) ->
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
if (threadRecipient == null) {
Log.w(TAG, "Unable to find thread recipient for thread: $threadId")
@@ -206,6 +207,12 @@ class MultiDeviceDeleteSendSyncJob private constructor(
sentTimestamp = it.dateSent,
authorRecipientId = it.fromRecipient.id.toLong()
)
},
nonExpiringMessages = nonExpiringMessages.map {
AddressableMessage(
sentTimestamp = it.dateSent,
authorRecipientId = it.fromRecipient.id.toLong()
)
}
)
}
@@ -269,16 +276,17 @@ class MultiDeviceDeleteSendSyncJob private constructor(
if (data.threadDeletes.isNotEmpty()) {
val success = syncDelete(
DeleteForMe(
conversationDeletes = data.threadDeletes.mapNotNull {
val conversation = Recipient.resolved(RecipientId.from(it.threadRecipientId)).toDeleteSyncConversationId()
conversationDeletes = data.threadDeletes.mapNotNull { threadDelete ->
val conversation = Recipient.resolved(RecipientId.from(threadDelete.threadRecipientId)).toDeleteSyncConversationId()
if (conversation != null) {
DeleteForMe.ConversationDelete(
conversation = conversation,
mostRecentMessages = it.messages.mapNotNull { m -> m.toDeleteSyncMessage() },
isFullDelete = it.isFullDelete
mostRecentMessages = threadDelete.messages.mapNotNull { it.toDeleteSyncMessage() },
isFullDelete = threadDelete.isFullDelete,
mostRecentNonExpiringMessages = threadDelete.messages.mapNotNull { it.toDeleteSyncMessage() }
)
} else {
Log.w(TAG, "Unable to resolve ${it.threadRecipientId} to conversation id")
Log.w(TAG, "Unable to resolve ${threadDelete.threadRecipientId} to conversation id")
null
}
}
@@ -408,9 +416,9 @@ class MultiDeviceDeleteSendSyncJob private constructor(
}
}
class Factory : Job.Factory<MultiDeviceDeleteSendSyncJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): MultiDeviceDeleteSendSyncJob {
return MultiDeviceDeleteSendSyncJob(DeleteSyncJobData.ADAPTER.decode(serializedData!!), parameters)
class Factory : Job.Factory<MultiDeviceDeleteSyncJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): MultiDeviceDeleteSyncJob {
return MultiDeviceDeleteSyncJob(DeleteSyncJobData.ADAPTER.decode(serializedData!!), parameters)
}
}
}

View File

@@ -14,7 +14,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AttachmentUtil;
@@ -87,7 +87,7 @@ final class MediaActions {
}
if (Recipient.self().getDeleteSyncCapability().isSupported() && Util.hasItems(deletedMessageRecords)) {
MultiDeviceDeleteSendSyncJob.enqueueMessageDeletes(deletedMessageRecords);
MultiDeviceDeleteSyncJob.enqueueMessageDeletes(deletedMessageRecords);
}
return null;

View File

@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.media
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob
import org.thoughtcrime.securesms.longmessage.resolveBody
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -86,7 +86,7 @@ class MediaPreviewRepository {
return Completable.fromRunnable {
val deletedMessageRecord = AttachmentUtil.deleteAttachment(attachment)
if (deletedMessageRecord != null && Recipient.self().deleteSyncCapability.isSupported) {
MultiDeviceDeleteSendSyncJob.enqueueMessageDeletes(setOf(deletedMessageRecord))
MultiDeviceDeleteSyncJob.enqueueMessageDeletes(setOf(deletedMessageRecord))
}
}.subscribeOn(Schedulers.io())
}

View File

@@ -1536,8 +1536,12 @@ object SyncMessageProcessor {
continue
}
val mostRecentMessagesToDelete: List<MessageTable.SyncMessageId> = delete.mostRecentMessages.mapNotNull { it.toSyncMessageId(envelopeTimestamp) }
val latestReceivedAt = SignalDatabase.messages.getLatestReceivedAt(threadId, mostRecentMessagesToDelete)
var latestReceivedAt = SignalDatabase.messages.getLatestReceivedAt(threadId, delete.mostRecentMessages.mapNotNull { it.toSyncMessageId(envelopeTimestamp) })
if (latestReceivedAt == null && delete.mostRecentNonExpiringMessages.isNotEmpty()) {
log(envelopeTimestamp, "[handleSynchronizeDeleteForMe] Using backup non-expiring messages")
latestReceivedAt = SignalDatabase.messages.getLatestReceivedAt(threadId, delete.mostRecentNonExpiringMessages.mapNotNull { it.toSyncMessageId(envelopeTimestamp) })
}
if (latestReceivedAt != null) {
SignalDatabase.threads.trimThread(threadId = threadId, syncThreadTrimDeletes = false, trimBeforeDate = latestReceivedAt, inclusive = true)

View File

@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint;
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.Collections;
@@ -105,7 +105,7 @@ public class AttachmentUtil {
} else {
SignalDatabase.attachments().deleteAttachment(attachmentId);
if (Recipient.self().getDeleteSyncCapability().isSupported()) {
MultiDeviceDeleteSendSyncJob.enqueueAttachmentDelete(SignalDatabase.messages().getMessageRecordOrNull(mmsId), attachment);
MultiDeviceDeleteSyncJob.enqueueAttachmentDelete(SignalDatabase.messages().getMessageRecordOrNull(mmsId), attachment);
}
}

View File

@@ -8,7 +8,7 @@ import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.MessageSender
@@ -122,7 +122,7 @@ object DeleteDialog {
}
if (Recipient.self().deleteSyncCapability.isSupported) {
MultiDeviceDeleteSendSyncJob.enqueueMessageDeletes(messageRecords)
MultiDeviceDeleteSyncJob.enqueueMessageDeletes(messageRecords)
}
return threadDeleted