mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 04:28:35 +00:00
Add notification profile and chat folder backupv2 proto support.
This commit is contained in:
committed by
Greyson Parrelli
parent
c91123e8e8
commit
d1bfa6ee9e
BIN
app/src/androidTest/assets/backupTests/chat_folder_00.binproto
Normal file
BIN
app/src/androidTest/assets/backupTests/chat_folder_00.binproto
Normal file
Binary file not shown.
BIN
app/src/androidTest/assets/backupTests/chat_folder_01.binproto
Normal file
BIN
app/src/androidTest/assets/backupTests/chat_folder_01.binproto
Normal file
Binary file not shown.
BIN
app/src/androidTest/assets/backupTests/chat_folder_02.binproto
Normal file
BIN
app/src/androidTest/assets/backupTests/chat_folder_02.binproto
Normal file
Binary file not shown.
BIN
app/src/androidTest/assets/backupTests/chat_folder_03.binproto
Normal file
BIN
app/src/androidTest/assets/backupTests/chat_folder_03.binproto
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -66,6 +66,11 @@ class ArchiveImportExportTests {
|
|||||||
runTests { it.startsWith("chat_") && !it.contains("_item") }
|
runTests { it.startsWith("chat_") && !it.contains("_item") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
fun chatFolders() {
|
||||||
|
runTests { it.startsWith("chat_folder_") }
|
||||||
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
fun chatItemContactMessage() {
|
fun chatItemContactMessage() {
|
||||||
runTests { it.startsWith("chat_item_contact_message_") }
|
runTests { it.startsWith("chat_item_contact_message_") }
|
||||||
@@ -191,7 +196,12 @@ class ArchiveImportExportTests {
|
|||||||
runTests { it.startsWith("chat_item_view_once_") }
|
runTests { it.startsWith("chat_item_view_once_") }
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
|
fun notificationProfiles() {
|
||||||
|
runTests { it.startsWith("notification_profile_") }
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
fun recipientCallLink() {
|
fun recipientCallLink() {
|
||||||
runTests { it.startsWith("recipient_call_link_") }
|
runTests { it.startsWith("recipient_call_link_") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1026,7 +1026,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun notificationProfile(name: String): NotificationProfile {
|
private fun notificationProfile(name: String): NotificationProfile {
|
||||||
return (SignalDatabase.notificationProfiles.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
|
return (SignalDatabase.notificationProfiles.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileTables.NotificationProfileChangeResult.Success).notificationProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMention(messageId: Long): MentionModel {
|
private fun getMention(messageId: Long): MentionModel {
|
||||||
|
|||||||
@@ -156,6 +156,14 @@ object ArchiveUploadProgress {
|
|||||||
updatePhase(ArchiveUploadProgressState.BackupPhase.Sticker)
|
updatePhase(ArchiveUploadProgressState.BackupPhase.Sticker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNotificationProfile() {
|
||||||
|
updatePhase(ArchiveUploadProgressState.BackupPhase.NotificationProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChatFolder() {
|
||||||
|
updatePhase(ArchiveUploadProgressState.BackupPhase.ChatFolder)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onMessage(currentProgress: Long, approximateCount: Long) {
|
override fun onMessage(currentProgress: Long, approximateCount: Long) {
|
||||||
updatePhase(ArchiveUploadProgressState.BackupPhase.Message, currentProgress, approximateCount)
|
updatePhase(ArchiveUploadProgressState.BackupPhase.Message, currentProgress, approximateCount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ import org.thoughtcrime.securesms.backup.v2.importer.ChatItemArchiveImporter
|
|||||||
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataArchiveProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataArchiveProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallArchiveProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallArchiveProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.ChatArchiveProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.ChatArchiveProcessor
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.processor.ChatFolderProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemArchiveProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemArchiveProcessor
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.processor.NotificationProfileProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.RecipientArchiveProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.RecipientArchiveProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.processor.StickerArchiveProcessor
|
import org.thoughtcrime.securesms.backup.v2.processor.StickerArchiveProcessor
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||||
@@ -576,6 +578,28 @@ object BackupRepository {
|
|||||||
return@export
|
return@export
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progressEmitter?.onNotificationProfile()
|
||||||
|
NotificationProfileProcessor.export(dbSnapshot, exportState) { frame ->
|
||||||
|
writer.write(frame)
|
||||||
|
eventTimer.emit("notification-profile")
|
||||||
|
frameCount++
|
||||||
|
}
|
||||||
|
if (cancellationSignal()) {
|
||||||
|
Log.w(TAG, "[export] Cancelled! Stopping")
|
||||||
|
return@export
|
||||||
|
}
|
||||||
|
|
||||||
|
progressEmitter?.onChatFolder()
|
||||||
|
ChatFolderProcessor.export(dbSnapshot, exportState) { frame ->
|
||||||
|
writer.write(frame)
|
||||||
|
eventTimer.emit("chat-folder")
|
||||||
|
frameCount++
|
||||||
|
}
|
||||||
|
if (cancellationSignal()) {
|
||||||
|
Log.w(TAG, "[export] Cancelled! Stopping")
|
||||||
|
return@export
|
||||||
|
}
|
||||||
|
|
||||||
val approximateMessageCount = dbSnapshot.messageTable.getApproximateExportableMessageCount(exportState.threadIds)
|
val approximateMessageCount = dbSnapshot.messageTable.getApproximateExportableMessageCount(exportState.threadIds)
|
||||||
val frameCountStart = frameCount
|
val frameCountStart = frameCount
|
||||||
progressEmitter?.onMessage(0, approximateMessageCount)
|
progressEmitter?.onMessage(0, approximateMessageCount)
|
||||||
@@ -727,9 +751,6 @@ object BackupRepository {
|
|||||||
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
|
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
|
||||||
SignalDatabase.recipients.setProfileSharing(selfId, true)
|
SignalDatabase.recipients.setProfileSharing(selfId, true)
|
||||||
|
|
||||||
// Add back default All Chats chat folder after clearing data
|
|
||||||
SignalDatabase.chatFolders.insertAllChatFolder()
|
|
||||||
|
|
||||||
val importState = ImportState(messageBackupKey, mediaRootBackupKey)
|
val importState = ImportState(messageBackupKey, mediaRootBackupKey)
|
||||||
val chatItemInserter: ChatItemArchiveImporter = ChatItemArchiveProcessor.beginImport(importState)
|
val chatItemInserter: ChatItemArchiveImporter = ChatItemArchiveProcessor.beginImport(importState)
|
||||||
|
|
||||||
@@ -768,6 +789,18 @@ object BackupRepository {
|
|||||||
frameCount++
|
frameCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frame.notificationProfile != null -> {
|
||||||
|
NotificationProfileProcessor.import(frame.notificationProfile, importState)
|
||||||
|
eventTimer.emit("notification-profile")
|
||||||
|
frameCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.chatFolder != null -> {
|
||||||
|
ChatFolderProcessor.import(frame.chatFolder, importState)
|
||||||
|
eventTimer.emit("chat-folder")
|
||||||
|
frameCount++
|
||||||
|
}
|
||||||
|
|
||||||
frame.chatItem != null -> {
|
frame.chatItem != null -> {
|
||||||
chatItemInserter.import(frame.chatItem)
|
chatItemInserter.import(frame.chatItem)
|
||||||
eventTimer.emit("chatItem")
|
eventTimer.emit("chatItem")
|
||||||
@@ -791,6 +824,11 @@ object BackupRepository {
|
|||||||
eventTimer.emit("chatItem")
|
eventTimer.emit("chatItem")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!importState.importedChatFolders) {
|
||||||
|
// Add back default All Chats chat folder after clearing data if missing
|
||||||
|
SignalDatabase.chatFolders.insertAllChatFolder()
|
||||||
|
}
|
||||||
|
|
||||||
stopwatch.split("frames")
|
stopwatch.split("frames")
|
||||||
|
|
||||||
Log.d(TAG, "[import] Rebuilding FTS index...")
|
Log.d(TAG, "[import] Rebuilding FTS index...")
|
||||||
@@ -1449,6 +1487,8 @@ object BackupRepository {
|
|||||||
fun onThread()
|
fun onThread()
|
||||||
fun onCall()
|
fun onCall()
|
||||||
fun onSticker()
|
fun onSticker()
|
||||||
|
fun onNotificationProfile()
|
||||||
|
fun onChatFolder()
|
||||||
fun onMessage(currentProgress: Long, approximateCount: Long)
|
fun onMessage(currentProgress: Long, approximateCount: Long)
|
||||||
fun onAttachment(currentProgress: Long, totalCount: Long)
|
fun onAttachment(currentProgress: Long, totalCount: Long)
|
||||||
}
|
}
|
||||||
@@ -1477,10 +1517,20 @@ class ImportState(val messageBackupKey: MessageBackupKey, val mediaRootBackupKey
|
|||||||
val chatIdToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()
|
val chatIdToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()
|
||||||
val chatIdToBackupRecipientId: MutableMap<Long, Long> = hashMapOf()
|
val chatIdToBackupRecipientId: MutableMap<Long, Long> = hashMapOf()
|
||||||
val remoteToLocalColorId: MutableMap<Long, Long> = hashMapOf()
|
val remoteToLocalColorId: MutableMap<Long, Long> = hashMapOf()
|
||||||
|
val recipientIdToLocalThreadId: MutableMap<RecipientId, Long> = hashMapOf()
|
||||||
|
val recipientIdToIsGroup: MutableMap<RecipientId, Boolean> = hashMapOf()
|
||||||
|
|
||||||
|
private var chatFolderPosition: Int = 0
|
||||||
|
val importedChatFolders: Boolean
|
||||||
|
get() = chatFolderPosition > 0
|
||||||
|
|
||||||
fun requireLocalRecipientId(remoteId: Long): RecipientId {
|
fun requireLocalRecipientId(remoteId: Long): RecipientId {
|
||||||
return remoteToLocalRecipientId[remoteId] ?: throw IllegalArgumentException("There is no local recipientId for remote recipientId $remoteId!")
|
return remoteToLocalRecipientId[remoteId] ?: throw IllegalArgumentException("There is no local recipientId for remote recipientId $remoteId!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getNextChatFolderPosition(): Int {
|
||||||
|
return chatFolderPosition++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackupMetadata(
|
class BackupMetadata(
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class LocalBackupV2Event(val type: Type, val count: Long = 0, val estimatedTotal
|
|||||||
PROGRESS_THREAD,
|
PROGRESS_THREAD,
|
||||||
PROGRESS_CALL,
|
PROGRESS_CALL,
|
||||||
PROGRESS_STICKER,
|
PROGRESS_STICKER,
|
||||||
|
NOTIFICATION_PROFILE,
|
||||||
|
CHAT_FOLDER,
|
||||||
PROGRESS_MESSAGE,
|
PROGRESS_MESSAGE,
|
||||||
PROGRESS_ATTACHMENT,
|
PROGRESS_ATTACHMENT,
|
||||||
PROGRESS_VERIFYING,
|
PROGRESS_VERIFYING,
|
||||||
|
|||||||
@@ -169,6 +169,14 @@ object LocalArchiver {
|
|||||||
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_STICKER))
|
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_STICKER))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNotificationProfile() {
|
||||||
|
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.NOTIFICATION_PROFILE))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChatFolder() {
|
||||||
|
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.CHAT_FOLDER))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onMessage(currentProgress: Long, approximateCount: Long) {
|
override fun onMessage(currentProgress: Long, approximateCount: Long) {
|
||||||
if (currentProgress == 0L) {
|
if (currentProgress == 0L) {
|
||||||
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_MESSAGE))
|
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_MESSAGE))
|
||||||
|
|||||||
@@ -46,5 +46,6 @@ object ChatArchiveProcessor {
|
|||||||
importState.chatIdToLocalRecipientId[chat.id] = recipientId
|
importState.chatIdToLocalRecipientId[chat.id] = recipientId
|
||||||
importState.chatIdToLocalThreadId[chat.id] = threadId
|
importState.chatIdToLocalThreadId[chat.id] = threadId
|
||||||
importState.chatIdToBackupRecipientId[chat.id] = chat.recipientId
|
importState.chatIdToBackupRecipientId[chat.id] = chat.recipientId
|
||||||
|
importState.recipientIdToLocalThreadId[recipientId] = threadId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2.processor
|
||||||
|
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
import org.signal.core.util.SqlUtil
|
||||||
|
import org.signal.core.util.insertInto
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.ChatFolder
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
|
||||||
|
import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderMembershipTable
|
||||||
|
import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderTable
|
||||||
|
import org.thoughtcrime.securesms.database.ChatFolderTables.MembershipType
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.ChatFolder as ChatFolderProto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles exporting and importing [ChatFolderRecord]s.
|
||||||
|
*/
|
||||||
|
object ChatFolderProcessor {
|
||||||
|
|
||||||
|
private val TAG = Log.tag(ChatFolderProcessor::class)
|
||||||
|
|
||||||
|
fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) {
|
||||||
|
val folders = db
|
||||||
|
.chatFoldersTable
|
||||||
|
.getChatFolders()
|
||||||
|
.sortedBy { it.position }
|
||||||
|
|
||||||
|
if (folders.isEmpty()) {
|
||||||
|
Log.d(TAG, "No chat folders, nothing to export")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folders.size == 1 && folders[0].folderType == ChatFolderRecord.FolderType.ALL) {
|
||||||
|
Log.d(TAG, "Only ALL chat folder present, skipping chat folder export")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folders.none { it.folderType == ChatFolderRecord.FolderType.ALL }) {
|
||||||
|
Log.w(TAG, "Missing ALL chat folder, exporting as first position")
|
||||||
|
emitter.emit(ChatFolderRecord.getAllChatsFolderForBackup().toBackupFrame(emptyList(), emptyList()))
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.forEach { folder ->
|
||||||
|
val includedRecipientIds = folder
|
||||||
|
.includedChats
|
||||||
|
.map { db.threadTable.getRecipientIdForThreadId(it)!!.toLong() }
|
||||||
|
.filter { exportState.recipientIds.contains(it) }
|
||||||
|
|
||||||
|
val excludedRecipientIds = folder
|
||||||
|
.excludedChats
|
||||||
|
.map { db.threadTable.getRecipientIdForThreadId(it)!!.toLong() }
|
||||||
|
.filter { exportState.recipientIds.contains(it) }
|
||||||
|
|
||||||
|
val frame = folder.toBackupFrame(includedRecipientIds, excludedRecipientIds)
|
||||||
|
emitter.emit(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun import(chatFolder: ChatFolderProto, importState: ImportState) {
|
||||||
|
val chatFolderId = SignalDatabase
|
||||||
|
.writableDatabase
|
||||||
|
.insertInto(ChatFolderTable.TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
ChatFolderTable.NAME to chatFolder.name,
|
||||||
|
ChatFolderTable.POSITION to importState.getNextChatFolderPosition(),
|
||||||
|
ChatFolderTable.SHOW_UNREAD to chatFolder.showOnlyUnread,
|
||||||
|
ChatFolderTable.SHOW_MUTED to chatFolder.showMutedChats,
|
||||||
|
ChatFolderTable.SHOW_INDIVIDUAL to chatFolder.includeAllIndividualChats,
|
||||||
|
ChatFolderTable.SHOW_GROUPS to chatFolder.includeAllGroupChats,
|
||||||
|
ChatFolderTable.FOLDER_TYPE to chatFolder.folderType.toLocal().value
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
if (chatFolderId < 0) {
|
||||||
|
Log.w(TAG, "Chat folder already exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val includedChatsQueries = chatFolder.includedRecipientIds.toMembershipInsertQueries(chatFolderId, importState, MembershipType.INCLUDED)
|
||||||
|
includedChatsQueries.forEach {
|
||||||
|
SignalDatabase.writableDatabase.execSQL(it.where, it.whereArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
val excludedChatsQueries = chatFolder.excludedRecipientIds.toMembershipInsertQueries(chatFolderId, importState, MembershipType.EXCLUDED)
|
||||||
|
excludedChatsQueries.forEach {
|
||||||
|
SignalDatabase.writableDatabase.execSQL(it.where, it.whereArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ChatFolderRecord.toBackupFrame(includedRecipientIds: List<Long>, excludedRecipientIds: List<Long>): Frame {
|
||||||
|
val chatFolder = ChatFolderProto(
|
||||||
|
name = this.name,
|
||||||
|
showOnlyUnread = this.showUnread,
|
||||||
|
showMutedChats = this.showMutedChats,
|
||||||
|
includeAllIndividualChats = this.showIndividualChats,
|
||||||
|
includeAllGroupChats = this.showGroupChats,
|
||||||
|
folderType = when (this.folderType) {
|
||||||
|
ChatFolderRecord.FolderType.ALL -> ChatFolderProto.FolderType.ALL
|
||||||
|
ChatFolderRecord.FolderType.CUSTOM -> ChatFolderProto.FolderType.CUSTOM
|
||||||
|
else -> throw IllegalStateException("Only ALL or CUSTOM should be in the db")
|
||||||
|
},
|
||||||
|
includedRecipientIds = includedRecipientIds,
|
||||||
|
excludedRecipientIds = excludedRecipientIds
|
||||||
|
)
|
||||||
|
|
||||||
|
return Frame(chatFolder = chatFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ChatFolderProto.FolderType.toLocal(): ChatFolderRecord.FolderType {
|
||||||
|
return when (this) {
|
||||||
|
ChatFolder.FolderType.UNKNOWN -> throw IllegalStateException()
|
||||||
|
ChatFolder.FolderType.ALL -> ChatFolderRecord.FolderType.ALL
|
||||||
|
ChatFolder.FolderType.CUSTOM -> ChatFolderRecord.FolderType.CUSTOM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Long>.toMembershipInsertQueries(chatFolderId: Long, importState: ImportState, membershipType: MembershipType): List<SqlUtil.Query> {
|
||||||
|
val values = this
|
||||||
|
.mapNotNull { importState.remoteToLocalRecipientId[it] }
|
||||||
|
.map { recipientId -> importState.recipientIdToLocalThreadId[recipientId] ?: SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, importState.recipientIdToIsGroup[recipientId] == true) }
|
||||||
|
.map { threadId ->
|
||||||
|
contentValuesOf(
|
||||||
|
ChatFolderMembershipTable.CHAT_FOLDER_ID to chatFolderId,
|
||||||
|
ChatFolderMembershipTable.THREAD_ID to threadId,
|
||||||
|
ChatFolderMembershipTable.MEMBERSHIP_TYPE to membershipType.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SqlUtil.buildBulkInsert(
|
||||||
|
ChatFolderMembershipTable.TABLE_NAME,
|
||||||
|
arrayOf(ChatFolderMembershipTable.CHAT_FOLDER_ID, ChatFolderMembershipTable.THREAD_ID, ChatFolderMembershipTable.MEMBERSHIP_TYPE),
|
||||||
|
values
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.backup.v2.processor
|
||||||
|
|
||||||
|
import org.signal.core.util.insertInto
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.toInt
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileTables.NotificationProfileAllowedMembersTable
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileTables.NotificationProfileScheduleTable
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileTables.NotificationProfileTable
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.serialize
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.NotificationProfile as NotificationProfileProto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles exporting and importing [NotificationProfile] models.
|
||||||
|
*/
|
||||||
|
object NotificationProfileProcessor {
|
||||||
|
|
||||||
|
private val TAG = Log.tag(NotificationProfileProcessor::class)
|
||||||
|
|
||||||
|
fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) {
|
||||||
|
db.notificationProfileTables
|
||||||
|
.getProfiles()
|
||||||
|
.forEach { profile ->
|
||||||
|
val frame = profile.toBackupFrame(includeRecipient = { id -> exportState.recipientIds.contains(id.toLong()) })
|
||||||
|
emitter.emit(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun import(profile: NotificationProfileProto, importState: ImportState) {
|
||||||
|
val profileId = SignalDatabase
|
||||||
|
.writableDatabase
|
||||||
|
.insertInto(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
NotificationProfileTable.NAME to profile.name,
|
||||||
|
NotificationProfileTable.EMOJI to (profile.emoji ?: ""),
|
||||||
|
NotificationProfileTable.COLOR to (AvatarColor.fromColor(profile.color) ?: AvatarColor.random()).serialize(),
|
||||||
|
NotificationProfileTable.CREATED_AT to profile.createdAtMs,
|
||||||
|
NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls.toInt(),
|
||||||
|
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt()
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
if (profileId < 0) {
|
||||||
|
Log.w(TAG, "Notification profile name already exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalDatabase
|
||||||
|
.writableDatabase
|
||||||
|
.insertInto(NotificationProfileScheduleTable.TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID to profileId,
|
||||||
|
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 { importState.remoteToLocalRecipientId[it] }
|
||||||
|
.forEach { recipientId ->
|
||||||
|
SignalDatabase
|
||||||
|
.writableDatabase
|
||||||
|
.insertInto(NotificationProfileAllowedMembersTable.TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID to profileId,
|
||||||
|
NotificationProfileAllowedMembersTable.RECIPIENT_ID to recipientId.serialize()
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotificationProfile.toBackupFrame(includeRecipient: (RecipientId) -> Boolean): Frame {
|
||||||
|
val profile = NotificationProfileProto(
|
||||||
|
name = this.name,
|
||||||
|
emoji = this.emoji.takeIf { it.isNotBlank() },
|
||||||
|
color = this.color.colorInt(),
|
||||||
|
createdAtMs = this.createdAt,
|
||||||
|
allowAllCalls = this.allowAllCalls,
|
||||||
|
allowAllMentions = this.allowAllMentions,
|
||||||
|
allowedMembers = this.allowedMembers.filter { includeRecipient(it) }.map { it.toLong() },
|
||||||
|
scheduleEnabled = this.schedule.enabled,
|
||||||
|
scheduleStartTime = this.schedule.start,
|
||||||
|
scheduleEndTime = this.schedule.end,
|
||||||
|
scheduleDaysEnabled = this.schedule.daysEnabled.map { it.toBackupProto() }
|
||||||
|
)
|
||||||
|
|
||||||
|
return Frame(notificationProfile = profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DayOfWeek.toBackupProto(): NotificationProfileProto.DayOfWeek {
|
||||||
|
return when (this) {
|
||||||
|
DayOfWeek.MONDAY -> NotificationProfileProto.DayOfWeek.MONDAY
|
||||||
|
DayOfWeek.TUESDAY -> NotificationProfileProto.DayOfWeek.TUESDAY
|
||||||
|
DayOfWeek.WEDNESDAY -> NotificationProfileProto.DayOfWeek.WEDNESDAY
|
||||||
|
DayOfWeek.THURSDAY -> NotificationProfileProto.DayOfWeek.THURSDAY
|
||||||
|
DayOfWeek.FRIDAY -> NotificationProfileProto.DayOfWeek.FRIDAY
|
||||||
|
DayOfWeek.SATURDAY -> NotificationProfileProto.DayOfWeek.SATURDAY
|
||||||
|
DayOfWeek.SUNDAY -> NotificationProfileProto.DayOfWeek.SUNDAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotificationProfileProto.DayOfWeek.toLocal(): DayOfWeek {
|
||||||
|
return when (this) {
|
||||||
|
NotificationProfileProto.DayOfWeek.UNKNOWN -> throw IllegalStateException()
|
||||||
|
NotificationProfileProto.DayOfWeek.MONDAY -> DayOfWeek.MONDAY
|
||||||
|
NotificationProfileProto.DayOfWeek.TUESDAY -> DayOfWeek.TUESDAY
|
||||||
|
NotificationProfileProto.DayOfWeek.WEDNESDAY -> DayOfWeek.WEDNESDAY
|
||||||
|
NotificationProfileProto.DayOfWeek.THURSDAY -> DayOfWeek.THURSDAY
|
||||||
|
NotificationProfileProto.DayOfWeek.FRIDAY -> DayOfWeek.FRIDAY
|
||||||
|
NotificationProfileProto.DayOfWeek.SATURDAY -> DayOfWeek.SATURDAY
|
||||||
|
NotificationProfileProto.DayOfWeek.SUNDAY -> DayOfWeek.SUNDAY
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,4 +49,15 @@ data class ChatFolderRecord(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getAllChatsFolderForBackup(): ChatFolderRecord {
|
||||||
|
return ChatFolderRecord(
|
||||||
|
folderType = ChatFolderRecord.FolderType.ALL,
|
||||||
|
showIndividualChats = true,
|
||||||
|
showGroupChats = true,
|
||||||
|
showMutedChats = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import org.thoughtcrime.securesms.database.NotificationProfileDatabase
|
import org.thoughtcrime.securesms.database.NotificationProfileTables
|
||||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
|
||||||
class EditNotificationProfileViewModel(private val profileId: Long, private val repository: NotificationProfilesRepository) : ViewModel() {
|
class EditNotificationProfileViewModel(private val profileId: Long, private val repository: NotificationProfilesRepository) : ViewModel() {
|
||||||
@@ -34,8 +34,8 @@ class EditNotificationProfileViewModel(private val profileId: Long, private val
|
|||||||
|
|
||||||
return save.map { r ->
|
return save.map { r ->
|
||||||
when (r) {
|
when (r) {
|
||||||
is NotificationProfileDatabase.NotificationProfileChangeResult.Success -> SaveNotificationProfileResult.Success(r.notificationProfile, createMode)
|
is NotificationProfileTables.NotificationProfileChangeResult.Success -> SaveNotificationProfileResult.Success(r.notificationProfile, createMode)
|
||||||
NotificationProfileDatabase.NotificationProfileChangeResult.DuplicateName -> SaveNotificationProfileResult.DuplicateNameFailure
|
NotificationProfileTables.NotificationProfileChangeResult.DuplicateName -> SaveNotificationProfileResult.DuplicateNameFailure
|
||||||
}
|
}
|
||||||
}.observeOn(AndroidSchedulers.mainThread())
|
}.observeOn(AndroidSchedulers.mainThread())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import io.reactivex.rxjava3.core.Single
|
|||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||||
import org.thoughtcrime.securesms.database.NotificationProfileDatabase
|
import org.thoughtcrime.securesms.database.NotificationProfileTables
|
||||||
import org.thoughtcrime.securesms.database.RxDatabaseObserver
|
import org.thoughtcrime.securesms.database.RxDatabaseObserver
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
@@ -24,7 +24,7 @@ import org.thoughtcrime.securesms.util.toMillis
|
|||||||
* One stop shop for all your Notification Profile data needs.
|
* One stop shop for all your Notification Profile data needs.
|
||||||
*/
|
*/
|
||||||
class NotificationProfilesRepository {
|
class NotificationProfilesRepository {
|
||||||
private val database: NotificationProfileDatabase = SignalDatabase.notificationProfiles
|
private val database: NotificationProfileTables = SignalDatabase.notificationProfiles
|
||||||
|
|
||||||
fun getProfiles(): Flowable<List<NotificationProfile>> {
|
fun getProfiles(): Flowable<List<NotificationProfile>> {
|
||||||
return RxDatabaseObserver
|
return RxDatabaseObserver
|
||||||
@@ -54,17 +54,17 @@ class NotificationProfilesRepository {
|
|||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createProfile(name: String, selectedEmoji: String): Single<NotificationProfileDatabase.NotificationProfileChangeResult> {
|
fun createProfile(name: String, selectedEmoji: String): Single<NotificationProfileTables.NotificationProfileChangeResult> {
|
||||||
return Single.fromCallable { database.createProfile(name = name, emoji = selectedEmoji, color = AvatarColor.random(), createdAt = System.currentTimeMillis()) }
|
return Single.fromCallable { database.createProfile(name = name, emoji = selectedEmoji, color = AvatarColor.random(), createdAt = System.currentTimeMillis()) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateProfile(profileId: Long, name: String, selectedEmoji: String): Single<NotificationProfileDatabase.NotificationProfileChangeResult> {
|
fun updateProfile(profileId: Long, name: String, selectedEmoji: String): Single<NotificationProfileTables.NotificationProfileChangeResult> {
|
||||||
return Single.fromCallable { database.updateProfile(profileId = profileId, name = name, emoji = selectedEmoji) }
|
return Single.fromCallable { database.updateProfile(profileId = profileId, name = name, emoji = selectedEmoji) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateProfile(profile: NotificationProfile): Single<NotificationProfileDatabase.NotificationProfileChangeResult> {
|
fun updateProfile(profile: NotificationProfile): Single<NotificationProfileTables.NotificationProfileChangeResult> {
|
||||||
return Single.fromCallable { database.updateProfile(profile) }
|
return Single.fromCallable { database.updateProfile(profile) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ class NotificationProfilesRepository {
|
|||||||
.take(1)
|
.take(1)
|
||||||
.singleOrError()
|
.singleOrError()
|
||||||
.flatMap { updateProfile(it.copy(allowAllMentions = !it.allowAllMentions)) }
|
.flatMap { updateProfile(it.copy(allowAllMentions = !it.allowAllMentions)) }
|
||||||
.map { (it as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile }
|
.map { (it as NotificationProfileTables.NotificationProfileChangeResult.Success).notificationProfile }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleAllowAllCalls(profileId: Long): Single<NotificationProfile> {
|
fun toggleAllowAllCalls(profileId: Long): Single<NotificationProfile> {
|
||||||
@@ -107,7 +107,7 @@ class NotificationProfilesRepository {
|
|||||||
.take(1)
|
.take(1)
|
||||||
.singleOrError()
|
.singleOrError()
|
||||||
.flatMap { updateProfile(it.copy(allowAllCalls = !it.allowAllCalls)) }
|
.flatMap { updateProfile(it.copy(allowAllCalls = !it.allowAllCalls)) }
|
||||||
.map { (it as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile }
|
.map { (it as NotificationProfileTables.NotificationProfileChangeResult.Success).notificationProfile }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun manuallyToggleProfile(profile: NotificationProfile, now: Long = System.currentTimeMillis()): Completable {
|
fun manuallyToggleProfile(profile: NotificationProfile, now: Long = System.currentTimeMillis()): Completable {
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import androidx.annotation.ColorInt;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A serializable set of color constants that can be used for avatars.
|
* A serializable set of color constants that can be used for avatars.
|
||||||
@@ -27,8 +29,11 @@ public enum AvatarColor {
|
|||||||
UNKNOWN("UNKNOWN", 0x00000000),
|
UNKNOWN("UNKNOWN", 0x00000000),
|
||||||
ON_SURFACE_VARIANT("ON_SURFACE_VARIANT", 0x00000000);
|
ON_SURFACE_VARIANT("ON_SURFACE_VARIANT", 0x00000000);
|
||||||
|
|
||||||
/** Fast map of name to enum, while also giving us a location to map old colors to new ones. */
|
/**
|
||||||
|
* Fast map of name to enum, while also giving us a location to map old colors to new ones.
|
||||||
|
*/
|
||||||
private static final Map<String, AvatarColor> NAME_MAP = new HashMap<>();
|
private static final Map<String, AvatarColor> NAME_MAP = new HashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
for (AvatarColor color : AvatarColor.values()) {
|
for (AvatarColor color : AvatarColor.values()) {
|
||||||
NAME_MAP.put(color.serialize(), color);
|
NAME_MAP.put(color.serialize(), color);
|
||||||
@@ -97,7 +102,9 @@ public enum AvatarColor {
|
|||||||
NAME_MAP.put("grey", A210);
|
NAME_MAP.put("grey", A210);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Colors that can be assigned via {@link #random()}. */
|
/**
|
||||||
|
* Colors that can be assigned via {@link #random()}.
|
||||||
|
*/
|
||||||
static final AvatarColor[] RANDOM_OPTIONS = new AvatarColor[] {
|
static final AvatarColor[] RANDOM_OPTIONS = new AvatarColor[] {
|
||||||
A100,
|
A100,
|
||||||
A110,
|
A110,
|
||||||
@@ -137,4 +144,11 @@ public enum AvatarColor {
|
|||||||
public static @NonNull AvatarColor deserialize(@Nullable String name) {
|
public static @NonNull AvatarColor deserialize(@Nullable String name) {
|
||||||
return Objects.requireNonNull(NAME_MAP.getOrDefault(name, A210));
|
return Objects.requireNonNull(NAME_MAP.getOrDefault(name, A210));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @Nullable AvatarColor fromColor(@ColorInt int color) {
|
||||||
|
return Arrays.stream(values())
|
||||||
|
.filter(c -> c.color == color)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import java.time.DayOfWeek
|
|||||||
/**
|
/**
|
||||||
* Database for maintaining Notification Profiles, Notification Profile Schedules, and Notification Profile allowed memebers.
|
* Database for maintaining Notification Profiles, Notification Profile Schedules, and Notification Profile allowed memebers.
|
||||||
*/
|
*/
|
||||||
class NotificationProfileDatabase(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper), RecipientIdDatabaseReference {
|
class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper), RecipientIdDatabaseReference {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmField
|
@JvmField
|
||||||
@@ -32,7 +32,7 @@ class NotificationProfileDatabase(context: Context, databaseHelper: SignalDataba
|
|||||||
val CREATE_INDEXES: Array<String> = arrayOf(NotificationProfileScheduleTable.CREATE_INDEX, NotificationProfileAllowedMembersTable.CREATE_INDEX)
|
val CREATE_INDEXES: Array<String> = arrayOf(NotificationProfileScheduleTable.CREATE_INDEX, NotificationProfileAllowedMembersTable.CREATE_INDEX)
|
||||||
}
|
}
|
||||||
|
|
||||||
private object NotificationProfileTable {
|
object NotificationProfileTable {
|
||||||
const val TABLE_NAME = "notification_profile"
|
const val TABLE_NAME = "notification_profile"
|
||||||
|
|
||||||
const val ID = "_id"
|
const val ID = "_id"
|
||||||
@@ -56,7 +56,7 @@ class NotificationProfileDatabase(context: Context, databaseHelper: SignalDataba
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
private object NotificationProfileScheduleTable {
|
object NotificationProfileScheduleTable {
|
||||||
const val TABLE_NAME = "notification_profile_schedule"
|
const val TABLE_NAME = "notification_profile_schedule"
|
||||||
|
|
||||||
const val ID = "_id"
|
const val ID = "_id"
|
||||||
@@ -82,7 +82,7 @@ class NotificationProfileDatabase(context: Context, databaseHelper: SignalDataba
|
|||||||
const val CREATE_INDEX = "CREATE INDEX notification_profile_schedule_profile_index ON $TABLE_NAME ($NOTIFICATION_PROFILE_ID)"
|
const val CREATE_INDEX = "CREATE INDEX notification_profile_schedule_profile_index ON $TABLE_NAME ($NOTIFICATION_PROFILE_ID)"
|
||||||
}
|
}
|
||||||
|
|
||||||
private object NotificationProfileAllowedMembersTable {
|
object NotificationProfileAllowedMembersTable {
|
||||||
const val TABLE_NAME = "notification_profile_allowed_members"
|
const val TABLE_NAME = "notification_profile_allowed_members"
|
||||||
|
|
||||||
const val ID = "_id"
|
const val ID = "_id"
|
||||||
@@ -367,7 +367,7 @@ class NotificationProfileDatabase(context: Context, databaseHelper: SignalDataba
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Iterable<DayOfWeek>.serialize(): String {
|
fun Iterable<DayOfWeek>.serialize(): String {
|
||||||
return joinToString(separator = ",", transform = { it.serialize() })
|
return joinToString(separator = ",", transform = { it.serialize() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
|||||||
val messageSendLogTables: MessageSendLogTables = MessageSendLogTables(context, this)
|
val messageSendLogTables: MessageSendLogTables = MessageSendLogTables(context, this)
|
||||||
val avatarPickerDatabase: AvatarPickerDatabase = AvatarPickerDatabase(context, this)
|
val avatarPickerDatabase: AvatarPickerDatabase = AvatarPickerDatabase(context, this)
|
||||||
val reactionTable: ReactionTable = ReactionTable(context, this)
|
val reactionTable: ReactionTable = ReactionTable(context, this)
|
||||||
val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
|
val notificationProfileTables: NotificationProfileTables = NotificationProfileTables(context, this)
|
||||||
val donationReceiptTable: DonationReceiptTable = DonationReceiptTable(context, this)
|
val donationReceiptTable: DonationReceiptTable = DonationReceiptTable(context, this)
|
||||||
val distributionListTables: DistributionListTables = DistributionListTables(context, this)
|
val distributionListTables: DistributionListTables = DistributionListTables(context, this)
|
||||||
val storySendTable: StorySendTable = StorySendTable(context, this)
|
val storySendTable: StorySendTable = StorySendTable(context, this)
|
||||||
@@ -120,7 +120,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
|||||||
executeStatements(db, SearchTable.CREATE_TABLE)
|
executeStatements(db, SearchTable.CREATE_TABLE)
|
||||||
executeStatements(db, RemappedRecordTables.CREATE_TABLE)
|
executeStatements(db, RemappedRecordTables.CREATE_TABLE)
|
||||||
executeStatements(db, MessageSendLogTables.CREATE_TABLE)
|
executeStatements(db, MessageSendLogTables.CREATE_TABLE)
|
||||||
executeStatements(db, NotificationProfileDatabase.CREATE_TABLE)
|
executeStatements(db, NotificationProfileTables.CREATE_TABLE)
|
||||||
executeStatements(db, DistributionListTables.CREATE_TABLE)
|
executeStatements(db, DistributionListTables.CREATE_TABLE)
|
||||||
executeStatements(db, ChatFolderTables.CREATE_TABLE)
|
executeStatements(db, ChatFolderTables.CREATE_TABLE)
|
||||||
db.execSQL(BackupMediaSnapshotTable.CREATE_TABLE)
|
db.execSQL(BackupMediaSnapshotTable.CREATE_TABLE)
|
||||||
@@ -137,7 +137,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
|||||||
executeStatements(db, MentionTable.CREATE_INDEXES)
|
executeStatements(db, MentionTable.CREATE_INDEXES)
|
||||||
executeStatements(db, PaymentTable.CREATE_INDEXES)
|
executeStatements(db, PaymentTable.CREATE_INDEXES)
|
||||||
executeStatements(db, MessageSendLogTables.CREATE_INDEXES)
|
executeStatements(db, MessageSendLogTables.CREATE_INDEXES)
|
||||||
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
|
executeStatements(db, NotificationProfileTables.CREATE_INDEXES)
|
||||||
executeStatements(db, DonationReceiptTable.CREATE_INDEXS)
|
executeStatements(db, DonationReceiptTable.CREATE_INDEXS)
|
||||||
executeStatements(db, StorySendTable.CREATE_INDEXS)
|
executeStatements(db, StorySendTable.CREATE_INDEXS)
|
||||||
executeStatements(db, DistributionListTables.CREATE_INDEXES)
|
executeStatements(db, DistributionListTables.CREATE_INDEXES)
|
||||||
@@ -456,8 +456,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
|||||||
|
|
||||||
@get:JvmStatic
|
@get:JvmStatic
|
||||||
@get:JvmName("notificationProfiles")
|
@get:JvmName("notificationProfiles")
|
||||||
val notificationProfiles: NotificationProfileDatabase
|
val notificationProfiles: NotificationProfileTables
|
||||||
get() = instance!!.notificationProfileDatabase
|
get() = instance!!.notificationProfileTables
|
||||||
|
|
||||||
@get:JvmStatic
|
@get:JvmStatic
|
||||||
@get:JvmName("payments")
|
@get:JvmName("payments")
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ message BackupInfo {
|
|||||||
// e.g. a Recipient must come before any Chat referencing it.
|
// e.g. a Recipient must come before any Chat referencing it.
|
||||||
// 3. All ChatItems must appear in global Chat rendering order.
|
// 3. All ChatItems must appear in global Chat rendering order.
|
||||||
// (The order in which they were received by the client.)
|
// (The order in which they were received by the client.)
|
||||||
|
// 4. ChatFolders must appear in render order (e.g., left to right for
|
||||||
|
// LTR locales), but can appear anywhere relative to other frames respecting
|
||||||
|
// rule 2 (after Recipients and Chats).
|
||||||
|
//
|
||||||
|
// Recipients, Chats, StickerPacks, AdHocCalls, and NotificationProfiles
|
||||||
|
// can be in any order. (But must respect rule 2.)
|
||||||
//
|
//
|
||||||
// Recipients, Chats, StickerPacks, and AdHocCalls can be in any order.
|
|
||||||
// (But must respect rule 2.)
|
|
||||||
// For example, Chats may all be together at the beginning,
|
// For example, Chats may all be together at the beginning,
|
||||||
// or may each immediately precede its first ChatItem.
|
// or may each immediately precede its first ChatItem.
|
||||||
message Frame {
|
message Frame {
|
||||||
@@ -31,6 +35,8 @@ message Frame {
|
|||||||
ChatItem chatItem = 4;
|
ChatItem chatItem = 4;
|
||||||
StickerPack stickerPack = 5;
|
StickerPack stickerPack = 5;
|
||||||
AdHocCall adHocCall = 6;
|
AdHocCall adHocCall = 6;
|
||||||
|
NotificationProfile notificationProfile = 7;
|
||||||
|
ChatFolder chatFolder = 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1182,4 +1188,49 @@ message ChatStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool dimWallpaperInDarkMode = 7;
|
bool dimWallpaperInDarkMode = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message NotificationProfile {
|
||||||
|
enum DayOfWeek {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
MONDAY = 1;
|
||||||
|
TUESDAY = 2;
|
||||||
|
WEDNESDAY = 3;
|
||||||
|
THURSDAY = 4;
|
||||||
|
FRIDAY = 5;
|
||||||
|
SATURDAY = 6;
|
||||||
|
SUNDAY = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
string name = 1;
|
||||||
|
optional string emoji = 2;
|
||||||
|
fixed32 color = 3; // 0xAARRGGBB
|
||||||
|
uint64 createdAtMs = 4;
|
||||||
|
bool allowAllCalls = 5;
|
||||||
|
bool allowAllMentions = 6;
|
||||||
|
repeated uint64 allowedMembers = 7; // generated recipient id for allowed groups and contacts
|
||||||
|
bool scheduleEnabled = 8;
|
||||||
|
uint32 scheduleStartTime = 9; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345)
|
||||||
|
uint32 scheduleEndTime = 10; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345)
|
||||||
|
repeated DayOfWeek scheduleDaysEnabled = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatFolder {
|
||||||
|
// Represents the default "All chats" folder record vs all other custom folders
|
||||||
|
enum FolderType {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
ALL = 1;
|
||||||
|
CUSTOM = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
string name = 1;
|
||||||
|
bool showOnlyUnread = 2;
|
||||||
|
bool showMutedChats = 3;
|
||||||
|
// Folder includes all 1:1 chats, unless excluded
|
||||||
|
bool includeAllIndividualChats = 4;
|
||||||
|
// Folder includes all group chats, unless excluded
|
||||||
|
bool includeAllGroupChats = 5;
|
||||||
|
FolderType folderType = 6;
|
||||||
|
repeated uint64 includedRecipientIds = 7; // generated recipient id of groups, contacts, and/or note to self
|
||||||
|
repeated uint64 excludedRecipientIds = 8; // generated recipient id of groups, contacts, and/or note to self
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ message ArchiveUploadProgressState {
|
|||||||
Call = 4;
|
Call = 4;
|
||||||
Sticker = 5;
|
Sticker = 5;
|
||||||
Message = 6;
|
Message = 6;
|
||||||
|
NotificationProfile = 7;
|
||||||
|
ChatFolder = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
State state = 1;
|
State state = 1;
|
||||||
|
|||||||
@@ -24,29 +24,29 @@ import java.time.DayOfWeek
|
|||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
@Config(manifest = Config.NONE, application = Application::class)
|
@Config(manifest = Config.NONE, application = Application::class)
|
||||||
class NotificationProfileDatabaseTest {
|
class NotificationProfileTablesTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val appDependencies = MockAppDependenciesRule()
|
val appDependencies = MockAppDependenciesRule()
|
||||||
|
|
||||||
private lateinit var db: SQLiteDatabase
|
private lateinit var db: SQLiteDatabase
|
||||||
private lateinit var database: NotificationProfileDatabase
|
private lateinit var database: NotificationProfileTables
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
|
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
|
||||||
NotificationProfileDatabase.CREATE_TABLE.forEach {
|
NotificationProfileTables.CREATE_TABLE.forEach {
|
||||||
println(it)
|
println(it)
|
||||||
this.execSQL(it)
|
this.execSQL(it)
|
||||||
}
|
}
|
||||||
NotificationProfileDatabase.CREATE_INDEXES.forEach {
|
NotificationProfileTables.CREATE_INDEXES.forEach {
|
||||||
println(it)
|
println(it)
|
||||||
this.execSQL(it)
|
this.execSQL(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db = sqlCipher.writableDatabase
|
db = sqlCipher.writableDatabase
|
||||||
database = NotificationProfileDatabase(ApplicationProvider.getApplicationContext(), sqlCipher)
|
database = NotificationProfileTables(ApplicationProvider.getApplicationContext(), sqlCipher)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -168,5 +168,5 @@ class NotificationProfileDatabaseTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val NotificationProfileDatabase.NotificationProfileChangeResult.profile: NotificationProfile
|
private val NotificationProfileTables.NotificationProfileChangeResult.profile: NotificationProfile
|
||||||
get() = (this as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
|
get() = (this as NotificationProfileTables.NotificationProfileChangeResult.Success).notificationProfile
|
||||||
Reference in New Issue
Block a user