Add notification profiles to storage service.

This commit is contained in:
Michelle Tang
2025-06-04 11:02:43 -04:00
committed by Cody Henthorne
parent 07d961fc09
commit e3ee3d3dba
27 changed files with 1132 additions and 152 deletions

View File

@@ -24,18 +24,12 @@ import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderId
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
import org.thoughtcrime.securesms.database.ThreadTable.Companion.ID
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.thoughtcrime.securesms.storage.StorageSyncModels
import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.api.util.UuidUtil
@@ -684,10 +678,10 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
RemoteChatFolderRecord.FolderType.UNKNOWN -> throw AssertionError("Folder type cannot be unknown")
},
includedChats = record.proto.includedRecipients
.mapNotNull { remoteRecipient -> getRecipientIdFromRemoteRecipient(remoteRecipient) }
.mapNotNull { remoteRecipient -> StorageSyncModels.remoteToLocalRecipient(remoteRecipient) }
.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) },
excludedChats = record.proto.excludedRecipients
.mapNotNull { remoteRecipient -> getRecipientIdFromRemoteRecipient(remoteRecipient) }
.mapNotNull { remoteRecipient -> StorageSyncModels.remoteToLocalRecipient(remoteRecipient) }
.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) },
chatFolderId = chatFolderId,
storageServiceId = StorageId.forChatFolder(record.id.raw),
@@ -696,34 +690,6 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
)
}
/**
* Parses a remote recipient into a local one. Used when configuring the chats of a remote chat folder into a local one.
*/
private fun getRecipientIdFromRemoteRecipient(remoteRecipient: RemoteChatFolderRecord.Recipient): Recipient? {
return if (remoteRecipient.contact != null) {
val serviceId = ServiceId.parseOrNull(remoteRecipient.contact!!.serviceId)
val e164 = remoteRecipient.contact!!.e164
Recipient.externalPush(SignalServiceAddress(serviceId, e164))
} else if (remoteRecipient.legacyGroupId != null) {
try {
Recipient.externalGroupExact(GroupId.v1(remoteRecipient.legacyGroupId!!.toByteArray()))
} catch (e: BadGroupIdException) {
Log.w(TAG, "Failed to parse groupV1 ID!", e)
null
}
} else if (remoteRecipient.groupMasterKey != null) {
try {
Recipient.externalGroupExact(GroupId.v2(GroupMasterKey(remoteRecipient.groupMasterKey!!.toByteArray())))
} catch (e: InvalidInputException) {
Log.w(TAG, "Failed to parse groupV2 master key!", e)
null
}
} else {
Log.w(TAG, "Could not find recipient")
null
}
}
private fun Collection<Long>.toContentValues(chatFolderId: Long, membershipType: MembershipType): List<ContentValues> {
return map {
contentValuesOf(

View File

@@ -5,23 +5,40 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteConstraintException
import androidx.core.content.contentValuesOf
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.exists
import org.signal.core.util.hasUnknownFields
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.readToMap
import org.signal.core.util.readToSingleLong
import org.signal.core.util.readToSingleObject
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.select
import org.signal.core.util.toInt
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
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 org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.storage.StorageSyncModels
import org.thoughtcrime.securesms.storage.StorageSyncModels.toLocal
import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.api.util.UuidUtil
import java.time.DayOfWeek
import kotlin.time.Duration.Companion.days
/**
* Database for maintaining Notification Profiles, Notification Profile Schedules, and Notification Profile allowed memebers.
@@ -30,6 +47,7 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
companion object {
private val TAG = Log.tag(NotificationProfileTable::class)
private val DELETED_LIFESPAN: Long = 30.days.inWholeMilliseconds
@JvmField
val CREATE_TABLE: Array<String> = arrayOf(NotificationProfileTable.CREATE_TABLE, NotificationProfileScheduleTable.CREATE_TABLE, NotificationProfileAllowedMembersTable.CREATE_TABLE)
@@ -49,17 +67,23 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
const val ALLOW_ALL_CALLS = "allow_all_calls"
const val ALLOW_ALL_MENTIONS = "allow_all_mentions"
const val NOTIFICATION_PROFILE_ID = "notification_profile_id"
const val DELETED_TIMESTAMP_MS = "deleted_timestamp_ms"
const val STORAGE_SERVICE_ID = "storage_service_id"
const val STORAGE_SERVICE_PROTO = "storage_service_proto"
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
$NAME TEXT NOT NULL UNIQUE,
$NAME TEXT NOT NULL,
$EMOJI TEXT NOT NULL,
$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,
$NOTIFICATION_PROFILE_ID TEXT DEFAULT NULL
$NOTIFICATION_PROFILE_ID TEXT DEFAULT NULL,
$DELETED_TIMESTAMP_MS INTEGER DEFAULT 0,
$STORAGE_SERVICE_ID TEXT DEFAULT NULL,
$STORAGE_SERVICE_PROTO TEXT DEFAULT NULL
)
"""
}
@@ -114,7 +138,12 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
db.beginTransaction()
try {
if (isDuplicateName(name)) {
return NotificationProfileChangeResult.DuplicateName
}
val notificationProfileId = NotificationProfileId.generate()
val storageServiceId = StorageSyncHelper.generateKey()
val profileValues = ContentValues().apply {
put(NotificationProfileTable.NAME, name)
put(NotificationProfileTable.EMOJI, emoji)
@@ -122,12 +151,10 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
put(NotificationProfileTable.CREATED_AT, createdAt)
put(NotificationProfileTable.ALLOW_ALL_CALLS, 1)
put(NotificationProfileTable.NOTIFICATION_PROFILE_ID, notificationProfileId.serialize())
put(NotificationProfileTable.STORAGE_SERVICE_ID, Base64.encodeWithPadding(storageServiceId))
}
val profileId = db.insert(NotificationProfileTable.TABLE_NAME, null, profileValues)
if (profileId < 0) {
return NotificationProfileChangeResult.DuplicateName
}
val scheduleValues = ContentValues().apply {
put(NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID, profileId)
@@ -147,7 +174,8 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
createdAt = createdAt,
schedule = getProfileSchedule(profileId),
allowAllCalls = true,
notificationProfileId = notificationProfileId
notificationProfileId = notificationProfileId,
storageServiceId = StorageId.forNotificationProfile(storageServiceId)
)
)
} finally {
@@ -157,6 +185,10 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
}
fun updateProfile(profileId: Long, name: String, emoji: String): NotificationProfileChangeResult {
if (isDuplicateName(name, profileId)) {
return NotificationProfileChangeResult.DuplicateName
}
val profileValues = ContentValues().apply {
put(NotificationProfileTable.NAME, name)
put(NotificationProfileTable.EMOJI, emoji)
@@ -164,37 +196,38 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profileId), profileValues)
return try {
val count = writableDatabase.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
if (count > 0) {
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
}
NotificationProfileChangeResult.Success(getProfile(profileId)!!)
} catch (e: SQLiteConstraintException) {
NotificationProfileChangeResult.DuplicateName
val count = writableDatabase.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
if (count > 0) {
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
}
return NotificationProfileChangeResult.Success(getProfile(profileId)!!)
}
fun updateProfile(profile: NotificationProfile): NotificationProfileChangeResult {
if (isDuplicateName(profile.name, profile.id)) {
return NotificationProfileChangeResult.DuplicateName
}
val db = writableDatabase
db.beginTransaction()
try {
val storageServiceId = profile.storageServiceId?.raw ?: StorageSyncHelper.generateKey()
val storageServiceProto = if (profile.storageServiceProto != null) Base64.encodeWithPadding(profile.storageServiceProto) else null
val profileValues = ContentValues().apply {
put(NotificationProfileTable.NAME, profile.name)
put(NotificationProfileTable.EMOJI, profile.emoji)
put(NotificationProfileTable.ALLOW_ALL_CALLS, profile.allowAllCalls.toInt())
put(NotificationProfileTable.ALLOW_ALL_MENTIONS, profile.allowAllMentions.toInt())
put(NotificationProfileTable.STORAGE_SERVICE_ID, Base64.encodeWithPadding(storageServiceId))
put(NotificationProfileTable.STORAGE_SERVICE_PROTO, storageServiceProto)
}
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profile.id), profileValues)
try {
db.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
} catch (e: SQLiteConstraintException) {
return NotificationProfileChangeResult.DuplicateName
}
db.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
updateSchedule(profile.schedule, true)
@@ -280,16 +313,16 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
return getProfile(profileId)!!
}
/**
* Returns all undeleted notification profiles
*/
fun getProfiles(): List<NotificationProfile> {
val profiles: MutableList<NotificationProfile> = mutableListOf()
readableDatabase.query(NotificationProfileTable.TABLE_NAME, null, null, null, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
profiles += getProfile(cursor)
}
}
return profiles
return readableDatabase
.select()
.from(NotificationProfileTable.TABLE_NAME)
.where("${NotificationProfileTable.DELETED_TIMESTAMP_MS} = 0")
.run()
.readToList { cursor -> getProfile(cursor) }
}
fun getProfile(profileId: Long): NotificationProfile? {
@@ -302,11 +335,196 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
}
}
fun getProfile(query: SqlUtil.Query): NotificationProfile? {
return readableDatabase
.select()
.from(NotificationProfileTable.TABLE_NAME)
.where(query.where, query.whereArgs)
.run()
.readToSingleObject { cursor -> getProfile(cursor) }
}
fun deleteProfile(profileId: Long) {
writableDatabase.delete(NotificationProfileTable.TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(profileId))
writableDatabase.withinTransaction { db ->
db.update(NotificationProfileTable.TABLE_NAME)
.values(NotificationProfileTable.DELETED_TIMESTAMP_MS to System.currentTimeMillis())
.where("${NotificationProfileTable.ID} = ?", profileId)
.run()
}
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
}
fun markNeedsSync(profileId: Long) {
writableDatabase.withinTransaction {
rotateStorageId(profileId)
}
}
fun applyStorageIdUpdate(id: NotificationProfileId, storageId: StorageId) {
applyStorageIdUpdates(hashMapOf(id to storageId))
}
fun applyStorageIdUpdates(storageIds: Map<NotificationProfileId, StorageId>) {
writableDatabase.withinTransaction { db ->
storageIds.forEach { (notificationProfileId, storageId) ->
db.update(NotificationProfileTable.TABLE_NAME)
.values(NotificationProfileTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(storageId.raw))
.where("${NotificationProfileTable.NOTIFICATION_PROFILE_ID} = ?", notificationProfileId.serialize())
.run()
}
}
}
fun insertNotificationProfileFromStorageSync(notificationProfileRecord: SignalNotificationProfileRecord) {
val profile = notificationProfileRecord.proto
writableDatabase.withinTransaction { db ->
val storageServiceProto = if (notificationProfileRecord.proto.hasUnknownFields()) Base64.encodeWithPadding(notificationProfileRecord.serializedUnknowns!!) else null
val id = db.insertInto(NotificationProfileTable.TABLE_NAME)
.values(
contentValuesOf(
NotificationProfileTable.NAME to profile.name,
NotificationProfileTable.EMOJI to profile.emoji.orEmpty(),
NotificationProfileTable.COLOR to (AvatarColor.fromColor(profile.color)?.serialize() ?: NotificationProfile.DEFAULT_NOTIFICATION_PROFILE_COLOR.serialize()),
NotificationProfileTable.CREATED_AT to profile.createdAtMs,
NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls,
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions,
NotificationProfileTable.NOTIFICATION_PROFILE_ID to NotificationProfileId(UuidUtil.parseOrThrow(profile.id)).serialize(),
NotificationProfileTable.DELETED_TIMESTAMP_MS to profile.deletedAtTimestampMs,
NotificationProfileTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(notificationProfileRecord.id.raw),
NotificationProfileTable.STORAGE_SERVICE_PROTO to storageServiceProto
)
)
.run()
db.insertInto(NotificationProfileScheduleTable.TABLE_NAME)
.values(
contentValuesOf(
NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID to id,
NotificationProfileScheduleTable.ENABLED to profile.scheduleEnabled.toInt(),
NotificationProfileScheduleTable.START to profile.scheduleStartTime,
NotificationProfileScheduleTable.END to profile.scheduleEndTime,
NotificationProfileScheduleTable.DAYS_ENABLED to profile.scheduleDaysEnabled.map { it.toLocal() }.toSet().serialize()
)
)
.run()
profile.allowedMembers
.mapNotNull { remoteRecipient -> StorageSyncModels.remoteToLocalRecipient(remoteRecipient) }
.forEach {
db.insertInto(NotificationProfileAllowedMembersTable.TABLE_NAME)
.values(
NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID to id,
NotificationProfileAllowedMembersTable.RECIPIENT_ID to it.id.serialize()
)
.run()
}
}
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
}
fun updateNotificationProfileFromStorageSync(notificationProfileRecord: SignalNotificationProfileRecord) {
val profile = notificationProfileRecord.proto
val notificationProfileId = NotificationProfileId(UuidUtil.parseOrThrow(profile.id))
val profileId = readableDatabase
.select(NotificationProfileTable.ID)
.from(NotificationProfileTable.TABLE_NAME)
.where("${NotificationProfileTable.NOTIFICATION_PROFILE_ID} = ?", notificationProfileId.serialize())
.run()
.readToSingleLong()
val scheduleId = readableDatabase
.select(NotificationProfileScheduleTable.ID)
.from(NotificationProfileScheduleTable.TABLE_NAME)
.where("${NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID} = ?", profileId)
.run()
.readToSingleLong()
updateProfile(
NotificationProfile(
id = profileId,
name = profile.name,
emoji = profile.emoji.orEmpty(),
color = AvatarColor.fromColor(profile.color) ?: NotificationProfile.DEFAULT_NOTIFICATION_PROFILE_COLOR,
createdAt = profile.createdAtMs,
allowAllCalls = profile.allowAllCalls,
allowAllMentions = profile.allowAllMentions,
schedule = NotificationProfileSchedule(
scheduleId,
profile.scheduleEnabled,
profile.scheduleStartTime,
profile.scheduleEndTime,
profile.scheduleDaysEnabled.map { it.toLocal() }.toSet()
),
allowedMembers = profile.allowedMembers.mapNotNull { remoteRecipient -> StorageSyncModels.remoteToLocalRecipient(remoteRecipient)?.id }.toSet(),
notificationProfileId = notificationProfileId,
deletedTimestampMs = profile.deletedAtTimestampMs,
storageServiceId = StorageId.forNotificationProfile(notificationProfileRecord.id.raw),
storageServiceProto = notificationProfileRecord.serializedUnknowns
)
)
}
fun getStorageSyncIdsMap(): Map<NotificationProfileId, StorageId> {
return readableDatabase
.select(NotificationProfileTable.NOTIFICATION_PROFILE_ID, NotificationProfileTable.STORAGE_SERVICE_ID)
.from(NotificationProfileTable.TABLE_NAME)
.where("${NotificationProfileTable.STORAGE_SERVICE_ID} IS NOT NULL")
.run()
.readToMap { cursor ->
val id = NotificationProfileId.from(cursor.requireNonNullString(NotificationProfileTable.NOTIFICATION_PROFILE_ID))
val encodedKey = cursor.requireNonNullString(NotificationProfileTable.STORAGE_SERVICE_ID)
val key = Base64.decodeOrThrow(encodedKey)
id to StorageId.forNotificationProfile(key)
}
}
fun getStorageSyncIds(): List<StorageId> {
return readableDatabase
.select(NotificationProfileTable.STORAGE_SERVICE_ID)
.from(NotificationProfileTable.TABLE_NAME)
.where("${NotificationProfileTable.STORAGE_SERVICE_ID} IS NOT NULL")
.run()
.readToList { cursor ->
val encodedKey = cursor.requireNonNullString(NotificationProfileTable.STORAGE_SERVICE_ID)
val key = Base64.decodeOrThrow(encodedKey)
StorageId.forNotificationProfile(key)
}.also { Log.i(TAG, "${it.size} profiles have storage ids.") }
}
/**
* Removes storageIds from notification profiles that have been deleted for [DELETED_LIFESPAN].
*/
fun removeStorageIdsFromOldDeletedProfiles(now: Long): Int {
return writableDatabase
.update(NotificationProfileTable.TABLE_NAME)
.values(NotificationProfileTable.STORAGE_SERVICE_ID to null)
.where("${NotificationProfileTable.STORAGE_SERVICE_ID} NOT NULL AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} > 0 AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} < ?", now - DELETED_LIFESPAN)
.run()
}
/**
* Removes storageIds of profiles that are local only and deleted
*/
fun removeStorageIdsFromLocalOnlyDeletedProfiles(storageIds: Collection<StorageId>): Int {
var updated = 0
SqlUtil.buildCollectionQuery(NotificationProfileTable.STORAGE_SERVICE_ID, storageIds.map { Base64.encodeWithPadding(it.raw) }, "${NotificationProfileTable.DELETED_TIMESTAMP_MS} > 0 AND")
.forEach {
updated += writableDatabase.update(
NotificationProfileTable.TABLE_NAME,
contentValuesOf(NotificationProfileTable.STORAGE_SERVICE_ID to null),
it.where,
it.whereArgs
)
}
return updated
}
override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
val count = writableDatabase
.update(NotificationProfileAllowedMembersTable.TABLE_NAME)
@@ -331,7 +549,10 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
allowAllMentions = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_MENTIONS),
schedule = getProfileSchedule(profileId),
allowedMembers = getProfileAllowedMembers(profileId),
notificationProfileId = NotificationProfileId.from(cursor.requireNonNullString(NotificationProfileTable.NOTIFICATION_PROFILE_ID))
notificationProfileId = NotificationProfileId.from(cursor.requireNonNullString(NotificationProfileTable.NOTIFICATION_PROFILE_ID)),
deletedTimestampMs = cursor.requireLong(NotificationProfileTable.DELETED_TIMESTAMP_MS),
storageServiceId = cursor.requireString(NotificationProfileTable.STORAGE_SERVICE_ID)?.let { StorageId.forNotificationProfile(Base64.decodeNullableOrThrow(it)) },
storageServiceProto = Base64.decodeOrNull(cursor.requireString(NotificationProfileTable.STORAGE_SERVICE_PROTO))
)
}
@@ -372,6 +593,24 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
return allowed
}
private fun rotateStorageId(id: Long) {
writableDatabase
.update(NotificationProfileTable.TABLE_NAME)
.values(NotificationProfileTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
.where("${NotificationProfileTable.ID} = ?", id)
.run()
}
/**
* Checks that there is no other notification profile with the same [name]
*/
private fun isDuplicateName(name: String, id: Long = -1): Boolean {
return readableDatabase
.exists(NotificationProfileTable.TABLE_NAME)
.where("${NotificationProfileTable.NAME} = ? AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} = 0 AND ${NotificationProfileTable.ID} != ?", name, id)
.run()
}
sealed class NotificationProfileChangeResult {
data class Success(val notificationProfile: NotificationProfile) : NotificationProfileChangeResult()
object DuplicateName : NotificationProfileChangeResult()

View File

@@ -131,6 +131,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V273_FixUnreadOrigi
import org.thoughtcrime.securesms.database.helpers.migration.V274_BackupMediaSnapshotLastSeenOnRemote
import org.thoughtcrime.securesms.database.helpers.migration.V275_EnsureDefaultAllChatsFolder
import org.thoughtcrime.securesms.database.helpers.migration.V276_AttachmentCdnDefaultValueMigration
import org.thoughtcrime.securesms.database.helpers.migration.V277_AddNotificationProfileStorageSync
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -267,10 +268,11 @@ object SignalDatabaseMigrations {
273 to V273_FixUnreadOriginalMessages,
274 to V274_BackupMediaSnapshotLastSeenOnRemote,
275 to V275_EnsureDefaultAllChatsFolder,
276 to V276_AttachmentCdnDefaultValueMigration
276 to V276_AttachmentCdnDefaultValueMigration,
277 to V277_AddNotificationProfileStorageSync
)
const val DATABASE_VERSION = 276
const val DATABASE_VERSION = 277
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,50 @@
/*
* 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.Base64
import org.signal.core.util.readToList
import org.signal.core.util.requireLong
import org.thoughtcrime.securesms.database.SQLiteDatabase
import org.thoughtcrime.securesms.storage.StorageSyncHelper
/**
* Adds columns to notification profiles to support storage service, drops names unique constraint, sets all profiles with a storage service id.
*/
object V277_AddNotificationProfileStorageSync : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// Rebuild table to drop unique constraint on 'name'
db.execSQL(
"""
CREATE TABLE notification_profile_temp (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
emoji TEXT NOT NULL,
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,
notification_profile_id TEXT DEFAULT NULL,
deleted_timestamp_ms INTEGER DEFAULT 0,
storage_service_id TEXT DEFAULT NULL,
storage_service_proto TEXT DEFAULT NULL
)
""".trimIndent()
)
db.execSQL("INSERT INTO notification_profile_temp (_id, name, emoji, color, created_at, allow_all_calls, allow_all_mentions, notification_profile_id) SELECT _id, name, emoji, color, created_at, allow_all_calls, allow_all_mentions, notification_profile_id FROM notification_profile")
db.execSQL("DROP TABLE notification_profile")
db.execSQL("ALTER TABLE notification_profile_temp RENAME TO notification_profile")
// Initialize all profiles with a storage service id
db.rawQuery("SELECT _id FROM notification_profile")
.readToList { it.requireLong("_id") }
.forEach { id ->
db.execSQL("UPDATE notification_profile SET storage_service_id = '${Base64.encodeWithPadding(StorageSyncHelper.generateKey())}' WHERE _id = $id")
}
}
}