Add additional delete sync support.

This commit is contained in:
Cody Henthorne
2024-06-13 10:11:29 -04:00
committed by Greyson Parrelli
parent d22d18da47
commit c80ccd70ec
37 changed files with 183 additions and 333 deletions

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.account.AccountAttributes
object AppCapabilities {
@@ -17,7 +18,8 @@ object AppCapabilities {
stories = true,
giftBadges = true,
pni = true,
paymentActivation = true
paymentActivation = true,
deleteSync = RemoteConfig.deleteSyncEnabled
)
}
}

View File

@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
/**
@@ -48,7 +48,7 @@ class DeleteSyncEducationDialog : ComposeBottomSheetDialogFragment() {
fun shouldShow(): Boolean {
return TextSecurePreferences.isMultiDevice(AppDependencies.application) &&
!SignalStore.uiHints().hasSeenDeleteSyncEducationSheet &&
RemoteConfig.deleteSyncEnabled
Recipient.self().deleteSyncCapability.isSupported
}
@JvmStatic

View File

@@ -66,7 +66,7 @@ import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.preferences.widgets.StorageGraphView
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.viewModel
@@ -138,7 +138,7 @@ class ManageStorageSettingsFragment : ComposeFragment() {
dialog("confirm-delete-chat-history") {
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.preferences_storage__delete_message_history),
body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && RemoteConfig.deleteSyncEnabled) {
body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && Recipient.self().deleteSyncCapability.isSupported) {
stringResource(id = R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device_linked_device)
} else {
stringResource(id = R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device)
@@ -154,7 +154,7 @@ class ManageStorageSettingsFragment : ComposeFragment() {
dialog("double-confirm-delete-chat-history", dialogProperties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.preferences_storage__are_you_sure_you_want_to_delete_all_message_history),
body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && RemoteConfig.deleteSyncEnabled) {
body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && Recipient.self().deleteSyncCapability.isSupported) {
stringResource(id = R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone_linked_device)
} else {
stringResource(id = R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone)

View File

@@ -341,7 +341,9 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
return if (capabilities != null) {
TextUtils.concat(
colorize("PaymentActivation", capabilities.paymentActivation)
colorize("PaymentActivation", capabilities.paymentActivation),
", ",
colorize("DeleteSync", capabilities.deleteSync)
)
} else {
"Recipient not found!"

View File

@@ -2408,7 +2408,7 @@ class ConversationFragment :
disposables += DeleteDialog.show(
context = requireContext(),
messageRecords = records,
message = if (TextSecurePreferences.isMultiDevice(requireContext()) && RemoteConfig.deleteSyncEnabled) {
message = if (TextSecurePreferences.isMultiDevice(requireContext()) && Recipient.self().deleteSyncCapability.isSupported) {
resources.getQuantityString(R.plurals.ConversationFragment_delete_on_linked_warning, records.size)
} else {
null

View File

@@ -1202,7 +1202,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
alert.setTitle(context.getResources().getQuantityString(R.plurals.ConversationListFragment_delete_selected_conversations,
conversationsCount, conversationsCount));
if (TextSecurePreferences.isMultiDevice(context) && RemoteConfig.deleteSyncEnabled()) {
if (TextSecurePreferences.isMultiDevice(context) && Recipient.self().getDeleteSyncCapability().isSupported()) {
alert.setMessage(context.getResources().getQuantityString(R.plurals.ConversationListFragment_this_will_permanently_delete_all_n_selected_conversations_linked_device,
conversationsCount, conversationsCount));
} else {
@@ -1230,7 +1230,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override
protected Void doInBackground(Void... params) {
SignalDatabase.threads().deleteConversations(selectedConversations);
SignalDatabase.threads().deleteConversations(selectedConversations, true);
AppDependencies.getMessageNotifier().updateNotification(requireActivity());
return null;
}

View File

@@ -462,8 +462,11 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
* - Not an encryption message
* - Not a report spam message
* - Not a message rqeuest accepted message
* - Not be a story
* - Have a valid sent timestamp
* - Be a normal message or direct (1:1) story reply
*
* Changes should be reflected in [MmsMessageRecord.canDeleteSync].
*/
private const val IS_ADDRESSABLE_CLAUSE = """
(($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE} OR ($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_INBOX_TYPE}) AND
@@ -472,7 +475,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
($TYPE & ${MessageTypes.KEY_EXCHANGE_MASK}) = 0 AND
($TYPE & ${MessageTypes.ENCRYPTION_MASK}) = 0 AND
($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) != ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM} AND
($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} AND
($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} AND
$STORY_TYPE = 0 AND
$DATE_SENT > 0 AND
$PARENT_STORY_ID <= 0
"""
@@ -3277,7 +3281,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
threads.setLastScrolled(threadId, 0)
val threadDeleted = if (updateThread) {
threads.update(threadId, false)
threads.update(threadId, unarchive = false, syncThreadDelete = false)
} else {
false
}
@@ -3332,11 +3336,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
}
fun deleteThread(threadId: Long) {
Log.d(TAG, "deleteThread($threadId)")
deleteThreads(setOf(threadId))
}
private fun getSerializedSharedContacts(insertedAttachmentIds: Map<Attachment, AttachmentId>, contacts: List<Contact>): String? {
if (contacts.isEmpty()) {
return null
@@ -3434,27 +3433,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return ids
}
private fun deleteThreads(threadIds: Set<Long>) {
Log.d(TAG, "deleteThreads(count: ${threadIds.size})")
writableDatabase.withinTransaction { db ->
SqlUtil.buildCollectionQuery(THREAD_ID, threadIds).forEach { query ->
db.select(ID, THREAD_ID)
.from(TABLE_NAME)
.where(query.where, query.whereArgs)
.run()
.forEach { cursor ->
deleteMessage(cursor.requireLong(ID), cursor.requireLong(THREAD_ID), notify = false, updateThread = false)
}
}
}
notifyConversationListeners(threadIds)
notifyStickerListeners()
notifyStickerPackListeners()
OptimizeMessageSearchIndexJob.enqueue()
}
fun deleteMessagesInThreadBeforeDate(threadId: Long, date: Long, inclusive: Boolean): Int {
val condition = if (inclusive) "<=" else "<"

View File

@@ -410,6 +410,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
fun maskCapabilitiesToLong(capabilities: SignalServiceProfile.Capabilities): Long {
var value: Long = 0
value = Bitmask.update(value, Capabilities.PAYMENT_ACTIVATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPaymentActivation).serialize().toLong())
value = Bitmask.update(value, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isDeleteSync).serialize().toLong())
return value
}
}
@@ -4577,6 +4578,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
// const val GIFT_BADGES = 6
// const val PNP = 7
const val PAYMENT_ACTIVATION = 8
const val DELETE_SYNC = 9
}
enum class VibrateState(val id: Int) {

View File

@@ -175,7 +175,8 @@ object RecipientTableCursorUtil {
val capabilities = cursor.requireLong(RecipientTable.CAPABILITIES)
return RecipientRecord.Capabilities(
rawBits = capabilities,
paymentActivation = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PAYMENT_ACTIVATION, Capabilities.BIT_LENGTH).toInt())
paymentActivation = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PAYMENT_ACTIVATION, Capabilities.BIT_LENGTH).toInt()),
deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH).toInt())
)
}

