Add chat folder support to storage service.

This commit is contained in:
Michelle Tang
2025-04-04 11:36:17 -04:00
parent f6ecb572b1
commit eb1cf8d62f
26 changed files with 1131 additions and 84 deletions

View File

@@ -3,23 +3,43 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import androidx.core.content.contentValuesOf
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.count
import org.signal.core.util.delete
import org.signal.core.util.groupBy
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.readToSingleInt
import org.signal.core.util.readToSingleLongOrNull
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.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.whispersystems.signalservice.api.storage.SignalChatFolderRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord as RemoteChatFolderRecord
/**
* Stores chat folders and the chats that belong in each chat folder
@@ -27,6 +47,8 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : DatabaseTable(context, databaseHelper), ThreadIdDatabaseReference {
companion object {
private val TAG = Log.tag(ChatFolderTable::class.java)
@JvmField
val CREATE_TABLE: Array<String> = arrayOf(ChatFolderTable.CREATE_TABLE, ChatFolderMembershipTable.CREATE_TABLE)
@@ -43,7 +65,9 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
ChatFolderTable.FOLDER_TYPE to ChatFolderRecord.FolderType.ALL.value,
ChatFolderTable.SHOW_INDIVIDUAL to 1,
ChatFolderTable.SHOW_GROUPS to 1,
ChatFolderTable.SHOW_MUTED to 1
ChatFolderTable.SHOW_MUTED to 1,
ChatFolderTable.CHAT_FOLDER_ID to ChatFolderId.generate().toString(),
ChatFolderTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(StorageSyncHelper.generateKey())
)
}
}
@@ -61,8 +85,11 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
const val SHOW_MUTED = "show_muted"
const val SHOW_INDIVIDUAL = "show_individual"
const val SHOW_GROUPS = "show_groups"
const val IS_MUTED = "is_muted"
const val FOLDER_TYPE = "folder_type"
const val CHAT_FOLDER_ID = "chat_folder_id"
const val STORAGE_SERVICE_ID = "storage_service_id"
const val STORAGE_SERVICE_PROTO = "storage_service_proto"
const val DELETED_TIMESTAMP_MS = "deleted_timestamp_ms"
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
@@ -73,8 +100,11 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
$SHOW_MUTED INTEGER DEFAULT 0,
$SHOW_INDIVIDUAL INTEGER DEFAULT 0,
$SHOW_GROUPS INTEGER DEFAULT 0,
$IS_MUTED INTEGER DEFAULT 0,
$FOLDER_TYPE INTEGER DEFAULT ${ChatFolderRecord.FolderType.CUSTOM.value}
$FOLDER_TYPE INTEGER DEFAULT ${ChatFolderRecord.FolderType.CUSTOM.value},
$CHAT_FOLDER_ID TEXT DEFAULT NULL,
$STORAGE_SERVICE_ID TEXT DEFAULT NULL,
$STORAGE_SERVICE_PROTO TEXT DEFAULT NULL,
$DELETED_TIMESTAMP_MS INTEGER DEFAULT 0
)
"""
@@ -119,10 +149,28 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
.run()
}
/**
* Returns a single chat folder that corresponds to that query.
* Assumes query will only match to one chat folder.
*/
fun getChatFolder(query: SqlUtil.Query): ChatFolderRecord? {
val id = readableDatabase
.select(ChatFolderTable.ID)
.from(ChatFolderTable.TABLE_NAME)
.where(query.where, query.whereArgs)
.run()
.readToSingleLongOrNull()
return getChatFolder(id)
}
/**
* Returns a single chat folder that corresponds to that id
*/
fun getChatFolder(id: Long): ChatFolderRecord {
fun getChatFolder(id: Long?): ChatFolderRecord? {
if (id == null) {
return null
}
val includedChats: Map<Long, List<Long>> = getIncludedChats(id)
val excludedChats: Map<Long, List<Long>> = getExcludedChats(id)
@@ -142,7 +190,11 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
showGroupChats = cursor.requireBoolean(ChatFolderTable.SHOW_GROUPS),
folderType = ChatFolderRecord.FolderType.deserialize(cursor.requireInt(ChatFolderTable.FOLDER_TYPE)),
includedChats = includedChats[id] ?: emptyList(),
excludedChats = excludedChats[id] ?: emptyList()
excludedChats = excludedChats[id] ?: emptyList(),
chatFolderId = ChatFolderId.from(cursor.requireNonNullString(ChatFolderTable.CHAT_FOLDER_ID)),
storageServiceId = StorageId.forChatFolder(Base64.decodeNullableOrThrow(cursor.requireString(ChatFolderTable.STORAGE_SERVICE_ID))),
storageServiceProto = Base64.decodeOrNull(cursor.requireString(ChatFolderTable.STORAGE_SERVICE_PROTO)),
deletedTimestampMs = cursor.requireLong(ChatFolderTable.DELETED_TIMESTAMP_MS)
)
}
@@ -150,15 +202,16 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
}
/**
* Maps the chat folder ids to its corresponding chat folder
* Returns all non-deleted chat folders
*/
fun getChatFolders(): List<ChatFolderRecord> {
fun getCurrentChatFolders(): List<ChatFolderRecord> {
val includedChats: Map<Long, List<Long>> = getIncludedChats()
val excludedChats: Map<Long, List<Long>> = getExcludedChats()
val folders = readableDatabase
.select()
.from(ChatFolderTable.TABLE_NAME)
.where("${ChatFolderTable.DELETED_TIMESTAMP_MS} = 0")
.orderBy(ChatFolderTable.POSITION)
.run()
.readToList { cursor ->
@@ -173,7 +226,10 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
showGroupChats = cursor.requireBoolean(ChatFolderTable.SHOW_GROUPS),
folderType = ChatFolderRecord.FolderType.deserialize(cursor.requireInt(ChatFolderTable.FOLDER_TYPE)),
includedChats = includedChats[id] ?: emptyList(),
excludedChats = excludedChats[id] ?: emptyList()
excludedChats = excludedChats[id] ?: emptyList(),
chatFolderId = ChatFolderId.from(cursor.requireNonNullString((ChatFolderTable.CHAT_FOLDER_ID))),
storageServiceId = StorageId.forChatFolder(Base64.decodeNullableOrThrow(cursor.requireString(RecipientTable.STORAGE_SERVICE_ID))),
storageServiceProto = Base64.decodeOrNull(cursor.requireString(ChatFolderTable.STORAGE_SERVICE_PROTO))
)
}
@@ -194,54 +250,13 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
}
/**
* Maps a chat folder id to all of its corresponding included chats.
* If an id is not specified, all chat folder ids will be mapped.
*/
private fun getIncludedChats(id: Long? = null): Map<Long, List<Long>> {
val whereQuery = if (id != null) {
"${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.INCLUDED.value} AND ${ChatFolderMembershipTable.CHAT_FOLDER_ID} = $id"
} else {
"${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.INCLUDED.value}"
}
return readableDatabase
.select()
.from(ChatFolderMembershipTable.TABLE_NAME)
.where(whereQuery)
.run()
.groupBy { cursor ->
cursor.requireLong(ChatFolderMembershipTable.CHAT_FOLDER_ID) to cursor.requireLong(ChatFolderMembershipTable.THREAD_ID)
}
}
/**
* Maps a chat folder id to all of its corresponding excluded chats.
* If an id is not specified, all chat folder ids will be mapped.
*/
private fun getExcludedChats(id: Long? = null): Map<Long, List<Long>> {
val whereQuery = if (id != null) {
"${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.EXCLUDED.value} AND ${ChatFolderMembershipTable.CHAT_FOLDER_ID} = $id"
} else {
"${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.EXCLUDED.value}"
}
return readableDatabase
.select()
.from(ChatFolderMembershipTable.TABLE_NAME)
.where(whereQuery)
.run()
.groupBy { cursor ->
cursor.requireLong(ChatFolderMembershipTable.CHAT_FOLDER_ID) to cursor.requireLong(ChatFolderMembershipTable.THREAD_ID)
}
}
/**
* Returns the number of folders a user has, including the default 'All Chats'
* Returns the number of non-deleted folders a user has, including the default 'All Chats'
*/
fun getFolderCount(): Int {
return readableDatabase
.count()
.from(ChatFolderTable.TABLE_NAME)
.where("${ChatFolderTable.DELETED_TIMESTAMP_MS} = 0")
.run()
.readToSingleInt()
}
@@ -257,6 +272,8 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
.run()
.readToSingleInt(0) + 1
val storageId = chatFolder.storageServiceId?.raw ?: StorageSyncHelper.generateKey()
val storageServiceProto = if (chatFolder.storageServiceProto != null) Base64.encodeWithPadding(chatFolder.storageServiceProto) else null
val id = db.insertInto(ChatFolderTable.TABLE_NAME)
.values(
contentValuesOf(
@@ -265,7 +282,11 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
ChatFolderTable.SHOW_MUTED to chatFolder.showMutedChats,
ChatFolderTable.SHOW_INDIVIDUAL to chatFolder.showIndividualChats,
ChatFolderTable.SHOW_GROUPS to chatFolder.showGroupChats,
ChatFolderTable.POSITION to position
ChatFolderTable.POSITION to if (chatFolder.position == -1 && chatFolder.deletedTimestampMs == 0L) position else chatFolder.position,
ChatFolderTable.CHAT_FOLDER_ID to chatFolder.chatFolderId.toString(),
ChatFolderTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(storageId),
ChatFolderTable.STORAGE_SERVICE_PROTO to storageServiceProto,
ChatFolderTable.DELETED_TIMESTAMP_MS to chatFolder.deletedTimestampMs
)
)
.run(SQLiteDatabase.CONFLICT_IGNORE)
@@ -298,14 +319,18 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
* Updates the details for an existing folder like name, chat types, etc.
*/
fun updateFolder(chatFolder: ChatFolderRecord) {
val storageServiceProto = if (chatFolder.storageServiceProto != null) Base64.encodeWithPadding(chatFolder.storageServiceProto) else null
writableDatabase.withinTransaction { db ->
db.update(ChatFolderTable.TABLE_NAME)
.values(
ChatFolderTable.NAME to chatFolder.name,
ChatFolderTable.POSITION to chatFolder.position,
ChatFolderTable.SHOW_UNREAD to chatFolder.showUnread,
ChatFolderTable.SHOW_MUTED to chatFolder.showMutedChats,
ChatFolderTable.SHOW_INDIVIDUAL to chatFolder.showIndividualChats,
ChatFolderTable.SHOW_GROUPS to chatFolder.showGroupChats
ChatFolderTable.SHOW_GROUPS to chatFolder.showGroupChats,
ChatFolderTable.STORAGE_SERVICE_PROTO to storageServiceProto,
ChatFolderTable.DELETED_TIMESTAMP_MS to chatFolder.deletedTimestampMs
)
.where("${ChatFolderTable.ID} = ?", chatFolder.id)
.run(SQLiteDatabase.CONFLICT_IGNORE)
@@ -336,11 +361,23 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
}
/**
* Deletes a chat folder
* Marks a chat folder as deleted and removes all associated chats
*/
fun deleteChatFolder(chatFolder: ChatFolderRecord) {
writableDatabase.withinTransaction { db ->
db.delete(ChatFolderTable.TABLE_NAME, "${ChatFolderTable.ID} = ?", SqlUtil.buildArgs(chatFolder.id))
db.update(ChatFolderTable.TABLE_NAME)
.values(
ChatFolderTable.DELETED_TIMESTAMP_MS to System.currentTimeMillis(),
ChatFolderTable.POSITION to -1
)
.where("$ID = ?", chatFolder.id)
.run()
db.delete(ChatFolderMembershipTable.TABLE_NAME)
.where("${ChatFolderMembershipTable.CHAT_FOLDER_ID} = ?", chatFolder.id)
.run()
resetPositions()
AppDependencies.databaseObserver.notifyChatFolderObservers()
}
}
@@ -405,6 +442,253 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
}
}
/**
* Saves the new storage id for a chat folder
*/
fun applyStorageIdUpdate(id: ChatFolderId, storageId: StorageId) {
applyStorageIdUpdates(hashMapOf(id to storageId))
}
/**
* Saves the new storage ids for all the chat folders in the map
*/
fun applyStorageIdUpdates(storageIds: Map<ChatFolderId, StorageId>) {
writableDatabase.withinTransaction { db ->
storageIds.forEach { (chatFolderId, storageId) ->
db.update(ChatFolderTable.TABLE_NAME)
.values(ChatFolderTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(storageId.raw))
.where("${ChatFolderTable.CHAT_FOLDER_ID} = ?", chatFolderId.toString())
.run()
}
}
}
/**
* Maps all chat folder ids to storage ids
*/
fun getStorageSyncIdsMap(): Map<ChatFolderId, StorageId> {
return readableDatabase
.select(ChatFolderTable.CHAT_FOLDER_ID, ChatFolderTable.STORAGE_SERVICE_ID)
.from(ChatFolderTable.TABLE_NAME)
.run()
.readToMap { cursor ->
val id = ChatFolderId.from(cursor.requireNonNullString(ChatFolderTable.CHAT_FOLDER_ID))
val encodedKey = cursor.requireNonNullString(ChatFolderTable.STORAGE_SERVICE_ID)
val key = Base64.decodeOrThrow(encodedKey)
id to StorageId.forChatFolder(key)
}
}
/**
* Returns a list of all the storage ids used in chat folders
*/
fun getStorageSyncIds(): List<StorageId> {
return readableDatabase
.select(ChatFolderTable.STORAGE_SERVICE_ID)
.from(ChatFolderTable.TABLE_NAME)
.run()
.readToList { cursor ->
val encodedKey = cursor.requireNonNullString(ChatFolderTable.STORAGE_SERVICE_ID)
val key = Base64.decodeOrThrow(encodedKey)
StorageId.forChatFolder(key)
}
}
/**
* Creates a new storage id for a folder. Assumption is that StorageSyncHelper.scheduleSyncForDataChange() will be called after.
*/
fun markNeedsSync(folderId: Long) {
markNeedsSync(listOf(folderId))
}
/**
* Creates new storage ids for multiple folders. Assumption is that StorageSyncHelper.scheduleSyncForDataChange() will be called after.
*/
fun markNeedsSync(folderIds: List<Long>) {
writableDatabase.withinTransaction {
for (id in folderIds) {
rotateStorageId(id)
}
}
}
/**
* Inserts a remote chat folder into the database
*/
fun insertChatFolderFromStorageSync(record: SignalChatFolderRecord) {
if (record.proto.folderType == RemoteChatFolderRecord.FolderType.ALL) {
Log.i(TAG, "All chats should already exists. Avoiding inserting another and only updating relevant fields")
val storageServiceProto = if (record.proto.hasUnknownFields()) Base64.encodeWithPadding(record.serializedUnknowns!!) else null
writableDatabase.withinTransaction { db ->
db.update(ChatFolderTable.TABLE_NAME)
.values(
ChatFolderTable.POSITION to record.proto.position,
ChatFolderTable.CHAT_FOLDER_ID to ChatFolderId.from(UuidUtil.parseOrThrow(record.proto.identifier)).toString(),
ChatFolderTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(record.id.raw),
ChatFolderTable.STORAGE_SERVICE_PROTO to storageServiceProto
)
.where("${ChatFolderTable.FOLDER_TYPE} = ?", ChatFolderRecord.FolderType.ALL.value)
.run(SQLiteDatabase.CONFLICT_IGNORE)
}
} else {
createFolder(remoteChatFolderRecordToLocal(record))
}
}
/**
* Updates an existing local chat folder with the details of the remote chat folder
*/
fun updateChatFolderFromStorageSync(record: SignalChatFolderRecord) {
updateFolder(remoteChatFolderRecordToLocal(record))
}
/**
* Maps a chat folder id to all of its corresponding included chats.
* If an id is not specified, all chat folder ids will be mapped.
*/
private fun getIncludedChats(id: Long? = null): Map<Long, List<Long>> {
val whereQuery = if (id != null) {
"${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.INCLUDED.value} AND ${ChatFolderMembershipTable.CHAT_FOLDER_ID} = $id"
} else {
"${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.INCLUDED.value}"
}
return readableDatabase
.select()
.from(ChatFolderMembershipTable.TABLE_NAME)
.where(whereQuery)
.run()
.groupBy { cursor ->
cursor.requireLong(ChatFolderMembershipTable.CHAT_FOLDER_ID) to cursor.requireLong(ChatFolderMembershipTable.THREAD_ID)
}
}
/**
* Maps a chat folder id to all of its corresponding excluded chats.
* If an id is not specified, all chat folder ids will be mapped.
*/
private fun getExcludedChats(id: Long? = null): Map<Long, List<Long>> {
val whereQuery = if (id != null) {
"${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.EXCLUDED.value} AND ${ChatFolderMembershipTable.CHAT_FOLDER_ID} = $id"
} else {
"${ChatFolderMembershipTable.MEMBERSHIP_TYPE} = ${MembershipType.EXCLUDED.value}"
}
return readableDatabase
.select()
.from(ChatFolderMembershipTable.TABLE_NAME)
.where(whereQuery)
.run()
.groupBy { cursor ->
cursor.requireLong(ChatFolderMembershipTable.CHAT_FOLDER_ID) to cursor.requireLong(ChatFolderMembershipTable.THREAD_ID)
}
}
/**
* Ensures that chat folders positions are 0-indexed and consecutive
*/
private fun resetPositions() {
val folders = readableDatabase
.select(ChatFolderTable.ID)
.from(ChatFolderTable.TABLE_NAME)
.where("${ChatFolderTable.DELETED_TIMESTAMP_MS} = 0")
.orderBy("${ChatFolderTable.POSITION} ASC")
.run()
.readToList { cursor -> cursor.requireLong(ChatFolderTable.ID) }
writableDatabase.withinTransaction { db ->
folders.forEachIndexed { index, id ->
db.update(ChatFolderTable.TABLE_NAME)
.values(ChatFolderTable.POSITION to index)
.where("${ChatFolderTable.ID} = ?", id)
.run(SQLiteDatabase.CONFLICT_IGNORE)
}
}
}
/**
* Rotates the storage id for a chat folder
*/
private fun rotateStorageId(id: Long) {
writableDatabase
.update(ChatFolderTable.TABLE_NAME)
.values(ChatFolderTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
.where("${ChatFolderTable.ID} = ?", id)
.run()
}
/**
* Given a [ChatFolderId], return the id for that folder
*/
private fun getIdByRemoteChatFolderId(chatFolderId: ChatFolderId): Long? {
return readableDatabase
.select(ChatFolderTable.ID)
.from(ChatFolderTable.TABLE_NAME)
.where("${ChatFolderTable.CHAT_FOLDER_ID} = ?", chatFolderId.toString())
.run()
.readToSingleLongOrNull()
}
/**
* Parses a remote chat folder [SignalChatFolderRecord] into a local folder [ChatFolderRecord]
*/
private fun remoteChatFolderRecordToLocal(record: SignalChatFolderRecord): ChatFolderRecord {
val chatFolderId = ChatFolderId.from(UuidUtil.parseOrThrow(record.proto.identifier))
val id = getIdByRemoteChatFolderId(chatFolderId)
return ChatFolderRecord(
id = id ?: -1,
name = record.proto.name,
position = record.proto.position,
showUnread = record.proto.showOnlyUnread,
showMutedChats = record.proto.showMutedChats,
showIndividualChats = record.proto.includeAllIndividualChats,
showGroupChats = record.proto.includeAllGroupChats,
folderType = when (record.proto.folderType) {
RemoteChatFolderRecord.FolderType.ALL -> ChatFolderRecord.FolderType.ALL
RemoteChatFolderRecord.FolderType.CUSTOM -> ChatFolderRecord.FolderType.CUSTOM
RemoteChatFolderRecord.FolderType.UNKNOWN -> throw AssertionError("Folder type cannot be unknown")
},
includedChats = record.proto.includedRecipients
.mapNotNull { remoteRecipient -> getRecipientIdFromRemoteRecipient(remoteRecipient) }
.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) },
excludedChats = record.proto.excludedRecipients
.mapNotNull { remoteRecipient -> getRecipientIdFromRemoteRecipient(remoteRecipient) }
.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) },
chatFolderId = chatFolderId,
storageServiceId = StorageId.forChatFolder(record.id.raw),
storageServiceProto = record.serializedUnknowns,
deletedTimestampMs = record.proto.deletedAtTimestampMs
)
}
/**
* 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

@@ -124,6 +124,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V266_UniqueThreadPi
import org.thoughtcrime.securesms.database.helpers.migration.V267_FixGroupInvitationDeclinedUpdate
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.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -253,10 +254,11 @@ object SignalDatabaseMigrations {
266 to V266_UniqueThreadPinOrder,
267 to V267_FixGroupInvitationDeclinedUpdate,
268 to V268_FixInAppPaymentsErrorStateConsistency,
269 to V269_BackupMediaSnapshotChanges
269 to V269_BackupMediaSnapshotChanges,
270 to V270_FixChatFolderColumnsForStorageSync
)
const val DATABASE_VERSION = 269
const val DATABASE_VERSION = 270
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,38 @@
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
/**
* Adds four columns chat_folder_id, storage_service_id, storage_service_proto and deleted_timestamp_ms to support chat folders in storage sync.
* Removes unnecessary is_muted column in chat folders table.
*/
@Suppress("ClassName")
object V270_FixChatFolderColumnsForStorageSync : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE chat_folder DROP COLUMN is_muted")
db.execSQL("ALTER TABLE chat_folder ADD COLUMN chat_folder_id TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE chat_folder ADD COLUMN storage_service_id TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE chat_folder ADD COLUMN storage_service_proto TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE chat_folder ADD COLUMN deleted_timestamp_ms INTEGER DEFAULT 0")
// Assign all of the folders with a [ChatFolderId] and reset position
db.rawQuery("SELECT _id FROM chat_folder ORDER BY position ASC")
.readToList { it.requireLong("_id") }
.forEachIndexed { index, id -> resetPositionAndSetChatFolderId(db, id, index) }
}
private fun resetPositionAndSetChatFolderId(db: SQLiteDatabase, id: Long, newPosition: Int) {
db.rawQuery(
"""
UPDATE chat_folder
SET chat_folder_id = '${UUID.randomUUID()}', position = $newPosition
WHERE _id = $id
"""
)
}
}