Add notification profile and chat folder backupv2 proto support.

This commit is contained in:
Cody Henthorne
2024-12-09 11:04:32 -05:00
committed by Greyson Parrelli
parent c91123e8e8
commit d1bfa6ee9e
34 changed files with 469 additions and 37 deletions

View File

@@ -66,6 +66,11 @@ class ArchiveImportExportTests {
runTests { it.startsWith("chat_") && !it.contains("_item") }
}
// @Test
fun chatFolders() {
runTests { it.startsWith("chat_folder_") }
}
// @Test
fun chatItemContactMessage() {
runTests { it.startsWith("chat_item_contact_message_") }
@@ -191,6 +196,11 @@ class ArchiveImportExportTests {
runTests { it.startsWith("chat_item_view_once_") }
}
// @Test
fun notificationProfiles() {
runTests { it.startsWith("notification_profile_") }
}
// @Test
fun recipientCallLink() {
runTests { it.startsWith("recipient_call_link_") }

View File

@@ -1026,7 +1026,7 @@ class RecipientTableTest_getAndPossiblyMerge {
}
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 {

View File

@@ -156,6 +156,14 @@ object ArchiveUploadProgress {
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) {
updatePhase(ArchiveUploadProgressState.BackupPhase.Message, currentProgress, approximateCount)
}

View File

@@ -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.AdHocCallArchiveProcessor
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.NotificationProfileProcessor
import org.thoughtcrime.securesms.backup.v2.processor.RecipientArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.StickerArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
@@ -576,6 +578,28 @@ object BackupRepository {
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 frameCountStart = frameCount
progressEmitter?.onMessage(0, approximateMessageCount)
@@ -727,9 +751,6 @@ object BackupRepository {
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
SignalDatabase.recipients.setProfileSharing(selfId, true)
// Add back default All Chats chat folder after clearing data
SignalDatabase.chatFolders.insertAllChatFolder()
val importState = ImportState(messageBackupKey, mediaRootBackupKey)
val chatItemInserter: ChatItemArchiveImporter = ChatItemArchiveProcessor.beginImport(importState)
@@ -768,6 +789,18 @@ object BackupRepository {
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 -> {
chatItemInserter.import(frame.chatItem)
eventTimer.emit("chatItem")
@@ -791,6 +824,11 @@ object BackupRepository {
eventTimer.emit("chatItem")
}
if (!importState.importedChatFolders) {
// Add back default All Chats chat folder after clearing data if missing
SignalDatabase.chatFolders.insertAllChatFolder()
}
stopwatch.split("frames")
Log.d(TAG, "[import] Rebuilding FTS index...")
@@ -1449,6 +1487,8 @@ object BackupRepository {
fun onThread()
fun onCall()
fun onSticker()
fun onNotificationProfile()
fun onChatFolder()
fun onMessage(currentProgress: Long, approximateCount: 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 chatIdToBackupRecipientId: 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 {
return remoteToLocalRecipientId[remoteId] ?: throw IllegalArgumentException("There is no local recipientId for remote recipientId $remoteId!")
}
fun getNextChatFolderPosition(): Int {
return chatFolderPosition++
}
}
class BackupMetadata(

View File

@@ -12,6 +12,8 @@ class LocalBackupV2Event(val type: Type, val count: Long = 0, val estimatedTotal
PROGRESS_THREAD,
PROGRESS_CALL,
PROGRESS_STICKER,
NOTIFICATION_PROFILE,
CHAT_FOLDER,
PROGRESS_MESSAGE,
PROGRESS_ATTACHMENT,
PROGRESS_VERIFYING,

View File

@@ -169,6 +169,14 @@ object LocalArchiver {
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) {
if (currentProgress == 0L) {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_MESSAGE))

View File

@@ -46,5 +46,6 @@ object ChatArchiveProcessor {
importState.chatIdToLocalRecipientId[chat.id] = recipientId
importState.chatIdToLocalThreadId[chat.id] = threadId
importState.chatIdToBackupRecipientId[chat.id] = chat.recipientId
importState.recipientIdToLocalThreadId[recipientId] = threadId
}
}

View File

@@ -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
)
}

View File

@@ -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
}
}

View File

@@ -49,4 +49,15 @@ data class ChatFolderRecord(
}
}
}
companion object {
fun getAllChatsFolderForBackup(): ChatFolderRecord {
return ChatFolderRecord(
folderType = ChatFolderRecord.FolderType.ALL,
showIndividualChats = true,
showGroupChats = true,
showMutedChats = true
)
}
}
}

View File

@@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
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
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 ->
when (r) {
is NotificationProfileDatabase.NotificationProfileChangeResult.Success -> SaveNotificationProfileResult.Success(r.notificationProfile, createMode)
NotificationProfileDatabase.NotificationProfileChangeResult.DuplicateName -> SaveNotificationProfileResult.DuplicateNameFailure
is NotificationProfileTables.NotificationProfileChangeResult.Success -> SaveNotificationProfileResult.Success(r.notificationProfile, createMode)
NotificationProfileTables.NotificationProfileChangeResult.DuplicateName -> SaveNotificationProfileResult.DuplicateNameFailure
}
}.observeOn(AndroidSchedulers.mainThread())
}

View File

