diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJobTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJobTest.kt deleted file mode 100644 index fc4032ff3c..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJobTest.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.jobs - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.mockk.CapturingSlot -import io.mockk.every -import io.mockk.mockkStatic -import io.mockk.slot -import io.mockk.unmockkStatic -import okio.ByteString.Companion.toByteString -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData -import org.thoughtcrime.securesms.messages.MessageHelper -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.testing.SignalActivityRule -import org.thoughtcrime.securesms.testing.assertIs -import org.thoughtcrime.securesms.testing.assertIsNotNull -import org.thoughtcrime.securesms.testing.assertIsSize -import org.thoughtcrime.securesms.util.MessageTableTestUtils -import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.whispersystems.signalservice.api.messages.SendMessageResult -import org.whispersystems.signalservice.api.push.SignalServiceAddress -import org.whispersystems.signalservice.internal.push.Content -import java.util.Optional - -@RunWith(AndroidJUnit4::class) -class MultiDeviceDeleteSendSyncJobTest { - - @get:Rule - val harness = SignalActivityRule(createGroup = true) - - private lateinit var messageHelper: MessageHelper - - private lateinit var success: SendMessageResult - private lateinit var failure: SendMessageResult - private lateinit var content: CapturingSlot - - @Before - fun setUp() { - messageHelper = MessageHelper(harness) - - mockkStatic(TextSecurePreferences::class) - every { TextSecurePreferences.isMultiDevice(any()) } answers { - true - } - - success = SendMessageResult.success(SignalServiceAddress(Recipient.self().requireServiceId()), listOf(2), true, false, 0, Optional.empty()) - failure = SendMessageResult.networkFailure(SignalServiceAddress(Recipient.self().requireServiceId())) - content = slot() - } - - @After - fun tearDown() { - messageHelper.tearDown() - - unmockkStatic(TextSecurePreferences::class) - } - - @Test - fun messageDeletes() { - // GIVEN - val messages = mutableListOf() - messages += messageHelper.incomingText() - messages += messageHelper.incomingText() - messages += messageHelper.outgoingText() - - val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!! - val records: Set = MessageTableTestUtils.getMessages(threadId).toSet() - - // WHEN - every { AppDependencies.signalServiceMessageSender.sendSyncMessage(capture(content), any(), any()) } returns success - - val job = MultiDeviceDeleteSendSyncJob.createMessageDeletes(records) - val result = job.run() - - // THEN - result.isSuccess assertIs true - assertDeleteSync(messageHelper.alice, messages) - } - - @Test - fun groupMessageDeletes() { - // GIVEN - val messages = mutableListOf() - messages += messageHelper.incomingText(destination = messageHelper.group.recipientId) - messages += messageHelper.incomingText(destination = messageHelper.group.recipientId) - messages += messageHelper.outgoingText(conversationId = messageHelper.group.recipientId) - - val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!! - val records: Set = MessageTableTestUtils.getMessages(threadId).toSet() - - // WHEN - every { AppDependencies.signalServiceMessageSender.sendSyncMessage(capture(content), any(), any()) } returns success - - val job = MultiDeviceDeleteSendSyncJob.createMessageDeletes(records) - val result = job.run() - - // THEN - result.isSuccess assertIs true - assertDeleteSync(messageHelper.group.recipientId, messages) - } - - @Test - fun retryOfDeletes() { - // GIVEN - val alice = messageHelper.alice.toLong() - - // WHEN - every { AppDependencies.signalServiceMessageSender.sendSyncMessage(capture(content), any(), any()) } returns failure - - val job = MultiDeviceDeleteSendSyncJob( - messages = listOf(DeleteSyncJobData.AddressableMessage(alice, 1, alice)), - threads = listOf(DeleteSyncJobData.ThreadDelete(alice, listOf(DeleteSyncJobData.AddressableMessage(alice, 1, alice)))), - localOnlyThreads = listOf(DeleteSyncJobData.ThreadDelete(alice)) - ) - - val result = job.run() - val data = DeleteSyncJobData.ADAPTER.decode(job.serialize()) - - // THEN - result.isRetry assertIs true - data.messageDeletes.assertIsSize(1) - data.threadDeletes.assertIsSize(1) - data.localOnlyThreadDeletes.assertIsSize(1) - } - - private fun assertDeleteSync(conversation: RecipientId, inputMessages: List) { - val messagesMap = inputMessages.associateBy { it.timestamp } - - val content = this.content.captured - - content.syncMessage?.padding.assertIsNotNull() - content.syncMessage?.deleteForMe.assertIsNotNull() - - val deleteForMe = content.syncMessage!!.deleteForMe!! - deleteForMe.messageDeletes.assertIsSize(1) - deleteForMe.conversationDeletes.assertIsSize(0) - deleteForMe.localOnlyConversationDeletes.assertIsSize(0) - - val messageDeletes = deleteForMe.messageDeletes[0] - val conversationRecipient = Recipient.resolved(conversation) - if (conversationRecipient.isGroup) { - messageDeletes.conversation!!.threadGroupId assertIs conversationRecipient.requireGroupId().decodedId.toByteString() - } else { - messageDeletes.conversation!!.threadAci assertIs conversationRecipient.requireAci().toString() - } - - messageDeletes - .messages - .forEach { delete -> - val messageData = messagesMap[delete.sentTimestamp] - delete.sentTimestamp assertIs messageData!!.timestamp - delete.authorAci assertIs Recipient.resolved(messageData.author).requireAci().toString() - } - } -} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt index e595005ace..1910f69a91 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt @@ -6,9 +6,6 @@ package org.thoughtcrime.securesms.messages import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.mockk.every -import io.mockk.mockkStatic -import io.mockk.unmockkStatic import org.hamcrest.Matchers.greaterThan import org.junit.After import org.junit.Before @@ -28,7 +25,6 @@ import org.thoughtcrime.securesms.testing.assert import org.thoughtcrime.securesms.testing.assertIs import org.thoughtcrime.securesms.testing.assertIsNotNull import org.thoughtcrime.securesms.util.IdentityUtil -import org.thoughtcrime.securesms.util.RemoteConfig @Suppress("ClassName") @RunWith(AndroidJUnit4::class) @@ -42,16 +38,11 @@ class SyncMessageProcessorTest_synchronizeDeleteForMe { @Before fun setUp() { messageHelper = MessageHelper(harness) - - mockkStatic(RemoteConfig::class) - every { RemoteConfig.deleteSyncEnabled } returns true } @After fun tearDown() { messageHelper.tearDown() - - unmockkStatic(RemoteConfig::class) } @Test diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt index d0f65bb6e3..c9caf60bdc 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt @@ -162,12 +162,12 @@ object MessageContentFuzzer { conversation = if (conversation.isGroup) { SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) } else { - SyncMessage.DeleteForMe.ConversationIdentifier(threadAci = conversation.requireAci().toString()) + SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) }, messages = conversationDeletes.map { (author, timestamp) -> SyncMessage.DeleteForMe.AddressableMessage( - authorAci = Recipient.resolved(author).requireAci().toString(), + authorServiceId = Recipient.resolved(author).requireAci().toString(), sentTimestamp = timestamp ) } @@ -190,12 +190,12 @@ object MessageContentFuzzer { conversation = if (conversation.isGroup) { SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) } else { - SyncMessage.DeleteForMe.ConversationIdentifier(threadAci = conversation.requireAci().toString()) + SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) }, mostRecentMessages = conversationDeletes.map { (author, timestamp) -> SyncMessage.DeleteForMe.AddressableMessage( - authorAci = Recipient.resolved(author).requireAci().toString(), + authorServiceId = Recipient.resolved(author).requireAci().toString(), sentTimestamp = timestamp ) }, @@ -220,7 +220,7 @@ object MessageContentFuzzer { conversation = if (conversation.isGroup) { SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString()) } else { - SyncMessage.DeleteForMe.ConversationIdentifier(threadAci = conversation.requireAci().toString()) + SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString()) } ) } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt index 269a03b1c7..bc8d734f84 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt @@ -141,7 +141,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i))) SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i")) SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew()) - SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true)) + SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, false)) SignalDatabase.recipients.setProfileSharing(recipientId, true) SignalDatabase.recipients.markRegistered(recipientId, aci) val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair() diff --git a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt index 2bdc7558eb..cd0abe21fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt @@ -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 ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt index 492464af7f..d47d6390f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt index 9c5509b964..faa109bad1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt index 6f29043d7e..fed7e2b6de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt @@ -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!" diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 2cbe4e295b..9f8c11b21b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index b565a5cde2..d7f6c043d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -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; } 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 e164e28198..42d57f4150 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -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, contacts: List): 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) { - 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 "<" diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 0ea135928e..1f0d5206af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt index 1adccfca35..df7e4f55d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt @@ -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()) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index 8f87d5377a..bf3fadcfba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -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>>() 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 = 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) { + fun deleteConversations(selectedConversations: Set, syncThreadDeletes: Boolean = true) { val recipientIds = getRecipientIdsForThreadIds(selectedConversations) val addressableMessages = mutableListOf>>() val queries: List = 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 000eff397f..c5a4da9301 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index d331bcb6e2..04e495083c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -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 reactions) { return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index a42d8ee116..42773c0b55 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index ed42194eb6..8cf5846f6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJob.kt index c59efabc29..d9316304a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJob.kt @@ -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): MultiDeviceDeleteSendSyncJob { - val deletes = messageRecords.mapNotNull { message -> + private fun createMessageDeletes(messageRecords: Collection): List { + 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>>, isFullDelete: Boolean): MultiDeviceDeleteSendSyncJob { - val threadDeletes: List = threads.mapNotNull { (threadId, messages) -> + private fun createThreadDeletes(threads: List>>, isFullDelete: Boolean): List { + 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 ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ThreadUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ThreadUpdateJob.java index cbcc6ecc0b..ce32d9c34a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ThreadUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ThreadUpdateJob.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/TrimThreadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/TrimThreadJob.java index dd91862bfe..1c9aeef5ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/TrimThreadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/TrimThreadJob.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java index 3a59134fbb..ee31897b76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt index 829cd5096a..812eb57e74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt @@ -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()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt index a0746efa97..5dbbeae4cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt @@ -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) { _, _ -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java index 2a5614768d..f76bb2c2a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -274,7 +274,7 @@ public final class MessageRequestRepository { } ThreadTable threadTable = SignalDatabase.threads(); - threadTable.deleteConversation(threadId); + threadTable.deleteConversation(threadId, false); onMessageRequestDeleted.run(); }); 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 d966bc3813..2240eaa263 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewCardRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewCardRepository.java index eba04def86..6cfa776989 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewCardRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewCardRepository.java @@ -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(); }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt index 0bdab79d96..431e880c28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index 928ab21d99..6312bf5d7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DeleteDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/util/DeleteDialog.kt index 5f3a89b545..15f836bb1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DeleteDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DeleteDialog.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java index b4a4517d29..bd754cd60a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -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); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index 6cd421df4e..cbdacdc9c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -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 */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 979eac29c6..f2a998c1ce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -545,6 +545,8 @@ Delete for everyone Delete on this device + + Delete Delete everywhere diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index d8500ae85f..cd5b6a7f80 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -124,7 +124,8 @@ object RecipientDatabaseTestUtils { unidentifiedAccessMode = unidentifiedAccessMode, capabilities = RecipientRecord.Capabilities( rawBits = capabilities, - paymentActivation = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.PAYMENT_ACTIVATION, RecipientTable.Capabilities.BIT_LENGTH).toInt()) + paymentActivation = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.PAYMENT_ACTIVATION, RecipientTable.Capabilities.BIT_LENGTH).toInt()), + deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.DELETE_SYNC, RecipientTable.Capabilities.BIT_LENGTH).toInt()) ), storageId = storageId, mentionSetting = mentionSetting, diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt index fba36c3122..43821a9416 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt @@ -61,6 +61,7 @@ class AccountAttributes @JsonCreator constructor( @JsonProperty val stories: Boolean, @JsonProperty val giftBadges: Boolean, @JsonProperty val pni: Boolean, - @JsonProperty val paymentActivation: Boolean + @JsonProperty val paymentActivation: Boolean, + @JsonProperty val deleteSync: Boolean ) } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index abeffc9511..c910c2fdab 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -195,12 +195,16 @@ public class SignalServiceProfile { @JsonProperty private boolean paymentActivation; + @JsonProperty + private boolean deleteSync; + @JsonCreator public Capabilities() {} - public Capabilities(boolean storage, boolean paymentActivation) { + public Capabilities(boolean storage, boolean paymentActivation, boolean deleteSync) { this.storage = storage; this.paymentActivation = paymentActivation; + this.deleteSync = deleteSync; } public boolean isStorage() { @@ -210,6 +214,10 @@ public class SignalServiceProfile { public boolean isPaymentActivation() { return paymentActivation; } + + public boolean isDeleteSync() { + return deleteSync; + } } public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse() { diff --git a/libsignal-service/src/main/protowire/SignalService.proto b/libsignal-service/src/main/protowire/SignalService.proto index dfcb6b1ca9..022da8f085 100644 --- a/libsignal-service/src/main/protowire/SignalService.proto +++ b/libsignal-service/src/main/protowire/SignalService.proto @@ -657,7 +657,7 @@ message SyncMessage { message DeleteForMe { message ConversationIdentifier { oneof identifier { - string threadAci = 1; + string threadServiceId = 1; bytes threadGroupId = 2; string threadE164 = 3; } @@ -665,7 +665,7 @@ message SyncMessage { message AddressableMessage { oneof author { - string authorAci = 1; + string authorServiceId = 1; string authorE164 = 2; } optional uint64 sentTimestamp = 3;