mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Add notification profiles to storage service.
This commit is contained in:
committed by
Cody Henthorne
parent
07d961fc09
commit
e3ee3d3dba
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user