View File

@@ -65,7 +65,6 @@ import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.JsonUtils
import org.thoughtcrime.securesms.util.JsonUtils.SaneJSONObject
import org.thoughtcrime.securesms.util.LRUCache
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.isScheduled
import org.whispersystems.signalservice.api.storage.SignalAccountRecord
@@ -326,7 +325,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
return
}
val syncThreadTrimDeletes = SignalStore.settings().shouldSyncThreadTrimDeletes() && RemoteConfig.deleteSyncEnabled
val syncThreadTrimDeletes = SignalStore.settings().shouldSyncThreadTrimDeletes() && Recipient.self().deleteSyncCapability.isSupported
val threadTrimsToSync = mutableListOf<Pair<Long, Set<MessageRecord>>>()
readableDatabase
@@ -1119,57 +1118,34 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
if (containsAddressable || isEmpty) {
false
} else {
deleteConversation(threadId, syncThreadDeletes = false)
deleteConversation(threadId, syncThreadDelete = false)
true
}
}
}
@JvmOverloads
fun deleteConversation(threadId: Long, syncThreadDeletes: Boolean = true) {
val recipientIdForThreadId = getRecipientIdForThreadId(threadId)
var addressableMessages: Set<MessageRecord> = emptySet()
writableDatabase.withinTransaction { db ->
if (syncThreadDeletes && RemoteConfig.deleteSyncEnabled) {
addressableMessages = messages.getMostRecentAddressableMessages(threadId)
}
messages.deleteThread(threadId)
drafts.clearDrafts(threadId)
db.deactivateThread(threadId)
synchronized(threadIdCache) {
threadIdCache.remove(recipientIdForThreadId)
}
}
if (syncThreadDeletes) {
MultiDeviceDeleteSendSyncJob.enqueueThreadDeletes(listOf(threadId to addressableMessages), isFullDelete = true)
}
notifyConversationListListeners()
notifyConversationListeners(threadId)
AppDependencies.databaseObserver.notifyConversationDeleteListeners(threadId)
ConversationUtil.clearShortcuts(context, setOf(recipientIdForThreadId))
fun deleteConversation(threadId: Long, syncThreadDelete: Boolean = true) {
deleteConversations(setOf(threadId), syncThreadDelete)
}
fun deleteConversations(selectedConversations: Set<Long>) {
fun deleteConversations(selectedConversations: Set<Long>, syncThreadDeletes: Boolean = true) {
val recipientIds = getRecipientIdsForThreadIds(selectedConversations)
val addressableMessages = mutableListOf<Pair<Long, Set<MessageRecord>>>()
val queries: List<SqlUtil.Query> = SqlUtil.buildCollectionQuery(ID, selectedConversations)
writableDatabase.withinTransaction { db ->
for (query in queries) {
db.deactivateThread(query)
}
if (RemoteConfig.deleteSyncEnabled) {
if (syncThreadDeletes && Recipient.self().deleteSyncCapability.isSupported) {
for (threadId in selectedConversations) {
addressableMessages += threadId to messages.getMostRecentAddressableMessages(threadId)
}
}
for (query in queries) {
db.deactivateThread(query)
}
messages.deleteAbandonedMessages()
attachments.trimAllAbandonedAttachments()
groupReceipts.deleteAbandonedRows()
@@ -1183,12 +1159,19 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
}
}
MultiDeviceDeleteSendSyncJob.enqueueThreadDeletes(addressableMessages, isFullDelete = true)
if (syncThreadDeletes) {
MultiDeviceDeleteSendSyncJob.enqueueThreadDeletes(addressableMessages, isFullDelete = true)
}
notifyConversationListListeners()
notifyConversationListeners(selectedConversations)
notifyStickerListeners()
notifyStickerPackListeners()
AppDependencies.databaseObserver.notifyConversationDeleteListeners(selectedConversations)
ConversationUtil.clearShortcuts(context, recipientIds)
OptimizeMessageSearchIndexJob.enqueue()
}
@SuppressLint("DiscouragedApi")
@@ -1485,12 +1468,13 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
.run()
}
fun update(threadId: Long, unarchive: Boolean): Boolean {
fun update(threadId: Long, unarchive: Boolean, syncThreadDelete: Boolean = true): Boolean {
return update(
threadId = threadId,
unarchive = unarchive,
allowDeletion = true,
notifyListeners = true
notifyListeners = true,
syncThreadDelete = syncThreadDelete
)
}
@@ -1499,16 +1483,18 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
threadId = threadId,
unarchive = unarchive,
allowDeletion = true,
notifyListeners = false
notifyListeners = false,
syncThreadDelete = true
)
}
fun update(threadId: Long, unarchive: Boolean, allowDeletion: Boolean): Boolean {
fun update(threadId: Long, unarchive: Boolean, allowDeletion: Boolean, syncThreadDelete: Boolean = true): Boolean {
return update(
threadId = threadId,
unarchive = unarchive,
allowDeletion = allowDeletion,
notifyListeners = true
notifyListeners = true,
syncThreadDelete = syncThreadDelete
)
}
@@ -1543,7 +1529,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
stopwatch?.split("thread-update")
}
private fun update(threadId: Long, unarchive: Boolean, allowDeletion: Boolean, notifyListeners: Boolean): Boolean {
private fun update(threadId: Long, unarchive: Boolean, allowDeletion: Boolean, notifyListeners: Boolean, syncThreadDelete: Boolean): Boolean {
if (threadId == -1L) {
Log.d(TAG, "Skipping update for threadId -1")
return false
@@ -1558,7 +1544,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
if (!meaningfulMessages) {
if (shouldDelete) {
Log.d(TAG, "Deleting thread $threadId because it has no meaningful messages.")
deleteConversation(threadId)
deleteConversation(threadId, syncThreadDelete = syncThreadDelete)
return@withinTransaction true
} else if (!isPinned) {
return@withinTransaction false

View File

@@ -823,6 +823,13 @@ public abstract class MessageRecord extends DisplayRecord {
return revisionNumber;
}
/**
* A message that can be correctly identified and delete sync'd across devices.
*/
public boolean canDeleteSync() {
return false;
}
public static final class InviteAddState {
private final boolean invited;

View File

@@ -308,6 +308,19 @@ public class MmsMessageRecord extends MessageRecord {
return latestRevisionId;
}
@Override
public boolean canDeleteSync() {
return (isSent() || MessageTypes.isInboxType(type)) &&
(isSecure() || isPush()) &&
(type & MessageTypes.GROUP_MASK) == 0 &&
(type & MessageTypes.KEY_EXCHANGE_MASK) == 0 &&
!isReportedSpam() &&
!isMessageRequestAccepted() &&
storyType == StoryType.NONE &&
getDateSent() > 0 &&
(parentStoryId == null || parentStoryId.isDirectReply());
}
public @NonNull MmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),

View File

@@ -119,12 +119,14 @@ data class RecipientRecord(
data class Capabilities(
val rawBits: Long,
val paymentActivation: Recipient.Capability
val paymentActivation: Recipient.Capability,
val deleteSync: Recipient.Capability
) {
companion object {
@JvmField
val UNKNOWN = Capabilities(
0,
Recipient.Capability.UNKNOWN,
Recipient.Capability.UNKNOWN
)
}

View File

@@ -1240,7 +1240,7 @@ final class GroupManagerV2 {
try {
long messageId = SignalDatabase.messages().insertMessageOutbox(outgoingMessage, threadId, false, null);
SignalDatabase.messages().markAsSent(messageId, true);
SignalDatabase.threads().update(threadId, true);
SignalDatabase.threads().update(threadId, true, true);
} catch (MmsException e) {
throw new AssertionError(e);
}

View File

@@ -20,9 +20,7 @@ import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData.AddressableMessa
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData.ThreadDelete
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.pad
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.Recipient.Companion.self
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException
import org.whispersystems.signalservice.internal.push.Content
@@ -58,13 +56,18 @@ class MultiDeviceDeleteSendSyncJob private constructor(
return
}
if (!RemoteConfig.deleteSyncEnabled) {
if (!Recipient.self().deleteSyncCapability.isSupported) {
Log.i(TAG, "Delete sync support not enabled.")
return
}
messageRecords.chunked(CHUNK_SIZE).forEach { chunk ->
AppDependencies.jobManager.add(createMessageDeletes(chunk))
val deletes = createMessageDeletes(chunk)
if (deletes.isNotEmpty()) {
AppDependencies.jobManager.add(MultiDeviceDeleteSendSyncJob(messages = deletes))
} else {
Log.i(TAG, "No valid message deletes to sync")
}
}
}
@@ -74,20 +77,29 @@ class MultiDeviceDeleteSendSyncJob private constructor(
return
}
if (!RemoteConfig.deleteSyncEnabled) {
if (!Recipient.self().deleteSyncCapability.isSupported) {
Log.i(TAG, "Delete sync support not enabled.")
return
}
threads.chunked(THREAD_CHUNK_SIZE).forEach { chunk ->
AppDependencies.jobManager.add(createThreadDeletes(chunk, isFullDelete))
val threadDeletes = createThreadDeletes(chunk, isFullDelete)
if (threadDeletes.isNotEmpty()) {
AppDependencies.jobManager.add(
MultiDeviceDeleteSendSyncJob(
threads = threadDeletes.filter { it.messages.isNotEmpty() },
localOnlyThreads = threadDeletes.filter { it.messages.isEmpty() }
)
)
} else {
Log.i(TAG, "No valid thread deletes to sync")
}
}
}
@WorkerThread
@VisibleForTesting
fun createMessageDeletes(messageRecords: Collection<MessageRecord>): MultiDeviceDeleteSendSyncJob {
val deletes = messageRecords.mapNotNull { message ->
private fun createMessageDeletes(messageRecords: Collection<MessageRecord>): List<AddressableMessage> {
return messageRecords.mapNotNull { message ->
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(message.threadId)
if (threadRecipient == null) {
Log.w(TAG, "Unable to find thread recipient for message: ${message.id} thread: ${message.threadId}")
@@ -95,6 +107,8 @@ class MultiDeviceDeleteSendSyncJob private constructor(
} else if (threadRecipient.isReleaseNotes) {
Log.w(TAG, "Syncing release channel deletes are not currently supported")
null
} else if (threadRecipient.isDistributionList || !message.canDeleteSync()) {
null
} else {
AddressableMessage(
threadRecipientId = threadRecipient.id.toLong(),
@@ -103,14 +117,11 @@ class MultiDeviceDeleteSendSyncJob private constructor(
)
}
}
return MultiDeviceDeleteSendSyncJob(messages = deletes)
}
@WorkerThread
@VisibleForTesting
fun createThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean): MultiDeviceDeleteSendSyncJob {
val threadDeletes: List<ThreadDelete> = threads.mapNotNull { (threadId, messages) ->
private fun createThreadDeletes(threads: List<Pair<Long, Set<MessageRecord>>>, isFullDelete: Boolean): List<ThreadDelete> {
return threads.mapNotNull { (threadId, messages) ->
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
if (threadRecipient == null) {
Log.w(TAG, "Unable to find thread recipient for thread: $threadId")
@@ -118,6 +129,8 @@ class MultiDeviceDeleteSendSyncJob private constructor(
} else if (threadRecipient.isReleaseNotes) {
Log.w(TAG, "Syncing release channel delete is not currently supported")
null
} else if (threadRecipient.isDistributionList) {
null
} else {
ThreadDelete(
threadRecipientId = threadRecipient.id.toLong(),
@@ -131,11 +144,6 @@ class MultiDeviceDeleteSendSyncJob private constructor(
)
}
}
return MultiDeviceDeleteSendSyncJob(
threads = threadDeletes.filter { it.messages.isNotEmpty() },
localOnlyThreads = threadDeletes.filter { it.messages.isEmpty() }
)
}
}
@@ -157,7 +165,7 @@ class MultiDeviceDeleteSendSyncJob private constructor(
override fun getFactoryKey(): String = KEY
override fun run(): Result {
if (!self().isRegistered) {
if (!Recipient.self().isRegistered) {
Log.w(TAG, "Not registered")
return Result.failure()
}
@@ -250,6 +258,7 @@ class MultiDeviceDeleteSendSyncJob private constructor(
val syncMessageContent = deleteForMeContent(deleteForMe)
return try {
Log.d(TAG, "Sending delete sync messageDeletes=${deleteForMe.messageDeletes.size} conversationDeletes=${deleteForMe.conversationDeletes.size} localOnlyConversationDeletes=${deleteForMe.localOnlyConversationDeletes.size}")
AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessageContent, true, Optional.empty()).isSuccess
} catch (e: IOException) {
Log.w(TAG, "Unable to send message delete sync", e)
@@ -271,7 +280,8 @@ class MultiDeviceDeleteSendSyncJob private constructor(
private fun Recipient.toDeleteSyncConversationId(): DeleteForMe.ConversationIdentifier? {
return when {
isGroup -> DeleteForMe.ConversationIdentifier(threadGroupId = requireGroupId().decodedId.toByteString())
hasAci -> DeleteForMe.ConversationIdentifier(threadAci = requireAci().toString())
hasAci -> DeleteForMe.ConversationIdentifier(threadServiceId = requireAci().toString())
hasPni -> DeleteForMe.ConversationIdentifier(threadServiceId = requirePni().toString())
hasE164 -> DeleteForMe.ConversationIdentifier(threadE164 = requireE164())
else -> null
}
@@ -279,19 +289,19 @@ class MultiDeviceDeleteSendSyncJob private constructor(
private fun AddressableMessage.toDeleteSyncMessage(): DeleteForMe.AddressableMessage? {
val author: Recipient = Recipient.resolved(RecipientId.from(authorRecipientId))
val authorAci: String? = author.aci.orNull()?.toString()
val authorE164: String? = if (authorAci == null) {
val authorServiceId: String? = author.aci.orNull()?.toString() ?: author.pni.orNull()?.toString()
val authorE164: String? = if (authorServiceId == null) {
author.e164.orNull()
} else {
null
}
return if (authorAci == null && authorE164 == null) {
Log.w(TAG, "Unable to send sync message without aci and e164 recipient: ${author.id}")
return if (authorServiceId == null && authorE164 == null) {
Log.w(TAG, "Unable to send sync message without serviceId or e164 recipient: ${author.id}")
null
} else {
DeleteForMe.AddressableMessage(
authorAci = authorAci,
authorServiceId = authorServiceId,
authorE164 = authorE164,
sentTimestamp = sentTimestamp
)

View File

@@ -55,7 +55,7 @@ public final class ThreadUpdateJob extends BaseJob {
@Override
protected void onRun() throws Exception {
SignalDatabase.threads().update(threadId, true);
SignalDatabase.threads().update(threadId, true, true);
if (!AppDependencies.getIncomingMessageObserver().getDecryptionDrained()) {
ThreadUtil.sleep(DEBOUNCE_INTERVAL_WITH_BACKLOG);
}

View File

@@ -24,11 +24,11 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.recipients.Recipient;
public class TrimThreadJob extends BaseJob {
@@ -78,7 +78,7 @@ public class TrimThreadJob extends BaseJob {
long trimBeforeDate = keepMessagesDuration != KeepMessagesDuration.FOREVER ? System.currentTimeMillis() - keepMessagesDuration.getDuration()
: ThreadTable.NO_TRIM_BEFORE_DATE_SET;
SignalDatabase.threads().trimThread(threadId, SignalStore.settings().shouldSyncThreadTrimDeletes() && RemoteConfig.deleteSyncEnabled(), trimLength, trimBeforeDate, false);
SignalDatabase.threads().trimThread(threadId, SignalStore.settings().shouldSyncThreadTrimDeletes() && Recipient.self().getDeleteSyncCapability().isSupported(), trimLength, trimBeforeDate, false);
}
@Override

View File

@@ -16,8 +16,8 @@ import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -65,7 +65,7 @@ final class MediaActions {
recordCount);
String confirmMessage;
if (TextSecurePreferences.isMultiDevice(context) && RemoteConfig.deleteSyncEnabled()) {
if (TextSecurePreferences.isMultiDevice(context) && Recipient.self().getDeleteSyncCapability().isSupported()) {
confirmMessage = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message_linked_device,
recordCount,
recordCount);
@@ -98,7 +98,7 @@ final class MediaActions {
}
}
if (RemoteConfig.deleteSyncEnabled() && Util.hasItems(deletedMessageRecords)) {
if (Recipient.self().getDeleteSyncCapability().isSupported() && Util.hasItems(deletedMessageRecords)) {
MultiDeviceDeleteSendSyncJob.enqueueMessageDeletes(deletedMessageRecords);
}

View File

@@ -23,10 +23,10 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob
import org.thoughtcrime.securesms.longmessage.resolveBody
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.AttachmentUtil
import org.thoughtcrime.securesms.util.RemoteConfig
/**
* Repository for accessing the attachments in the encrypted database.
@@ -85,7 +85,7 @@ class MediaPreviewRepository {
fun localDelete(attachment: DatabaseAttachment): Completable {
return Completable.fromRunnable {
val deletedMessageRecord = AttachmentUtil.deleteAttachment(attachment)
if (deletedMessageRecord != null && RemoteConfig.deleteSyncEnabled) {
if (deletedMessageRecord != null && Recipient.self().deleteSyncCapability.isSupported) {
MultiDeviceDeleteSendSyncJob.enqueueMessageDeletes(setOf(deletedMessageRecord))
}
}.subscribeOn(Schedulers.io())

View File

@@ -70,7 +70,6 @@ import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.MessageConstraintsUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.StorageUtil
@@ -601,7 +600,7 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v
MaterialAlertDialogBuilder(requireContext()).apply {
setIcon(R.drawable.symbol_error_triangle_fill_24)
setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title)
setMessage(if (TextSecurePreferences.isMultiDevice(requireContext()) && RemoteConfig.deleteSyncEnabled) R.string.MediaPreviewActivity_media_delete_confirmation_message_linked_device else R.string.MediaPreviewActivity_media_delete_confirmation_message)
setMessage(if (TextSecurePreferences.isMultiDevice(requireContext()) && Recipient.self().deleteSyncCapability.isSupported) R.string.MediaPreviewActivity_media_delete_confirmation_message_linked_device else R.string.MediaPreviewActivity_media_delete_confirmation_message)
setCancelable(true)
setNegativeButton(android.R.string.cancel, null)
setPositiveButton(R.string.ConversationFragment_delete_for_me) { _, _ ->

View File

@@ -274,7 +274,7 @@ public final class MessageRequestRepository {
}
ThreadTable threadTable = SignalDatabase.threads();
threadTable.deleteConversation(threadId);
threadTable.deleteConversation(threadId, false);
onMessageRequestDeleted.run();
});

View File

@@ -1103,7 +1103,7 @@ object SyncMessageProcessor {
MessageRequestResponse.Type.DELETE -> {
SignalDatabase.recipients.setProfileSharing(recipient.id, false)
if (threadId > 0) {
SignalDatabase.threads.deleteConversation(threadId)
SignalDatabase.threads.deleteConversation(threadId, syncThreadDelete = false)
}
}
MessageRequestResponse.Type.BLOCK -> {
@@ -1114,7 +1114,7 @@ object SyncMessageProcessor {
SignalDatabase.recipients.setBlocked(recipient.id, true)
SignalDatabase.recipients.setProfileSharing(recipient.id, false)
if (threadId > 0) {
SignalDatabase.threads.deleteConversation(threadId)
SignalDatabase.threads.deleteConversation(threadId, syncThreadDelete = false)
}
}
MessageRequestResponse.Type.SPAM -> {
@@ -1475,11 +1475,6 @@ object SyncMessageProcessor {
}
private fun handleSynchronizeDeleteForMe(context: Context, deleteForMe: SyncMessage.DeleteForMe, envelopeTimestamp: Long, earlyMessageCacheEntry: EarlyMessageCacheEntry?) {
if (!RemoteConfig.deleteSyncEnabled) {
warn(envelopeTimestamp, "Delete for me sync message dropped as support not enabled")
return
}
log(envelopeTimestamp, "Synchronize delete message messageDeletes=${deleteForMe.messageDeletes.size} conversationDeletes=${deleteForMe.conversationDeletes.size} localOnlyConversationDeletes=${deleteForMe.localOnlyConversationDeletes.size}")
if (deleteForMe.messageDeletes.isNotEmpty()) {
@@ -1586,8 +1581,8 @@ object SyncMessageProcessor {
}
}
threadAci != null -> {
ServiceId.parseOrNull(threadAci)?.let {
threadServiceId != null -> {
ServiceId.parseOrNull(threadServiceId)?.let {
SignalDatabase.recipients.getOrInsertFromServiceId(it)
}
}
@@ -1601,8 +1596,8 @@ object SyncMessageProcessor {
}
private fun SyncMessage.DeleteForMe.AddressableMessage.toSyncMessageId(envelopeTimestamp: Long): MessageTable.SyncMessageId? {
return if (this.sentTimestamp != null && (this.authorAci != null || this.authorE164 != null)) {
val serviceId = ServiceId.parseOrNull(this.authorAci)
return if (this.sentTimestamp != null && (this.authorServiceId != null || this.authorE164 != null)) {
val serviceId = ServiceId.parseOrNull(this.authorServiceId)
val id = if (serviceId != null) {
SignalDatabase.recipients.getOrInsertFromServiceId(serviceId)
} else {

View File

@@ -87,7 +87,7 @@ class ReviewCardRepository {
ThreadTable threadTable = SignalDatabase.threads();
long threadId = Objects.requireNonNull(threadTable.getThreadIdFor(recipientId));
threadTable.deleteConversation(threadId);
threadTable.deleteConversation(threadId, false);
onActionCompleteListener.run();
});
}

View File

@@ -317,6 +317,9 @@ class Recipient(
/** The user's payment capability. */
val paymentActivationCapability: Capability = capabilities.paymentActivation
/** The user's payment capability. */
val deleteSyncCapability: Capability = capabilities.deleteSync
/** The state around whether we can send sealed sender to this user. */
val unidentifiedAccessMode: UnidentifiedAccessMode = if (pni.isPresent && pni == serviceId) {
UnidentifiedAccessMode.DISABLED

View File

@@ -210,7 +210,7 @@ public class MessageSender {
onMessageSent();
for (long threadId : threads) {
threadTable.update(threadId, true);
threadTable.update(threadId, true, true);
}
}
@@ -242,7 +242,7 @@ public class MessageSender {
sendMessageInternal(context, recipient, sendType, messageId, Collections.emptyList(), message.getScheduledDate() > 0);
onMessageSent();
threadTable.update(allocatedThreadId, true);
threadTable.update(allocatedThreadId, true, true);
return allocatedThreadId;
} catch (MmsException e) {
@@ -279,7 +279,7 @@ public class MessageSender {
sendMessageInternal(context, recipient, SendType.SIGNAL, messageId, jobIds, false);
onMessageSent();
threadTable.update(allocatedThreadId, true);
threadTable.update(allocatedThreadId, true, true);
return allocatedThreadId;
} catch (MmsException e) {

View File

@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSendSyncJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask
@@ -45,13 +46,24 @@ object DeleteDialog {
if (forceRemoteDelete) {
builder.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> deleteForEveryone(messageRecords, emitter) }
} else {
builder.setPositiveButton(if (isNoteToSelfDelete) R.string.ConversationFragment_delete_on_this_device else R.string.ConversationFragment_delete_for_me) { _, _ ->
val deleteSyncEnabled = Recipient.self().deleteSyncCapability.isSupported
val positiveButton = if (isNoteToSelfDelete) {
if (deleteSyncEnabled) R.string.ConversationFragment_delete else R.string.ConversationFragment_delete_on_this_device
} else {
R.string.ConversationFragment_delete_for_me
}
builder.setPositiveButton(positiveButton) { _, _ ->
DeleteProgressDialogAsyncTask(context, messageRecords) {
emitter.onSuccess(Pair(true, it))
}.executeOnExecutor(SignalExecutors.BOUNDED)
}
if (MessageConstraintsUtil.isValidRemoteDeleteSend(messageRecords, System.currentTimeMillis()) && (!isNoteToSelfDelete || TextSecurePreferences.isMultiDevice(context))) {
val canDeleteForEveryone = MessageConstraintsUtil.isValidRemoteDeleteSend(messageRecords, System.currentTimeMillis()) && !isNoteToSelfDelete
val canDeleteForEveryoneInNoteToSelf = isNoteToSelfDelete && TextSecurePreferences.isMultiDevice(context) && !deleteSyncEnabled
if (canDeleteForEveryone || canDeleteForEveryoneInNoteToSelf) {
builder.setNeutralButton(if (isNoteToSelfDelete) R.string.ConversationFragment_delete_everywhere else R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleDeleteForEveryone(context, messageRecords, emitter) }
}
}
@@ -109,7 +121,7 @@ object DeleteDialog {
}
}
if (RemoteConfig.deleteSyncEnabled) {
if (Recipient.self().deleteSyncCapability.isSupported) {
MultiDeviceDeleteSendSyncJob.enqueueMessageDeletes(messageRecords)
}

View File

@@ -98,7 +98,7 @@ public final class IdentityUtil {
} catch (MmsException e) {
throw new AssertionError(e);
}
SignalDatabase.threads().update(threadId, true);
SignalDatabase.threads().update(threadId, true, true);
}
}
}
@@ -129,7 +129,7 @@ public final class IdentityUtil {
} catch (MmsException e) {
throw new AssertionError();
}
SignalDatabase.threads().update(threadId, true);
SignalDatabase.threads().update(threadId, true, true);
}
}

View File

@@ -10,6 +10,8 @@ import org.signal.core.util.mebiBytes
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.SelectionLimits
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
import org.thoughtcrime.securesms.jobs.Svr3MirrorJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -1070,12 +1072,13 @@ object RemoteConfig {
)
/** Whether or not to delete syncing is enabled. */
@JvmStatic
@get:JvmName("deleteSyncEnabled")
val deleteSyncEnabled: Boolean by remoteBoolean(
key = "android.deleteSyncSendReceive",
key = "android.deleteSyncEnabled",
defaultValue = false,
hotSwappable = true
hotSwappable = true,
onChangeListener = {
AppDependencies.jobManager.startChain(RefreshAttributesJob()).then(RefreshOwnProfileJob()).enqueue()
}
)
/** Which phase we're in for the SVR3 migration */