diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_00.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_00.binproto new file mode 100644 index 0000000000..a7549ad005 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_00.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_01.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_01.binproto new file mode 100644 index 0000000000..103251933a Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_01.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_02.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_02.binproto new file mode 100644 index 0000000000..f5f3c61905 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_02.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_03.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_03.binproto new file mode 100644 index 0000000000..6aff5bbf0c Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_03.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_04.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_04.binproto new file mode 100644 index 0000000000..01a2cd5c8c Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_04.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_05.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_05.binproto new file mode 100644 index 0000000000..2f6f2f9258 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_05.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_06.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_06.binproto new file mode 100644 index 0000000000..729b67c143 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_06.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_07.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_07.binproto new file mode 100644 index 0000000000..defcf467cb Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_07.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_08.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_08.binproto new file mode 100644 index 0000000000..6dc43d4ae6 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_08.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_09.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_09.binproto new file mode 100644 index 0000000000..540b319162 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_09.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_10.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_10.binproto new file mode 100644 index 0000000000..9ef8daab39 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_10.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_11.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_11.binproto new file mode 100644 index 0000000000..654866d38d Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_11.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_12.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_12.binproto new file mode 100644 index 0000000000..16d2304088 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_12.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_13.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_13.binproto new file mode 100644 index 0000000000..7c32a44129 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_13.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_14.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_14.binproto new file mode 100644 index 0000000000..1030a76c57 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_14.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_update_00.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_update_00.binproto new file mode 100644 index 0000000000..cee46bfb8d Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_update_00.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_update_01.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_update_01.binproto new file mode 100644 index 0000000000..a432ad73c6 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_update_01.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_pin_message_update_02.binproto b/app/src/androidTest/assets/backupTests/chat_item_pin_message_update_02.binproto new file mode 100644 index 0000000000..64db0e17f6 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_pin_message_update_02.binproto differ diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt index d61388b94a..fb831a9e19 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt @@ -216,6 +216,11 @@ class ArchiveImportExportTests { runTests { it.startsWith("chat_item_poll_") } } +// @Test + fun chatItemPinMessage() { + runTests { it.startsWith("chat_item_pin_message_") } + } + // @Test fun notificationProfiles() { runTests { it.startsWith("notification_profile_") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt index c1d2200855..6b16d9cd34 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt @@ -139,6 +139,10 @@ object ExportSkips { return log(sentTimestamp, "Poll was not in a group chat.") } + fun pinMessageIsInvalid(sentTimestamp: Long): String { + return log(sentTimestamp, "Pin message update was invalid.") + } + fun individualChatUpdateInWrongTypeOfChat(sentTimestamp: Long): String { return log(sentTimestamp, "A chat update that only makes sense for individual chats was found in a different kind of chat.") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableArchiveExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableArchiveExtensions.kt index 46270a3754..4e8a7e14da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableArchiveExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableArchiveExtensions.kt @@ -65,7 +65,10 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self ${MessageTable.MISMATCHED_IDENTITIES}, ${MessageTable.TYPE}, ${MessageTable.MESSAGE_EXTRAS}, - ${MessageTable.VIEW_ONCE} + ${MessageTable.VIEW_ONCE}, + ${MessageTable.PINNED_UNTIL}, + ${MessageTable.PINNING_MESSAGE_ID}, + ${MessageTable.PINNED_AT} ) WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 """.trimMargin() @@ -155,7 +158,10 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self MessageTable.TYPE, MessageTable.MESSAGE_EXTRAS, MessageTable.VIEW_ONCE, - PARENT_STORY_ID + PARENT_STORY_ID, + MessageTable.PINNED_UNTIL, + MessageTable.PINNING_MESSAGE_ID, + MessageTable.PINNED_AT ) .from("${MessageTable.TABLE_NAME} INDEXED BY $dateReceivedIndex") .where("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND ($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds}) AND $DATE_RECEIVED >= $lastSeenReceivedTime $cutoffQuery") diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt index fc1d0a827e..d86270239c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt @@ -9,6 +9,7 @@ import android.database.Cursor import okio.ByteString.Companion.toByteString import org.json.JSONArray import org.json.JSONException +import org.signal.core.models.ServiceId import org.signal.core.util.Base64 import org.signal.core.util.EventTimer import org.signal.core.util.Hex @@ -53,6 +54,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall import org.thoughtcrime.securesms.backup.v2.proto.LearnedProfileChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification +import org.thoughtcrime.securesms.backup.v2.proto.PinMessageUpdate import org.thoughtcrime.securesms.backup.v2.proto.Poll import org.thoughtcrime.securesms.backup.v2.proto.PollTerminateUpdate import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate @@ -413,6 +415,16 @@ class ChatItemArchiveExporter( transformTimer.emit("poll") } + MessageTypes.isPinnedMessageUpdate(record.type) -> { + val pinMessageUpdate = record.toRemotePinMessageUpdate(exportState) + if (pinMessageUpdate == null) { + Log.w(TAG, ExportSkips.pinMessageIsInvalid(record.dateSent)) + continue + } + builder.updateMessage = ChatUpdateMessage(pinMessage = pinMessageUpdate) + transformTimer.emit("pin-message") + } + else -> { val attachments = extraData.attachmentsById[record.id] val sticker = attachments?.firstOrNull { dbAttachment -> dbAttachment.isSticker } @@ -595,6 +607,7 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: Recipien expiresInMs = record.expiresIn.takeIf { it > 0 } revisions = emptyList() sms = record.type.isSmsType() + pinDetails = record.toPinDetails() when (direction) { Direction.DIRECTIONLESS -> { directionless = ChatItem.DirectionlessMessageDetails() @@ -847,6 +860,27 @@ private fun BackupMessageRecord.toRemotePollTerminateUpdate(): PollTerminateUpda ) } +private fun BackupMessageRecord.toPinDetails(): ChatItem.PinDetails? { + return if (this.pinnedAt == 0L || this.pinnedUntil == 0L) { + null + } else { + ChatItem.PinDetails( + pinnedAtTimestamp = this.pinnedAt, + pinExpiresAtTimestamp = this.pinnedUntil.takeIf { it != MessageTable.PIN_FOREVER }, + pinNeverExpires = (this.pinnedUntil == MessageTable.PIN_FOREVER).takeIf { it } + ) + } +} + +private fun BackupMessageRecord.toRemotePinMessageUpdate(exportState: ExportState): PinMessageUpdate? { + val pinMessage = this.messageExtras?.pinnedMessage ?: return null + val authorId = exportState.aciToRecipientId[ServiceId.ACI.parseOrNull(pinMessage.targetAuthorAci).toString()] ?: return null + return PinMessageUpdate( + targetSentTimestamp = pinMessage.targetTimestamp, + authorId = authorId + ) +} + private fun BackupMessageRecord.toRemoteSharedContact(attachments: List?): Contact? { if (this.sharedContacts.isNullOrEmpty()) { return null @@ -1591,7 +1625,8 @@ private fun Long.isDirectionlessType(): Boolean { MessageTypes.isGroupUpdate(this) || MessageTypes.isGroupV1MigrationEvent(this) || MessageTypes.isGroupQuit(this) || - MessageTypes.isPollTerminate(this) + MessageTypes.isPollTerminate(this) || + MessageTypes.isPinnedMessageUpdate(this) } private fun Long.isIdentityVerifyType(): Boolean { @@ -1776,6 +1811,8 @@ private fun Cursor.toBackupMessageRecord(pastIds: Set, backupStartTime: Lo messageExtras = messageExtras.parseMessageExtras(), viewOnce = this.requireBoolean(MessageTable.VIEW_ONCE), parentStoryId = this.requireLong(MessageTable.PARENT_STORY_ID), + pinnedAt = this.requireLong(MessageTable.PINNED_AT), + pinnedUntil = this.requireLong(MessageTable.PINNED_UNTIL), messageExtrasSize = messageExtras?.size ?: 0 ) } @@ -1816,6 +1853,8 @@ private class BackupMessageRecord( val baseType: Long, val messageExtras: MessageExtras?, val viewOnce: Boolean, + val pinnedAt: Long, + val pinnedUntil: Long, private val messageExtrasSize: Int ) { val estimatedSizeInBytes: Int = (body?.length ?: 0) + diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt index 14a51c0300..aa04260860 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt @@ -64,6 +64,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescrip import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras import org.thoughtcrime.securesms.database.model.databaseprotos.PaymentTombstone +import org.thoughtcrime.securesms.database.model.databaseprotos.PinnedMessage import org.thoughtcrime.securesms.database.model.databaseprotos.PollTerminate import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent @@ -137,7 +138,10 @@ class ChatItemArchiveImporter( MessageTable.LATEST_REVISION_ID, MessageTable.REVISION_NUMBER, MessageTable.PARENT_STORY_ID, - MessageTable.NOTIFIED + MessageTable.NOTIFIED, + MessageTable.PINNED_UNTIL, + MessageTable.PINNING_MESSAGE_ID, + MessageTable.PINNED_AT ) private val REACTION_COLUMNS = arrayOf( @@ -343,6 +347,32 @@ class ChatItemArchiveImporter( SignalDatabase.polls.endPoll(pollId = pollId, endingMessageId = endPollMessageId) } } + } else if (this.updateMessage.pinMessage != null) { + followUps += { pinUpdateMessageId -> + val targetAuthorId = importState.remoteToLocalRecipientId[updateMessage.pinMessage.authorId] + if (targetAuthorId != null) { + val pinnedMessageId = SignalDatabase.messages.getMessageFor(updateMessage.pinMessage.targetSentTimestamp, targetAuthorId)?.id ?: -1 + val messageExtras = MessageExtras( + pinnedMessage = PinnedMessage( + pinnedMessageId = pinnedMessageId, + targetAuthorAci = recipients.getRecord(targetAuthorId).aci!!.toByteString(), + targetTimestamp = updateMessage.pinMessage.targetSentTimestamp + ) + ) + + db.update(MessageTable.TABLE_NAME) + .values(MessageTable.MESSAGE_EXTRAS to messageExtras.encode()) + .where("${MessageTable.ID} = ?", pinUpdateMessageId) + .run() + + if (pinnedMessageId != -1L) { + db.update(MessageTable.TABLE_NAME) + .values(MessageTable.PINNING_MESSAGE_ID to pinUpdateMessageId) + .where("${MessageTable.ID} = ?", pinnedMessageId) + .run() + } + } + } } } @@ -645,6 +675,12 @@ class ChatItemArchiveImporter( contentValues.put(MessageTable.REMOTE_DELETED, 0) contentValues.put(MessageTable.PARENT_STORY_ID, 0) + if (this.pinDetails != null) { + val pinnedUntil = if (this.pinDetails.pinNeverExpires == true) MessageTable.PIN_FOREVER else this.pinDetails.pinExpiresAtTimestamp + contentValues.put(MessageTable.PINNED_UNTIL, pinnedUntil ?: 0) + contentValues.put(MessageTable.PINNED_AT, this.pinDetails.pinnedAtTimestamp) + } + when { this.standardMessage != null -> contentValues.addStandardMessage(this.standardMessage) this.remoteDeletedMessage != null -> contentValues.put(MessageTable.REMOTE_DELETED, 1) @@ -846,6 +882,9 @@ class ChatItemArchiveImporter( updateMessage.pollTerminate != null -> { typeFlags = MessageTypes.SPECIAL_TYPE_POLL_TERMINATE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv()) } + updateMessage.pinMessage != null -> { + typeFlags = MessageTypes.SPECIAL_TYPE_PINNED_MESSAGE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv()) + } updateMessage.sessionSwitchover != null -> { typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv()) val sessionSwitchoverDetails = SessionSwitchoverEvent(e164 = updateMessage.sessionSwitchover.e164.toString()).encode() diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index 15abe2f367..16f6f134ed 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -460,6 +460,14 @@ message ChatItem { message DirectionlessMessageDetails { } + message PinDetails { + uint64 pinnedAtTimestamp = 1; + oneof pinExpiry { + uint64 pinExpiresAtTimestamp = 2; // timestamp when the pin should expire + bool pinNeverExpires = 3; + } + } + uint64 chatId = 1; // conversation id uint64 authorId = 2; // recipient id uint64 dateSent = 3; @@ -488,6 +496,8 @@ message ChatItem { DirectStoryReplyMessage directStoryReplyMessage = 19; // group story reply messages are not backed up Poll poll = 20; } + + PinDetails pinDetails = 21; // only set if message is pinned } message SendStatus { @@ -898,6 +908,7 @@ message ChatUpdateMessage { GroupCall groupCall = 8; LearnedProfileChatUpdate learnedProfileChange = 9; PollTerminateUpdate pollTerminate = 10; + PinMessageUpdate pinMessage = 11; } } @@ -1268,6 +1279,11 @@ message PollTerminateUpdate { string question = 2; // Between 1-100 characters } +message PinMessageUpdate { + uint64 targetSentTimestamp = 1; + uint64 authorId = 2; // recipient id +} + message StickerPack { bytes packId = 1; bytes packKey = 2; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c7d02e093d..88c8e82eeb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ androidx-window = "1.3.0" glide = "4.15.1" gradle = "8.9.0" kotlin = "2.2.20" -libsignal-client = "0.86.6" +libsignal-client = "0.86.8" mp4parser = "1.9.39" android-gradle-plugin = "8.10.1" accompanist = "0.28.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e3431d7adb..849e788fa3 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -15400,28 +15400,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - + + + - - - - + + - - - - - + + + - - - - + +