From d1bfa6ee9e10f3fabd1eed01f8fe0c4eaf558e78 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 9 Dec 2024 11:04:32 -0500 Subject: [PATCH] Add notification profile and chat folder backupv2 proto support. --- .../backupTests/chat_folder_00.binproto | Bin 0 -> 814 bytes .../backupTests/chat_folder_01.binproto | Bin 0 -> 808 bytes .../backupTests/chat_folder_02.binproto | Bin 0 -> 822 bytes .../backupTests/chat_folder_03.binproto | Bin 0 -> 808 bytes .../notification_profile_00.binproto | Bin 0 -> 814 bytes .../notification_profile_01.binproto | Bin 0 -> 831 bytes .../notification_profile_02.binproto | Bin 0 -> 836 bytes .../notification_profile_03.binproto | Bin 0 -> 825 bytes .../notification_profile_04.binproto | Bin 0 -> 844 bytes .../notification_profile_05.binproto | Bin 0 -> 842 bytes .../notification_profile_06.binproto | Bin 0 -> 830 bytes .../notification_profile_07.binproto | Bin 0 -> 845 bytes .../notification_profile_08.binproto | Bin 0 -> 828 bytes .../notification_profile_09.binproto | Bin 0 -> 819 bytes .../notification_profile_10.binproto | Bin 0 -> 844 bytes .../notification_profile_11.binproto | Bin 0 -> 840 bytes .../backup/v2/ArchiveImportExportTests.kt | 12 +- .../RecipientTableTest_getAndPossiblyMerge.kt | 2 +- .../securesms/backup/ArchiveUploadProgress.kt | 8 + .../securesms/backup/v2/BackupRepository.kt | 56 ++++++- .../securesms/backup/v2/LocalBackupV2Event.kt | 2 + .../backup/v2/local/LocalArchiver.kt | 8 + .../v2/processor/ChatArchiveProcessor.kt | 1 + .../v2/processor/ChatFolderProcessor.kt | 144 ++++++++++++++++++ .../processor/NotificationProfileProcessor.kt | 131 ++++++++++++++++ .../app/chats/folders/ChatFolderRecord.kt | 11 ++ .../EditNotificationProfileViewModel.kt | 6 +- .../NotificationProfilesRepository.kt | 14 +- .../conversation/colors/AvatarColor.java | 18 ++- .../database/NotificationProfileTables.kt | 10 +- .../securesms/database/SignalDatabase.kt | 10 +- app/src/main/protowire/Backup.proto | 57 ++++++- app/src/main/protowire/KeyValue.proto | 2 + ...st.kt => NotificationProfileTablesTest.kt} | 14 +- 34 files changed, 469 insertions(+), 37 deletions(-) create mode 100644 app/src/androidTest/assets/backupTests/chat_folder_00.binproto create mode 100644 app/src/androidTest/assets/backupTests/chat_folder_01.binproto create mode 100644 app/src/androidTest/assets/backupTests/chat_folder_02.binproto create mode 100644 app/src/androidTest/assets/backupTests/chat_folder_03.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_00.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_01.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_02.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_03.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_04.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_05.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_06.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_07.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_08.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_09.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_10.binproto create mode 100644 app/src/androidTest/assets/backupTests/notification_profile_11.binproto create mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatFolderProcessor.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/NotificationProfileProcessor.kt rename app/src/test/java/org/thoughtcrime/securesms/database/{NotificationProfileDatabaseTest.kt => NotificationProfileTablesTest.kt} (92%) diff --git a/app/src/androidTest/assets/backupTests/chat_folder_00.binproto b/app/src/androidTest/assets/backupTests/chat_folder_00.binproto new file mode 100644 index 0000000000000000000000000000000000000000..8542eb0bc65f268ddba699f6d89bf1469d1972a9 GIT binary patch literal 814 zcmdPqU=+CVYr*{QhEfXsyKEj#$*x#(ig%Lc-3q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@sV_r#SenDcc0h1LYD*&6p+7kc( literal 0 HcmV?d00001 diff --git a/app/src/androidTest/assets/backupTests/chat_folder_02.binproto b/app/src/androidTest/assets/backupTests/chat_folder_02.binproto new file mode 100644 index 0000000000000000000000000000000000000000..9b45d2852aa01d4b2754440b7e1b671d4b78dbaf GIT binary patch literal 822 zcmdPqU=+CVYr*{QhEfXsyKEj#$*x#(ig%Lc-3q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@?Z{ literal 0 HcmV?d00001 diff --git a/app/src/androidTest/assets/backupTests/notification_profile_00.binproto b/app/src/androidTest/assets/backupTests/notification_profile_00.binproto new file mode 100644 index 0000000000000000000000000000000000000000..ff34e2c8b3b3c6daff1dc334d9b36d48e7a62f05 GIT binary patch literal 814 zcmdPqU=+CVYr*{QhEfXsyKEj#$*x#(ig%Lc-3q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@MC1onOWF{w;2(f&aKe1Q#!;2gL75@C0dw7C5 XC{q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@a NY+{aJiC~Rj0|1U9;imuq literal 0 HcmV?d00001 diff --git a/app/src/androidTest/assets/backupTests/notification_profile_04.binproto b/app/src/androidTest/assets/backupTests/notification_profile_04.binproto new file mode 100644 index 0000000000000000000000000000000000000000..36e205d288a0e08c7147775c48ee94046189ab6e GIT binary patch literal 844 zcmdPqU=+CVYr*{QhEfXsyKEj#$*x#(ig%Lc-3q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@r6%gx1 literal 0 HcmV?d00001 diff --git a/app/src/androidTest/assets/backupTests/notification_profile_05.binproto b/app/src/androidTest/assets/backupTests/notification_profile_05.binproto new file mode 100644 index 0000000000000000000000000000000000000000..0dacc8abbe63daff0d4807b4cd6a8f806ff570c1 GIT binary patch literal 842 zcmdPqU=+CVYr*{QhEfXsyKEj#$*x#(ig%Lc-3q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@MC1onOWF{w;2yuUyKXK)whTaeJC+(K~_xS67 fg+G7h9-d%s#mM5ZUNB%2a|BBSYXn;adjul@aoFrp literal 0 HcmV?d00001 diff --git a/app/src/androidTest/assets/backupTests/notification_profile_06.binproto b/app/src/androidTest/assets/backupTests/notification_profile_06.binproto new file mode 100644 index 0000000000000000000000000000000000000000..1f29253765b6f9eaf14ea90894d3e9246a8803c8 GIT binary patch literal 830 zcmdPqU=+CVYr*{QhEfXsyKEj#$*x#(ig%Lc-3q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@Rah@afOyBIF}}6=42+yzPa)3KPVkpF|l$vFnX*P4A{gR Q!4$z9!4kn5!4|;|0NC5$(*OVf literal 0 HcmV?d00001 diff --git a/app/src/androidTest/assets/backupTests/notification_profile_07.binproto b/app/src/androidTest/assets/backupTests/notification_profile_07.binproto new file mode 100644 index 0000000000000000000000000000000000000000..3a6c1bb54a5d1adbd92f25c9c19dce074deaa6c3 GIT binary patch literal 845 zcmdPqU=+CVYr*{QhEfXsyKEj#$*x#(ig%Lc-3q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@6;|wO hSijrcikXF#%VWJ@z$WGh#t5be<_MMu)(Ex;b^yca=?DM- literal 0 HcmV?d00001 diff --git a/app/src/androidTest/assets/backupTests/notification_profile_08.binproto b/app/src/androidTest/assets/backupTests/notification_profile_08.binproto new file mode 100644 index 0000000000000000000000000000000000000000..7cd1433e0cb23aba66593dc0eafc66a914cb4cf3 GIT binary patch literal 828 zcmdPqU=+CVYr*{QhEfXsyKEj#$*x#(ig%Lc-3q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@RM@Yar@>MC1onOWF{w;2(f&aKe1Q#!qq44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@#LDHs=&@cfU=wo$Qv@>r1DNWT literal 0 HcmV?d00001 diff --git a/app/src/androidTest/assets/backupTests/notification_profile_11.binproto b/app/src/androidTest/assets/backupTests/notification_profile_11.binproto new file mode 100644 index 0000000000000000000000000000000000000000..2904964ba32bdd5304f89772327154b4c9be819a GIT binary patch literal 840 zcmdPqU=+CVYr*{QhEfXsyKEj#$*x#(ig%Lc-3q44 z*Nj{WiA)n$zdh{a=`nZlEW?va&(1u1DgSG`(W8vV;qMqqZ{85%Ny<-3j897~DbX`C zlQQE{NKKC^zQDIZfc5e28ZY-x(@v*`eD7>j?ht(`dPC)}-ee(x#Dk2B7_K^rcOGJu zxZzp#L4rkz#R+JN7K<&CWWbw?)7pnOnIo<%F(RuY4?hFXG;bU1D)!epU<+cd&xp!NARp z<_=U5CWUDbcd!UJ;zd!+z^F)K^~%pnEe5(m_g+r!`StvNPal2s>Lg#3#G%a02{lvn z);?bF*mX+xqq+)4D~2_ULMs_LxTKZ<(+f&qVc`fcYSjq|aPj%3>L`FctfK(5Pf9MM zq@MC1onOWF{w;2yuUyKXK)whTaeJC+(KKdHwo- dg+G7h9-d%s#mvIW<*{BcU=wo$O9X2K8vu#I>sJ5( literal 0 HcmV?d00001 diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt index 39f605eb24..e010a89caa 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt @@ -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,7 +196,12 @@ class ArchiveImportExportTests { runTests { it.startsWith("chat_item_view_once_") } } - // @Test +// @Test + fun notificationProfiles() { + runTests { it.startsWith("notification_profile_") } + } + +// @Test fun recipientCallLink() { runTests { it.startsWith("recipient_call_link_") } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt index 5f4ef1fcae..5ae0b1744d 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt index 4a13daf424..ffd5686e42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 197d660c9a..bd1b26494a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -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 = hashMapOf() val chatIdToBackupRecipientId: MutableMap = hashMapOf() val remoteToLocalColorId: MutableMap = hashMapOf() + val recipientIdToLocalThreadId: MutableMap = hashMapOf() + val recipientIdToIsGroup: MutableMap = 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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/LocalBackupV2Event.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/LocalBackupV2Event.kt index d3f681baed..51f5e38dde 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/LocalBackupV2Event.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/LocalBackupV2Event.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt index 52800e768f..66eb222ef6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt @@ -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)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatArchiveProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatArchiveProcessor.kt index 274dc47e4f..08aed203c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatArchiveProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatArchiveProcessor.kt @@ -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 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatFolderProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatFolderProcessor.kt new file mode 100644 index 0000000000..f228644737 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatFolderProcessor.kt @@ -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, excludedRecipientIds: List): 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.toMembershipInsertQueries(chatFolderId: Long, importState: ImportState, membershipType: MembershipType): List { + 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 + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/NotificationProfileProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/NotificationProfileProcessor.kt new file mode 100644 index 0000000000..81a110c157 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/NotificationProfileProcessor.kt @@ -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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFolderRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFolderRecord.kt index 7ffcafff0e..112610b946 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFolderRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFolderRecord.kt @@ -49,4 +49,15 @@ data class ChatFolderRecord( } } } + + companion object { + fun getAllChatsFolderForBackup(): ChatFolderRecord { + return ChatFolderRecord( + folderType = ChatFolderRecord.FolderType.ALL, + showIndividualChats = true, + showGroupChats = true, + showMutedChats = true + ) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/EditNotificationProfileViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/EditNotificationProfileViewModel.kt index 1b3b22d740..717da2070b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/EditNotificationProfileViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/EditNotificationProfileViewModel.kt @@ -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()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/NotificationProfilesRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/NotificationProfilesRepository.kt index 7937647951..aac2e1d69e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/NotificationProfilesRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/profiles/NotificationProfilesRepository.kt @@ -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> { return RxDatabaseObserver @@ -54,17 +54,17 @@ class NotificationProfilesRepository { }.subscribeOn(Schedulers.io()) } - fun createProfile(name: String, selectedEmoji: String): Single { + fun createProfile(name: String, selectedEmoji: String): Single { 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 { + fun updateProfile(profileId: Long, name: String, selectedEmoji: String): Single { return Single.fromCallable { database.updateProfile(profileId = profileId, name = name, emoji = selectedEmoji) } .subscribeOn(Schedulers.io()) } - fun updateProfile(profile: NotificationProfile): Single { + fun updateProfile(profile: NotificationProfile): Single { 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 { @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java index 854e48babe..f88433b087 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java @@ -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 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); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt b/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt index efe3ce6b44..255ecba35b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/NotificationProfileTables.kt @@ -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 = 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.serialize(): String { +fun Iterable.serialize(): String { return joinToString(separator = ",", transform = { it.serialize() }) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt index 21c4256336..eeb03a6809 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt @@ -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") diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index ca5917a295..3ae0e294e9 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -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; } } @@ -1182,4 +1188,49 @@ message ChatStyle { } bool dimWallpaperInDarkMode = 7; -} \ No newline at end of file +} + +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 +} diff --git a/app/src/main/protowire/KeyValue.proto b/app/src/main/protowire/KeyValue.proto index 550dfb0a2e..9b4ee7a308 100644 --- a/app/src/main/protowire/KeyValue.proto +++ b/app/src/main/protowire/KeyValue.proto @@ -36,6 +36,8 @@ message ArchiveUploadProgressState { Call = 4; Sticker = 5; Message = 6; + NotificationProfile = 7; + ChatFolder = 8; } State state = 1; diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileDatabaseTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt similarity index 92% rename from app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileDatabaseTest.kt rename to app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt index 1d531f33e6..d915f920aa 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileDatabaseTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/NotificationProfileTablesTest.kt @@ -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