mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Add chat folder support to storage service.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user