diff --git a/app/src/androidTest/assets/backupTests/notification_profile_00.binproto b/app/src/androidTest/assets/backupTests/notification_profile_00.binproto index 0509b3220a..ba86841538 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_00.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_00.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_01.binproto b/app/src/androidTest/assets/backupTests/notification_profile_01.binproto index 8b86f53682..4bc4b96657 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_01.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_01.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_02.binproto b/app/src/androidTest/assets/backupTests/notification_profile_02.binproto index 26a5aa9ef0..ff058e2ff7 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_02.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_02.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_03.binproto b/app/src/androidTest/assets/backupTests/notification_profile_03.binproto index 9c6f4e3ac7..de20ced25e 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_03.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_03.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_04.binproto b/app/src/androidTest/assets/backupTests/notification_profile_04.binproto index 6fdadbcd63..f85c699e35 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_04.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_04.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_05.binproto b/app/src/androidTest/assets/backupTests/notification_profile_05.binproto index 9052299c45..04c8048e18 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_05.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_05.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_06.binproto b/app/src/androidTest/assets/backupTests/notification_profile_06.binproto index 954f60944b..636634a2d9 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_06.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_06.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_07.binproto b/app/src/androidTest/assets/backupTests/notification_profile_07.binproto index 33b5feb1de..4da8e55dc8 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_07.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_07.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_08.binproto b/app/src/androidTest/assets/backupTests/notification_profile_08.binproto index c0dcb5d7f2..1aad305397 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_08.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_08.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_09.binproto b/app/src/androidTest/assets/backupTests/notification_profile_09.binproto index cd608e7c1d..e76246a52f 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_09.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_09.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_10.binproto b/app/src/androidTest/assets/backupTests/notification_profile_10.binproto index caa09453eb..04311e2fa5 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_10.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_10.binproto differ diff --git a/app/src/androidTest/assets/backupTests/notification_profile_11.binproto b/app/src/androidTest/assets/backupTests/notification_profile_11.binproto index be77d1542b..092a36ff20 100644 Binary files a/app/src/androidTest/assets/backupTests/notification_profile_11.binproto and b/app/src/androidTest/assets/backupTests/notification_profile_11.binproto differ 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 09ba782afc..679bfa64af 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 @@ -187,6 +187,10 @@ object ImportSkips { return log(0, "Failed to parse chatFolderId for the provided chat folder.") } + fun notificationProfileIdNotFound(): String { + return log(0, "Failed to parse notificationProfileId for the provided notification profile.") + } + private fun log(sentTimestamp: Long, message: String): String { return "[SKIP][$sentTimestamp] $message" } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/NotificationProfileProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/NotificationProfileProcessor.kt index 81a110c157..dc7dcb073c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/NotificationProfileProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/NotificationProfileProcessor.kt @@ -5,10 +5,12 @@ package org.thoughtcrime.securesms.backup.v2.processor +import okio.ByteString.Companion.toByteString import org.signal.core.util.insertInto import org.signal.core.util.logging.Log import org.signal.core.util.toInt import org.thoughtcrime.securesms.backup.v2.ExportState +import org.thoughtcrime.securesms.backup.v2.ImportSkips import org.thoughtcrime.securesms.backup.v2.ImportState import org.thoughtcrime.securesms.backup.v2.proto.Frame import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter @@ -20,7 +22,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.serialize import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile import org.thoughtcrime.securesms.recipients.RecipientId -import java.lang.IllegalStateException +import org.whispersystems.signalservice.api.util.UuidUtil import java.time.DayOfWeek import org.thoughtcrime.securesms.backup.v2.proto.NotificationProfile as NotificationProfileProto @@ -41,6 +43,12 @@ object NotificationProfileProcessor { } fun import(profile: NotificationProfileProto, importState: ImportState) { + val notificationProfileUuid = UuidUtil.parseOrNull(profile.id) + if (notificationProfileUuid == null) { + ImportSkips.notificationProfileIdNotFound() + return + } + val profileId = SignalDatabase .writableDatabase .insertInto(NotificationProfileTable.TABLE_NAME) @@ -50,7 +58,8 @@ object NotificationProfileProcessor { NotificationProfileTable.COLOR to (AvatarColor.fromColor(profile.color) ?: AvatarColor.random()).serialize(), NotificationProfileTable.CREATED_AT to profile.createdAtMs, NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls.toInt(), - NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt() + NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt(), + NotificationProfileTable.NOTIFICATION_PROFILE_ID to notificationProfileUuid.toString() ) .run() @@ -89,6 +98,7 @@ object NotificationProfileProcessor { private fun NotificationProfile.toBackupFrame(includeRecipient: (RecipientId) -> Boolean): Frame { val profile = NotificationProfileProto( + id = UuidUtil.toByteArray(this.notificationProfileId.uuid).toByteString(), name = this.name, emoji = this.emoji.takeIf { it.isNotBlank() }, color = this.color.colorInt(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt b/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt index 1cbb931d10..885a4f58ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt @@ -11,12 +11,14 @@ import org.signal.core.util.logging.Log import org.signal.core.util.requireBoolean import org.signal.core.util.requireInt import org.signal.core.util.requireLong +import org.signal.core.util.requireNonNullString import org.signal.core.util.requireString import org.signal.core.util.toInt import org.signal.core.util.update import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile +import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule import org.thoughtcrime.securesms.recipients.RecipientId import java.time.DayOfWeek @@ -46,6 +48,7 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase const val CREATED_AT = "created_at" const val ALLOW_ALL_CALLS = "allow_all_calls" const val ALLOW_ALL_MENTIONS = "allow_all_mentions" + const val NOTIFICATION_PROFILE_ID = "notification_profile_id" val CREATE_TABLE = """ CREATE TABLE $TABLE_NAME ( @@ -55,7 +58,8 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase $COLOR TEXT NOT NULL, $CREATED_AT INTEGER NOT NULL, $ALLOW_ALL_CALLS INTEGER NOT NULL DEFAULT 0, - $ALLOW_ALL_MENTIONS INTEGER NOT NULL DEFAULT 0 + $ALLOW_ALL_MENTIONS INTEGER NOT NULL DEFAULT 0, + $NOTIFICATION_PROFILE_ID TEXT DEFAULT NULL ) """ } @@ -110,12 +114,14 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase db.beginTransaction() try { + val notificationProfileId = NotificationProfileId.generate() val profileValues = ContentValues().apply { put(NotificationProfileTable.NAME, name) put(NotificationProfileTable.EMOJI, emoji) put(NotificationProfileTable.COLOR, color.serialize()) put(NotificationProfileTable.CREATED_AT, createdAt) put(NotificationProfileTable.ALLOW_ALL_CALLS, 1) + put(NotificationProfileTable.NOTIFICATION_PROFILE_ID, notificationProfileId.serialize()) } val profileId = db.insert(NotificationProfileTable.TABLE_NAME, null, profileValues) @@ -140,7 +146,8 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase emoji = emoji, createdAt = createdAt, schedule = getProfileSchedule(profileId), - allowAllCalls = true + allowAllCalls = true, + notificationProfileId = notificationProfileId ) ) } finally { @@ -323,7 +330,8 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase allowAllCalls = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_CALLS), allowAllMentions = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_MENTIONS), schedule = getProfileSchedule(profileId), - allowedMembers = getProfileAllowedMembers(profileId) + allowedMembers = getProfileAllowedMembers(profileId), + notificationProfileId = NotificationProfileId.from(cursor.requireNonNullString(NotificationProfileTable.NOTIFICATION_PROFILE_ID)) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 019a2479a4..966299bc91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -125,6 +125,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V267_FixGroupInvita import org.thoughtcrime.securesms.database.helpers.migration.V268_FixInAppPaymentsErrorStateConsistency import org.thoughtcrime.securesms.database.helpers.migration.V269_BackupMediaSnapshotChanges import org.thoughtcrime.securesms.database.helpers.migration.V270_FixChatFolderColumnsForStorageSync +import org.thoughtcrime.securesms.database.helpers.migration.V271_AddNotificationProfileIdColumn import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase /** @@ -255,10 +256,11 @@ object SignalDatabaseMigrations { 267 to V267_FixGroupInvitationDeclinedUpdate, 268 to V268_FixInAppPaymentsErrorStateConsistency, 269 to V269_BackupMediaSnapshotChanges, - 270 to V270_FixChatFolderColumnsForStorageSync + 270 to V270_FixChatFolderColumnsForStorageSync, + 271 to V271_AddNotificationProfileIdColumn ) - const val DATABASE_VERSION = 270 + const val DATABASE_VERSION = 271 @JvmStatic fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V271_AddNotificationProfileIdColumn.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V271_AddNotificationProfileIdColumn.kt new file mode 100644 index 0000000000..6be622c3c7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V271_AddNotificationProfileIdColumn.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import org.signal.core.util.readToList +import org.signal.core.util.requireLong +import org.thoughtcrime.securesms.database.SQLiteDatabase +import java.util.UUID + +/** + * Add notification_profile_id column to Notification Profiles to support backups. + */ +@Suppress("ClassName") +object V271_AddNotificationProfileIdColumn : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE notification_profile ADD COLUMN notification_profile_id TEXT DEFAULT NULL") + + db.rawQuery("SELECT _id FROM notification_profile") + .readToList { it.requireLong("_id") } + .forEach { id -> + db.execSQL("UPDATE notification_profile SET notification_profile_id = '${UUID.randomUUID()}' WHERE _id = $id") + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfile.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfile.kt index 822bf7e26e..61bfa9c8da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfile.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfile.kt @@ -12,7 +12,8 @@ data class NotificationProfile( val allowAllCalls: Boolean = true, val allowAllMentions: Boolean = false, val schedule: NotificationProfileSchedule, - val allowedMembers: Set = emptySet() + val allowedMembers: Set = emptySet(), + val notificationProfileId: NotificationProfileId ) : Comparable { fun isRecipientAllowed(id: RecipientId): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfileId.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfileId.kt new file mode 100644 index 0000000000..2888f6cb60 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfileId.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.notifications.profiles + +import org.signal.core.util.DatabaseId +import org.whispersystems.signalservice.api.util.UuidUtil +import java.util.UUID + +/** + * Typed wrapper for notification profile uuid. + */ +data class NotificationProfileId(val uuid: UUID) : DatabaseId { + companion object { + fun from(id: String): NotificationProfileId { + return NotificationProfileId(UuidUtil.parseOrThrow(id)) + } + + fun generate(): NotificationProfileId { + return NotificationProfileId(UUID.randomUUID()) + } + } + + override fun serialize(): String { + return uuid.toString() + } +} diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index be94e934ea..5ac5ceb260 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -724,11 +724,30 @@ message FilePointer { message InvalidAttachmentLocator { } + // References attachments in a local encrypted backup. + // Importers should first attempt to read the file from the local backup, + // and on failure fallback to backup and transit cdn if possible. + message LocalLocator { + string mediaName = 1; + // Separate key used to encrypt this file for the local backup. + // Generally required. Missing field indicates attachment was not + // available locally when the backup was generated, but remote + // backup or transit info was available. + optional bytes localKey = 2; + bytes remoteKey = 3; + bytes remoteDigest = 4; + uint32 size = 5; + optional uint32 backupCdnNumber = 6; + optional string transitCdnKey = 7; + optional uint32 transitCdnNumber = 8; + } + // If unset, importers should consider it to be an InvalidAttachmentLocator without throwing an error. oneof locator { BackupLocator backupLocator = 1; AttachmentLocator attachmentLocator = 2; InvalidAttachmentLocator invalidAttachmentLocator = 3; + LocalLocator localLocator = 12; } optional string contentType = 4; @@ -1291,6 +1310,7 @@ message NotificationProfile { uint32 scheduleStartTime = 9; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345) uint32 scheduleEndTime = 10; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345) repeated DayOfWeek scheduleDaysEnabled = 11; + bytes id = 12; // should be 16 bytes } message ChatFolder { diff --git a/app/src/test/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfilesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfilesTest.kt index 93e45b2621..6c8d80f23d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfilesTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/notifications/profiles/NotificationProfilesTest.kt @@ -41,7 +41,8 @@ class NotificationProfilesTest { "first", "", createdAt = 1000L, - schedule = NotificationProfileSchedule(1) + schedule = NotificationProfileSchedule(1), + notificationProfileId = NotificationProfileId.generate() ) private val second = NotificationProfile( @@ -49,7 +50,8 @@ class NotificationProfilesTest { "second", "", createdAt = 2000L, - schedule = NotificationProfileSchedule(2) + schedule = NotificationProfileSchedule(2), + notificationProfileId = NotificationProfileId.generate() ) private lateinit var notificationProfileValues: NotificationProfileValues