mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-23 11:15:44 +00:00
Add single attachment delete sync.
This commit is contained in:
committed by
Greyson Parrelli
parent
ea87108def
commit
09003d85b1
@@ -5,6 +5,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.audio.AudioHash
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import java.util.UUID
|
||||
|
||||
object UriAttachmentBuilder {
|
||||
fun build(
|
||||
@@ -22,23 +23,28 @@ object UriAttachmentBuilder {
|
||||
stickerLocator: StickerLocator? = null,
|
||||
blurHash: BlurHash? = null,
|
||||
audioHash: AudioHash? = null,
|
||||
transformProperties: AttachmentTable.TransformProperties? = null
|
||||
transformProperties: AttachmentTable.TransformProperties? = null,
|
||||
uuid: UUID? = UUID.randomUUID()
|
||||
): UriAttachment {
|
||||
return UriAttachment(
|
||||
uri,
|
||||
contentType,
|
||||
transferState,
|
||||
size,
|
||||
fileName,
|
||||
voiceNote,
|
||||
borderless,
|
||||
videoGif,
|
||||
quote,
|
||||
caption,
|
||||
stickerLocator,
|
||||
blurHash,
|
||||
audioHash,
|
||||
transformProperties
|
||||
dataUri = uri,
|
||||
contentType = contentType,
|
||||
transferState = transferState,
|
||||
size = size,
|
||||
width = 0,
|
||||
height = 0,
|
||||
fileName = fileName,
|
||||
fastPreflightId = null,
|
||||
voiceNote = voiceNote,
|
||||
borderless = borderless,
|
||||
videoGif = videoGif,
|
||||
quote = quote,
|
||||
caption = caption,
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
audioHash = audioHash,
|
||||
transformProperties = transformProperties,
|
||||
uuid = uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,23 +5,31 @@
|
||||
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.net.Uri
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
||||
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Makes inserting messages through the "normal" code paths simpler. Mostly focused on incoming messages.
|
||||
@@ -68,7 +76,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun outgoingText(conversationId: RecipientId = alice, successfulSend: Boolean = true, updateMessage: (OutgoingMessage.() -> OutgoingMessage)? = null): MessageData {
|
||||
fun outgoingText(conversationId: RecipientId = alice, successfulSend: Boolean = true, updateMessage: ((OutgoingMessage) -> OutgoingMessage)? = null): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
|
||||
@@ -80,7 +88,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
||||
sentTimeMillis = messageData.timestamp,
|
||||
isUrgent = true,
|
||||
isSecure = true
|
||||
).apply { updateMessage?.invoke(this) }
|
||||
).let { updateMessage?.invoke(it) ?: it }
|
||||
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
|
||||
@@ -111,6 +119,20 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
||||
return messageData.copy(messageId = messageId)
|
||||
}
|
||||
|
||||
fun outgoingAttachment(data: ByteArray, uuid: UUID? = UUID.randomUUID()): Attachment {
|
||||
val uri: Uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
||||
|
||||
val attachment: UriAttachment = UriAttachmentBuilder.build(
|
||||
id = Random.nextLong(),
|
||||
uri = uri,
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
transformProperties = AttachmentTable.TransformProperties(),
|
||||
uuid = uuid
|
||||
)
|
||||
|
||||
return attachment
|
||||
}
|
||||
|
||||
fun outgoingGroupChange(): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
@@ -238,6 +260,20 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): MessageData {
|
||||
startTime = nextStartTime()
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncDeleteForMeAttachment(conversationId, message, uuid, digest, plainTextHash),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next "sentTimestamp" for current + [nextMessageOffset]th message. Useful for early message processing and future message timestamps.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,11 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -24,8 +28,11 @@ import org.thoughtcrime.securesms.testing.MessageContentFuzzer.DeleteForMeSync
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assert
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsSize
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -531,4 +538,154 @@ class SyncMessageProcessorTest_synchronizeDeleteForMe {
|
||||
harness.inMemoryLogger.flush()
|
||||
harness.inMemoryLogger.entries().filter { it.message?.contains("Thread is not local only") == true }.size assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleAttachmentDeletes() {
|
||||
// GIVEN
|
||||
val message1 = messageHelper.outgoingText { message ->
|
||||
message.copy(
|
||||
attachments = listOf(
|
||||
messageHelper.outgoingAttachment(byteArrayOf(1, 2, 3)),
|
||||
messageHelper.outgoingAttachment(byteArrayOf(2, 3, 4), null),
|
||||
messageHelper.outgoingAttachment(byteArrayOf(5, 6, 7), null),
|
||||
messageHelper.outgoingAttachment(byteArrayOf(10, 11, 12))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
var attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
attachments assertIsSize 4
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
|
||||
// Has all three
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[0].attachmentId,
|
||||
attachment = attachments[0].copy(digest = byteArrayOf(attachments[0].attachmentId.id.toByte())),
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
|
||||
// Missing uuid and digest
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[1].attachmentId,
|
||||
attachment = attachments[1],
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
|
||||
// Missing uuid and plain text
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[2].attachmentId,
|
||||
attachment = attachments[2].copy(digest = byteArrayOf(attachments[2].attachmentId.id.toByte())),
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
SignalDatabase.rawDatabase.update(AttachmentTable.TABLE_NAME).values(AttachmentTable.DATA_HASH_END to null).where("${AttachmentTable.ID} = ?", attachments[2].attachmentId).run()
|
||||
|
||||
// Different has all three
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[3].attachmentId,
|
||||
attachment = attachments[3].copy(digest = byteArrayOf(attachments[3].attachmentId.id.toByte())),
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
|
||||
attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[0].uuid,
|
||||
attachments[0].remoteDigest,
|
||||
attachments[0].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
var updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 3
|
||||
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[0].attachmentId }
|
||||
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[1].uuid,
|
||||
attachments[1].remoteDigest,
|
||||
attachments[1].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 2
|
||||
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[1].attachmentId }
|
||||
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[2].uuid,
|
||||
attachments[2].remoteDigest,
|
||||
attachments[2].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 1
|
||||
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[2].attachmentId }
|
||||
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[3].uuid,
|
||||
attachments[3].remoteDigest,
|
||||
attachments[3].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 0
|
||||
|
||||
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.copy(
|
||||
uuid: UUID? = this.uuid,
|
||||
digest: ByteArray? = this.remoteDigest
|
||||
): Attachment {
|
||||
return DatabaseAttachment(
|
||||
attachmentId = this.attachmentId,
|
||||
mmsId = this.mmsId,
|
||||
hasData = this.hasData,
|
||||
hasThumbnail = false,
|
||||
hasArchiveThumbnail = false,
|
||||
contentType = this.contentType,
|
||||
transferProgress = this.transferState,
|
||||
size = this.size,
|
||||
fileName = this.fileName,
|
||||
cdn = this.cdn,
|
||||
location = this.remoteLocation,
|
||||
key = this.remoteKey,
|
||||
digest = digest,
|
||||
incrementalDigest = this.incrementalDigest,
|
||||
incrementalMacChunkSize = this.incrementalMacChunkSize,
|
||||
fastPreflightId = this.fastPreflightId,
|
||||
voiceNote = this.voiceNote,
|
||||
borderless = this.borderless,
|
||||
videoGif = this.videoGif,
|
||||
width = this.width,
|
||||
height = this.height,
|
||||
quote = this.quote,
|
||||
caption = this.caption,
|
||||
stickerLocator = this.stickerLocator,
|
||||
blurHash = this.blurHash,
|
||||
audioHash = this.audioHash,
|
||||
transformProperties = this.transformProperties,
|
||||
displayOrder = this.displayOrder,
|
||||
uploadTimestamp = this.uploadTimestamp,
|
||||
dataHash = this.dataHash,
|
||||
archiveCdn = this.archiveCdn,
|
||||
archiveThumbnailCdn = this.archiveThumbnailCdn,
|
||||
archiveMediaName = this.archiveMediaName,
|
||||
archiveMediaId = this.archiveMediaId,
|
||||
thumbnailRestoreState = this.thumbnailRestoreState,
|
||||
uuid = uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@ package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.thoughtcrime.securesms.messages.TestMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
@@ -229,6 +232,35 @@ object MessageContentFuzzer {
|
||||
).build()
|
||||
}
|
||||
|
||||
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): Content {
|
||||
val conversation = Recipient.resolved(conversationId)
|
||||
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage(
|
||||
deleteForMe = SyncMessage.DeleteForMe(
|
||||
attachmentDeletes = listOf(
|
||||
SyncMessage.DeleteForMe.AttachmentDelete(
|
||||
conversation = if (conversation.isGroup) {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||
} else {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||
},
|
||||
targetMessage = SyncMessage.DeleteForMe.AddressableMessage(
|
||||
authorServiceId = Recipient.resolved(message.first).requireAci().toString(),
|
||||
sentTimestamp = message.second
|
||||
),
|
||||
uuid = uuid?.let { UuidUtil.toByteString(it) },
|
||||
fallbackDigest = digest?.toByteString(),
|
||||
fallbackPlaintextHash = plainTextHash?.let { Base64.decodeOrNull(it)?.toByteString() }
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that may be:
|
||||
* - A text body
|
||||
@@ -373,7 +405,8 @@ object MessageContentFuzzer {
|
||||
data class DeleteForMeSync(
|
||||
val conversationId: RecipientId,
|
||||
val messages: List<Pair<RecipientId, Long>>,
|
||||
val isFullDelete: Boolean = true
|
||||
val isFullDelete: Boolean = true,
|
||||
val attachments: List<Pair<Long, AttachmentTable.SyncAttachmentId>> = emptyList()
|
||||
) {
|
||||
constructor(conversationId: RecipientId, vararg messages: Pair<RecipientId, Long>) : this(conversationId, messages.toList())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.database.Cursor
|
||||
import android.util.Base64
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.hasSize
|
||||
@@ -9,6 +8,7 @@ import org.hamcrest.Matchers.`is`
|
||||
import org.hamcrest.Matchers.not
|
||||
import org.hamcrest.Matchers.notNullValue
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.select
|
||||
@@ -67,27 +67,31 @@ fun CountDownLatch.awaitFor(duration: Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
fun dumpTableToLogs(tag: String = "TestUtils", table: String) {
|
||||
dumpTable(table).forEach { Log.d(tag, it.toString()) }
|
||||
fun dumpTableToLogs(tag: String = "TestUtils", table: String, columns: Set<String>? = null) {
|
||||
dumpTable(table, columns).forEach { Log.d(tag, it.toString()) }
|
||||
}
|
||||
|
||||
fun dumpTable(table: String): List<List<Pair<String, String?>>> {
|
||||
fun dumpTable(table: String, columns: Set<String>?): List<List<Pair<String, String?>>> {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select()
|
||||
.from(table)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
|
||||
val index = cursor.getColumnIndex(column)
|
||||
var data: String? = when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
val map: List<Pair<String, String?>> = cursor.columnNames.mapNotNull { column ->
|
||||
if (columns == null || columns.contains(column)) {
|
||||
val index = cursor.getColumnIndex(column)
|
||||
var data: String? = when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_BLOB -> Hex.toStringCondensed(cursor.getBlob(index))
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
|
||||
column to data
|
||||
column to data
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user