@@ -8,7 +8,7 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
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.SignalDatabase
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.
*/
class NotificationProfilesRepository {
private val database: NotificationProfileDatabase = SignalDatabase.notificationProfiles
private val database: NotificationProfileTables = SignalDatabase.notificationProfiles
fun getProfiles(): Flowable<List<NotificationProfile>> {
return RxDatabaseObserver
@@ -54,17 +54,17 @@ class NotificationProfilesRepository {
}.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()) }
.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) }
.subscribeOn(Schedulers.io())
}
fun updateProfile(profile: NotificationProfile): Single<NotificationProfileDatabase.NotificationProfileChangeResult> {
fun updateProfile(profile: NotificationProfile): Single<NotificationProfileTables.NotificationProfileChangeResult> {
return Single.fromCallable { database.updateProfile(profile) }
.subscribeOn(Schedulers.io())
}
@@ -99,7 +99,7 @@ class NotificationProfilesRepository {
.take(1)
.singleOrError()
.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> {
@@ -107,7 +107,7 @@ class NotificationProfilesRepository {
.take(1)
.singleOrError()
.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 {

View File

@@ -4,9 +4,11 @@ import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* A serializable set of color constants that can be used for avatars.
@@ -27,8 +29,11 @@ public enum AvatarColor {
UNKNOWN("UNKNOWN", 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<>();
static {
for (AvatarColor color : AvatarColor.values()) {
NAME_MAP.put(color.serialize(), color);
@@ -97,7 +102,9 @@ public enum AvatarColor {
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[] {
A100,
A110,
@@ -137,4 +144,11 @@ public enum AvatarColor {
public static @NonNull AvatarColor deserialize(@Nullable String name) {
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);
}
}

View File

@@ -22,7 +22,7 @@ import java.time.DayOfWeek
/**
* 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 {
@JvmField
@@ -32,7 +32,7 @@ class NotificationProfileDatabase(context: Context, databaseHelper: SignalDataba
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 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 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)"
}
private object NotificationProfileAllowedMembersTable {
object NotificationProfileAllowedMembersTable {
const val TABLE_NAME = "notification_profile_allowed_members"
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() })
}

View File

@@ -63,7 +63,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val messageSendLogTables: MessageSendLogTables = MessageSendLogTables(context, this)
val avatarPickerDatabase: AvatarPickerDatabase = AvatarPickerDatabase(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 distributionListTables: DistributionListTables = DistributionListTables(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, RemappedRecordTables.CREATE_TABLE)
executeStatements(db, MessageSendLogTables.CREATE_TABLE)
executeStatements(db, NotificationProfileDatabase.CREATE_TABLE)
executeStatements(db, NotificationProfileTables.CREATE_TABLE)
executeStatements(db, DistributionListTables.CREATE_TABLE)
executeStatements(db, ChatFolderTables.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, PaymentTable.CREATE_INDEXES)
executeStatements(db, MessageSendLogTables.CREATE_INDEXES)
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
executeStatements(db, NotificationProfileTables.CREATE_INDEXES)
executeStatements(db, DonationReceiptTable.CREATE_INDEXS)
executeStatements(db, StorySendTable.CREATE_INDEXS)
executeStatements(db, DistributionListTables.CREATE_INDEXES)
@@ -456,8 +456,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
@get:JvmStatic
@get:JvmName("notificationProfiles")
val notificationProfiles: NotificationProfileDatabase
get() = instance!!.notificationProfileDatabase
val notificationProfiles: NotificationProfileTables
get() = instance!!.notificationProfileTables
@get:JvmStatic
@get:JvmName("payments")

View File

@@ -18,9 +18,13 @@ message BackupInfo {
// e.g. a Recipient must come before any Chat referencing it.
// 3. All ChatItems must appear in global Chat rendering order.
// (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,
// or may each immediately precede its first ChatItem.
message Frame {
@@ -31,6 +35,8 @@ message Frame {
ChatItem chatItem = 4;
StickerPack stickerPack = 5;
AdHocCall adHocCall = 6;
NotificationProfile notificationProfile = 7;
ChatFolder chatFolder = 8;
}
}
@@ -1183,3 +1189,48 @@ message ChatStyle {
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
}

View File

@@ -36,6 +36,8 @@ message ArchiveUploadProgressState {
Call = 4;
Sticker = 5;
Message = 6;
NotificationProfile = 7;
ChatFolder = 8;
}
State state = 1;

View File

@@ -24,29 +24,29 @@ import java.time.DayOfWeek
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class NotificationProfileDatabaseTest {
class NotificationProfileTablesTest {
@get:Rule
val appDependencies = MockAppDependenciesRule()
private lateinit var db: SQLiteDatabase
private lateinit var database: NotificationProfileDatabase
private lateinit var database: NotificationProfileTables
@Before
fun setup() {
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
NotificationProfileDatabase.CREATE_TABLE.forEach {
NotificationProfileTables.CREATE_TABLE.forEach {
println(it)
this.execSQL(it)
}
NotificationProfileDatabase.CREATE_INDEXES.forEach {
NotificationProfileTables.CREATE_INDEXES.forEach {
println(it)
this.execSQL(it)
}
}
db = sqlCipher.writableDatabase
database = NotificationProfileDatabase(ApplicationProvider.getApplicationContext(), sqlCipher)
database = NotificationProfileTables(ApplicationProvider.getApplicationContext(), sqlCipher)
}
@After
@@ -168,5 +168,5 @@ class NotificationProfileDatabaseTest {
}
}
private val NotificationProfileDatabase.NotificationProfileChangeResult.profile: NotificationProfile
get() = (this as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
private val NotificationProfileTables.NotificationProfileChangeResult.profile: NotificationProfile
get() = (this as NotificationProfileTables.NotificationProfileChangeResult.Success).notificationProfile