Compare commits

..

243 Commits

Author SHA1 Message Date
Clark Chen
1f2496625e Bump version to 7.10.3 2024-07-02 10:32:22 -04:00
Clark Chen
aaacd28d86 Update translations and other static files. 2024-07-02 10:25:54 -04:00
Michelle Tang
c24473e176 Fix default name for linked devices. 2024-07-01 16:44:53 -04:00
mtang-signal
1311ec498f Default to back camera when linking device. 2024-07-01 08:59:35 -04:00
Nicholas Tinsley
251cec5dee Bump version to 7.10.2 2024-06-28 15:23:01 -04:00
Nicholas Tinsley
1e15a8c1d3 Update translations and other static files. 2024-06-28 15:22:12 -04:00
Michelle Tang
9c5c58794b Fix invalid qr code crash. 2024-06-27 16:12:38 -04:00
Nicholas Tinsley
50063854d7 Bump version to 7.10.1 2024-06-26 13:57:39 -04:00
Nicholas Tinsley
79d2041e46 Update translations and other static files. 2024-06-26 13:45:34 -04:00
Nicholas Tinsley
479b27ce94 Fix benchmark tests. 2024-06-26 13:39:28 -04:00
Cody Henthorne
a66857a7cc Fix incorrect local group state bug. 2024-06-26 13:39:28 -04:00
Alex Hart
37815a3f39 Fix bug with cut off avatars. 2024-06-26 13:39:28 -04:00
Alex Hart
b55ba67b66 Split out backup and subscription error sheet handling. 2024-06-26 13:39:28 -04:00
Michelle Tang
37a2d5fbca Release updated linked devices. 2024-06-26 13:39:28 -04:00
Nicholas Tinsley
d7b5c6bff3 Delete registration V1. 2024-06-26 13:39:28 -04:00
Alex Hart
f11028529e Fix hard coded placeholder in iDEAL dialog. 2024-06-25 10:19:27 -03:00
Greyson Parrelli
93ec322bb9 Bump version to 7.10.0 2024-06-24 15:37:57 -04:00
Greyson Parrelli
71e0468d2c Update translations and other static files. 2024-06-24 15:37:08 -04:00
moiseev-signal
816e3442a0 Adopt libsignal 0.51.1 2024-06-24 15:04:03 -04:00
Cody Henthorne
c37ed722dc Attempt to fix potential draft setting loop. 2024-06-24 15:04:03 -04:00
Michelle Tang
e08c2966c3 Move biometrics check when linking a device. 2024-06-24 15:04:03 -04:00
Cody Henthorne
976f80ff7e Fix conversation not closing after delete bug. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
34a4bda331 Do not send PNI-hello-worlds for new installs. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
a4077ccb4a Add app bundle support.
Co-authored-by: Joshua Lund <josh@signal.org>
2024-06-24 15:04:03 -04:00
Cody Henthorne
21ada2a503 Fix request to rejoin after group updates bug. 2024-06-24 15:04:03 -04:00
Alex Hart
57a70c3085 Quiet down auth check job. 2024-06-24 15:04:03 -04:00
Cody Henthorne
16c8b88f0f Fix multiple text attachments when forwarding to multiple recipients.
Fixes #13593
2024-06-24 15:04:03 -04:00
Michelle Tang
b806952430 Add device-specific support configs. 2024-06-24 15:04:03 -04:00
Alex Hart
c0da0bd272 Add proper payment method type to BackupTypeSettings screen. 2024-06-24 15:04:03 -04:00
Alex Hart
45239c2264 Add payment history screens for backups. 2024-06-24 15:04:03 -04:00
Alex Hart
690236c4e5 Handle manual cancellation UI hint in DonationValues. 2024-06-24 15:04:03 -04:00
Clark
ebee3f72e6 Add test infrastructure for backup binprotos. 2024-06-24 15:04:03 -04:00
Alex Hart
37cec7d44f Implement 1:1 call event delete syncs. 2024-06-24 15:04:03 -04:00
Alex Hart
187fd63a75 Add content for disk full alert for backups. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
362cdfc463 Use a snapshot of the SignalStore during backups. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
863b443317 Convert SignalStore to kotlin. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
341c474610 Remove some indirect database reads from backup export. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
cbb3c0911c Create backups from copies of the database file.
Still more work here to do with regards to certain tables, like
SignalStore and Recipient.
2024-06-24 15:04:03 -04:00
Greyson Parrelli
890facc6f6 Clean up ChatItemExportIterator. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
6fa8337058 Update to the latest backup v2 spec.
Removes some dead protos, removes some sticker details, adds in gift
badges.
2024-06-24 15:04:03 -04:00
Alex Hart
3f1cb65e02 Migrate translatable strings from fragment to xml. 2024-06-24 15:04:03 -04:00
Cody Henthorne
3551e7ec00 Remove rx send remote config and group send using rx always. 2024-06-24 15:04:03 -04:00
Alex Hart
5ecf60a306 Add ability to turn off and delete backups. 2024-06-24 15:04:03 -04:00
Cody Henthorne
6659700a1c Improve delete sync coverage for partial expiring threads. 2024-06-24 15:04:02 -04:00
Cody Henthorne
070174fee6 Add delete sync capability to log section. 2024-06-24 15:04:02 -04:00
Cody Henthorne
09003d85b1 Add single attachment delete sync. 2024-06-24 15:04:02 -04:00
Alex Hart
ea87108def Heal InAppPaymentSubscriber currency if we have a payment with a matching subscriber id. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
7a696f9a62 Increase pluralization for raised hand snackbar. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
8ba57a2733 Upgrade OkHttp to 4.12.
Addresses #13491
2024-06-24 15:04:02 -04:00
Cody Henthorne
9824cc2cbe Update delete sync strings. 2024-06-24 15:04:02 -04:00
Michelle Tang
ad60cc72cb Add empty linked device state. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
1950b80402 Update AndroidX Camera libraries to 1.3.4. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
2acb47952b Differently pluralize raise hand strings. 2024-06-24 15:04:02 -04:00
Cody Henthorne
14cacaef86 Add verbose tracking to DelteForMeSync test to help finding flakey test. 2024-06-24 15:04:02 -04:00
Michelle Tang
958e815933 Remove ability to scan qr code from gallery. 2024-06-24 15:04:02 -04:00
Alex Hart
6b50be78c0 Implement start of backups payment integration work. 2024-06-24 15:04:02 -04:00
Michelle Tang
680223c4b6 Update permission buttons for contacts.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2024-06-24 15:04:02 -04:00
Greyson Parrelli
1af914d5ef Use SubsamplingImageView for everything except GIFs.
Fixes #10324
2024-06-24 15:04:02 -04:00
Cody Henthorne
a2fc710261 Add support for addressing attachments within a message. 2024-06-24 15:04:02 -04:00
Dan Brunwasser
10922594b3 Improve system emoji rendering across the app with EmojiCompat2.
Resolves #13327
2024-06-24 15:04:02 -04:00
Michelle Tang
abd80c5204 Update linked devices UI. 2024-06-24 15:04:02 -04:00
Alex Hart
ff589e3b91 Fix call link export crash. 2024-06-24 15:04:02 -04:00
Cody Henthorne
c80ccd70ec Add additional delete sync support. 2024-06-24 15:04:02 -04:00
Jim Gustafson
d22d18da47 Update to RingRTC v2.44.0 2024-06-24 15:04:02 -04:00
Clark
75b41c34ea Add import/export for stickers and sticker packs. 2024-06-24 15:04:02 -04:00
Alex Hart
11557e4815 Rewrite fallbackphoto system. 2024-06-24 15:04:02 -04:00
Greyson Parrelli
d698f74d0b Rename FeatureFlags -> RemoteConfig. 2024-06-24 15:04:02 -04:00
Greyson Parrelli
ecbea9fd95 Improve FeatureFlag change detection, use for SVR3. 2024-06-24 15:04:02 -04:00
Greyson Parrelli
13f7a64139 Refactor FeatureFlags. 2024-06-24 15:04:02 -04:00
Michelle Tang
39cb1c638e Remove sms tag from contacts. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
489b58ad67 Abort transcoding if frame processing gets stuck. 2024-06-24 15:04:02 -04:00
Cody Henthorne
f20fe33af9 Ignore flakey delete sync test. 2024-06-24 15:04:02 -04:00
Clark
6adddf4a0c Add display of last backup time to restore flow. 2024-06-24 15:04:01 -04:00
Clark Chen
16773c9b17 Fix import/export tests with my story. 2024-06-24 15:04:01 -04:00
Michelle Tang
4b50365fa9 Release updated linked devices behind feature flag. 2024-06-24 15:04:01 -04:00
Clark Chen
98766b9ebb Fix backup type dark mode. 2024-06-24 15:04:01 -04:00
Cody Henthorne
45a739ce92 Show notification for group adds. 2024-06-24 15:04:01 -04:00
Clark
c0d7145ada Add handling for "My Story" import/export. 2024-06-24 15:04:01 -04:00
Clark
f94c007af8 Make message backup settings screen update properly. 2024-06-24 15:04:01 -04:00
Michelle Tang
df19cb5795 Increase minimum height of keyboard. 2024-06-24 15:04:01 -04:00
Nicholas Tinsley
e6ceb55092 Match account deletion number by short NSN.
Fixes #13583.
2024-06-24 15:04:01 -04:00
Michelle Tang
bfe2b5cba9 Add loading screen to linked devices. 2024-06-21 09:19:47 -03:00
Alex Hart
571004df50 Tokenize group title search. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
f32b59f0aa Fix crash with AnalyzeDatabaseJob. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
e4019d8595 Ignore deprecated backup tests. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
0b66a8701e Convert FeatureFlags to kotlin. 2024-06-21 09:19:47 -03:00
Clark
e62b8de1bc Fix most of import/export integration tests. 2024-06-21 09:19:47 -03:00
Michelle Tang
d5cd790871 Remove redundant gallery permission ask. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
664c22d8f1 Add mostly-working SVR3 implementation behind flag. 2024-06-21 09:19:47 -03:00
Cody Henthorne
143a61e312 Fix calling error state display bugs. 2024-06-21 09:19:47 -03:00
Michelle Tang
baaad0e475 Fix camera-first qr scans. 2024-06-21 09:19:47 -03:00
Michelle Tang
7086709082 Update devices screen after linking a new device. 2024-06-21 09:19:47 -03:00
Michelle Tang
7bd5ad8c0b Use common compose qr screen for usernames. 2024-06-21 09:19:47 -03:00
Michelle Tang
df19c91ae2 Add padding to quoted messages. 2024-06-21 09:19:47 -03:00
Clark
e5872037e0 Add import/export of chat colors. 2024-06-21 09:19:47 -03:00
Clark
b782fabbb6 Update backup proto with subscriber and recipient changes. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
485b466bd2 Crash on RuntimeExceptions thrown during all Jobs. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
3beac6dfa9 Fix linked device inactive filtering. 2024-06-21 09:19:47 -03:00
Michelle Tang
98290a9fa3 Update max limit string. 2024-06-21 09:19:47 -03:00
Michelle Tang
13dd59f226 Skip biometrics check if unavailable when linking a device. 2024-06-21 09:19:47 -03:00
Michelle Tang
d9c42a4135 Add ability to scan linked device qr code from gallery. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
644b93e5a3 Provide default text background color. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
3ff218f9c6 Make build deprecation more resilient to clock skew. 2024-06-21 09:19:47 -03:00
Alex Hart
f572eb5322 Add CallLink Observed event and handling. 2024-06-21 09:19:47 -03:00
Michelle Tang
d3eb480d31 Update add linked devices screen. 2024-06-21 09:19:47 -03:00
Michelle Tang
ac52b5b992 Update linked devices screen. 2024-06-21 09:19:47 -03:00
Michelle Tang
5c181e774f Prevent editing on stickers. 2024-06-21 09:19:46 -03:00
Michelle Tang
05d25718da Add animation when editing a message. 2024-06-21 09:19:46 -03:00
Clark
66c50bef44 Hook up message backup restore flow to reg v2.
Co-authored-by: Nicholas Tinsley <nicholas@signal.org>
2024-06-21 09:19:46 -03:00
Alex Hart
26bd59c378 Bump version to 7.9.6 2024-06-20 18:13:53 -03:00
Alex Hart
e90eae6080 Update baseline profile. 2024-06-20 18:08:30 -03:00
Alex Hart
86a7db7653 Update translations and other static files. 2024-06-20 18:05:24 -03:00
Cody Henthorne
230de7e9dc Use URL for S3.
Thanks to Oscar Mira <valldrac@molly.im> for bringing this to our attention.
2024-06-20 12:15:49 -04:00
Greyson Parrelli
4b8546a151 Bump version to 7.9.5 2024-06-14 15:16:17 -04:00
Greyson Parrelli
ecd214b91b Update translations and other static files. 2024-06-14 15:15:35 -04:00
Greyson Parrelli
6b5de6e3e5 Only do local donation cancel if it's currently active. 2024-06-14 15:06:13 -04:00
Greyson Parrelli
58b6e49aae Fix NPE when canceling a donation. 2024-06-14 15:06:13 -04:00
Greyson Parrelli
c480512600 Bump version to 7.9.4 2024-06-13 18:34:38 -04:00
Greyson Parrelli
3a5b6476aa Updated language translations. 2024-06-13 18:32:18 -04:00
Alex Hart
cb171092cf Fix crash loop when writing invalid currency . 2024-06-13 18:03:20 -03:00
Nicholas Tinsley
71979b34db Alert user to file system errors during backup restore. 2024-06-13 16:13:43 -04:00
Nicholas Tinsley
73142cea39 Don't hold lazy reference to view binding in delayed runnable. 2024-06-13 15:43:51 -04:00
Nicholas Tinsley
2ab2c6f039 Ensure that substrings match in the registration contact support bottom sheet. 2024-06-13 15:36:41 -04:00
Greyson Parrelli
0bea15c0af Bump version to 7.9.3 2024-06-12 11:23:20 -04:00
Greyson Parrelli
cbd78d78ba Update translations and other static files. 2024-06-12 11:22:44 -04:00
Cody Henthorne
ac0604a753 Fix rare remote megaphone crash. 2024-06-12 11:10:59 -04:00
Nicholas Tinsley
0e57335be1 Split Raise Hand plurals into separate strings. 2024-06-11 11:43:58 -04:00
Greyson Parrelli
c4e64f6fa3 Bump version to 7.9.2 2024-06-10 16:11:51 -04:00
Greyson Parrelli
bf9716f206 Update translations and other static files. 2024-06-10 16:10:42 -04:00
Cody Henthorne
057ffdbaaf Fix conversation memory leak. 2024-06-10 14:54:02 -04:00
Nicholas Tinsley
65dc0d3f34 Disable verbose logging in media converter. 2024-06-10 14:38:19 -04:00
Clark
173ee95e62 Fix backup jitter and add unit tests. 2024-06-10 14:20:56 -04:00
Nicholas Tinsley
789339afa7 Update Raise Hand string. 2024-06-10 11:01:56 -04:00
Nicholas Tinsley
21b518da7a Don't show volume indicator nor switch camera button until incoming call connects. 2024-06-10 11:01:56 -04:00
Nicholas Tinsley
57b6b8dcf1 Improve Raise Hand behavior when in a call with a linked device. 2024-06-07 13:53:58 -04:00
Cody Henthorne
543a85316e Improve FCM check clock skew handling. 2024-06-07 13:02:44 -04:00
Cody Henthorne
2fedb3a0ee Bump version to 7.9.1 2024-06-07 11:59:02 -04:00
Cody Henthorne
ae450aed67 Update baseline profile. 2024-06-07 11:48:08 -04:00
Cody Henthorne
0abb4727fc Update translations and other static files. 2024-06-07 11:30:34 -04:00
Alex Hart
4bc6eb96ff Fix 3DS waiting-for-auth state when launching external application. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
e6a126d416 Only submit captcha once in Registration V2. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
fdf858f379 Prevent crash if linked device also raises their hand. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
4151d123cd Fix crash in registration v2 country code drop down. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
c8a9759eba Unify Raise Hand copy. 2024-06-07 11:05:09 -04:00
Cody Henthorne
c59b74627f Improve strings for localization. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
f2191d2996 Adjust text colors in dark mode in Registration V2. 2024-06-07 11:05:09 -04:00
Cody Henthorne
7dfffbd50b Add missing windows aapt2. 2024-06-06 10:21:22 -04:00
Cody Henthorne
329fc52077 Bump version to 7.9.0 2024-06-05 16:20:29 -04:00
Cody Henthorne
8976111f61 Update translations and other static files. 2024-06-05 15:54:32 -04:00
Cody Henthorne
7402959ac6 Fix error handling for resumable uploads to cdn3. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
220d3877a2 Fix phone number autofill in Registration V2. 2024-06-05 15:46:01 -04:00
Clark Chen
380c33642c Clean quote when canceling edit message. 2024-06-05 15:46:01 -04:00
Alex Hart
7acb2bef3d Fix private story listing My Story as a recipient. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
1a103106a5 Catch more stuff in SqlCipherDeletingErrorHandler.
Fixes #13577
2024-06-05 15:46:01 -04:00
Greyson Parrelli
6025e423e8 Fix payment request message text. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
54656ea14e Potentially fix 'design assumption violated' ISE. 2024-06-05 15:46:01 -04:00
Alex Hart
fd00ed71b5 Fix clickable link in donation thanks sheet. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
d4fba5f3c7 Ship Raise Hand. 2024-06-05 15:46:01 -04:00
Alex Hart
ce244f2e8f Fix hit detection for story link previews. 2024-06-05 15:46:01 -04:00
Cody Henthorne
4ad466390f Fix second person translations for group story reactions. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
c5c9b09f7b Turn on Registration V2 and Change Number V2. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
0638b31c1f More registration lock V2 improvements. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
c3c713a75a Prevent getting stuck on registration lock V2 fragment. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
9af1c72233 Registration V2 restore tweaks. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
500a1e46ad Improve ABS logging. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
4b446877af Change number captcha submission improvement. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
015548613a Fix change view model test.
Co-authored-by: Cody Henthorne <cody@signal.org>
2024-06-05 15:46:01 -04:00
Alex Hart
30b339a482 Remove unnecessary subscriber call for manual cancel. 2024-06-05 15:46:01 -04:00
Cody Henthorne
6dcb2e8d24 Fix androidTest message content fuzer for attachment pointers. 2024-06-05 15:46:01 -04:00
Cody Henthorne
3e8e17526b Fix missing group context on message records. 2024-06-05 15:46:01 -04:00
Clark Chen
30ecaf7aea Remove double remote deleted column from ChatItemImportInserter. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
f761008509 Clean up some stuff around ImportExportTest. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
c3ab8dddd0 Fix runPostSuccessfulTransaction behavior. 2024-06-05 15:46:01 -04:00
Alex Hart
164f089d37 Fix NPE in deleteAll call. 2024-06-05 15:46:01 -04:00
Alex Hart
a021b400bd Fix SafetyNumberBottomSheetRepositoryTest. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
fac8f403be Remove outdated dlist test. 2024-06-05 15:46:01 -04:00
Clark
d85ab37828 Add import and tombstones for mobile coin payments. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
1e35403c87 Change Number V2. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
b99c2165fa Handle SVR exceptions in Registration V2. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
303100bb6b Further registration lock improvements in Registration V2. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
b71ba79b8a Flesh out registration lock support for Registration V2. 2024-06-05 15:46:01 -04:00
Clark
54cd84b842 Add handling for import/export of edited messages. 2024-06-05 15:46:01 -04:00
Clark
1565ecdcea Fix multiple scheduled backups due to jitter. 2024-06-05 15:46:01 -04:00
Clark
0a99b68d25 Fix shared contacts avatar double upload. 2024-06-05 15:46:01 -04:00
Alex Hart
f4fac5bd90 Prevent reactions from being overlaid by raise hand. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
f6760b90da Flesh out verification challenge support for registration v2. 2024-06-05 15:46:01 -04:00
Clark
ad9b1f05b4 Disable restore on open if auto-download is off. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
17581a7a5e Update SignalProgressDialog.
Fixes #12949.
2024-06-05 15:46:01 -04:00
Clark
b41bf66133 Disable "Edited" click listener for outgoing messages. 2024-06-05 15:46:01 -04:00
Clark
8bb3d71472 Fix thumbnail not being clickable on initial media receive. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
295d4b9466 Pin left GIF button to bottom of compose box. 2024-06-05 15:46:01 -04:00
Ahmed El herz
5e490376f4 Fix initial event not triggering onTouchEvent.
Fixes #13351
2024-06-05 15:46:01 -04:00
Greyson Parrelli
fa27531c00 Inline SVR2 feature flag. 2024-06-05 15:46:01 -04:00
Cody Henthorne
2737e5613c Use raw values for learned profile name event. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
d84612ebf4 Revert "Validate full edit message payload."
This reverts commit 268b621667e3144fb3f07099d04aa5609387a5e6.
2024-06-05 15:46:00 -04:00
Greyson Parrelli
96165ad5a8 Fix username getting prematurely removed from recipient. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
19caef057e Update to AGP 8.4.1. 2024-06-05 15:46:00 -04:00
Clark
29cafb11eb Update proto and add payments export without tombstone. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
7e458bfde0 Validate full edit message payload. 2024-06-05 15:46:00 -04:00
Clark
2a3cb80217 Add ui wiring for archive thumbnail support. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
3d382ee15e Use extension functions instead of LibSignalNetwork class. 2024-06-05 15:46:00 -04:00
Clark
6069dfc6f8 Add a separate column for tracking thumbnail restore state. 2024-06-05 15:46:00 -04:00
Clark Chen
dee19ed94a Fix attachment table v231 migration consistency. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
905b0681f5 Update otpk/kpk tests. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
b6a4e1f145 Rewrite the AppDependencies system. 2024-06-05 15:46:00 -04:00
Alex Hart
a0131bf39b Fix db inconsistency. 2024-06-05 15:46:00 -04:00
Alex Hart
7ed77a00df Remove unused method from RecipientTable. 2024-06-05 15:46:00 -04:00
Alex Hart
887c173d8f Move camera flip and improve movement of some ui elements. 2024-06-05 15:46:00 -04:00
Cody Henthorne
6362da7a50 Refactor group state processing. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
1296365bed Upgrade to libsignal 0.47.0 2024-06-05 15:46:00 -04:00
Alex Hart
99ae7c5961 Add search view extension for incognito keyboards. 2024-06-05 15:46:00 -04:00
Clark
5c3ea712fe Add streaming video support for attachment files. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
bc5cb454bf Ship "instant" video playback. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
8a7c2c1e20 Rotate libsignal CDS feature flag. 2024-06-05 15:46:00 -04:00
Cody Henthorne
a81a675d59 Add Delete for Me sync support. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
1c66da7873 Update slow notification debugging info. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
afe3cd1098 Additional error handling for registration v2. 2024-06-05 15:46:00 -04:00
Alex Hart
4f3ee9ca1d Skip the contact links migration if contact permissions are disabled. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
7771aaa501 Sort the language list during build for determinism.
Addresses #13565.
2024-06-05 15:46:00 -04:00
Greyson Parrelli
5ad38c7960 Ensure archive data is copied when deduping. 2024-06-05 15:46:00 -04:00
Alex Hart
0fb1514da2 Consolidate subscription information and manage button to a single row. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
f37efd7e15 Add additional error handling for registration v2. 2024-06-05 15:46:00 -04:00
mtang-signal
1ae2464df1 Update remaining gallery permission UI. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
0425b70d31 Do not show unregistered contacts in search results. 2024-06-05 15:46:00 -04:00
Clark
7b0d3f36dc Ignore digest for downloading archived thumbnails. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
14b917dc7e Fix typo in query resulting in contacts not being unlinked. 2024-06-05 15:46:00 -04:00
Ehren Kret
6184cc0307 Migrate existing raw contacts to add video call links. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
870aa8e7b0 Fix country code sorting in reg v2. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
d88016669b Shorten groupId string patterns. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
a464b413d9 Use correct label in log. 2024-06-05 15:46:00 -04:00
Alex Hart
d719edf104 Rewrite in-app-payment flows to prepare for backups support. 2024-06-05 15:46:00 -04:00
mtang-signal
b36b00a11c Update camera permission UI for voice calls. 2024-06-05 15:46:00 -04:00
mtang-signal
a99db2b16e Update camera permission UI for usernames. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
2744dec43a Switch to using dateSent for jump-to-calendar.
We use dateSent for date dividers, but were using dateReceived for
calendar date availability, which would occasionally result in a
mismatch. Switched to use the same thing we use for date dividers.
2024-06-05 15:45:59 -04:00
Greyson Parrelli
6f2cce1494 Add acknowledgements from libsigna/ringrtc. 2024-06-05 15:45:59 -04:00
Greyson Parrelli
689ee243aa Fix potential sqlite conflict in dlist on recipient remap. 2024-06-05 15:45:59 -04:00
Cody Henthorne
537fc0ef5c Update to Kotlin 1.9.20, AGP 8.4.0, and Gradle 8.6 2024-06-05 15:45:59 -04:00
Clark Chen
e647b31f29 Explicitly persist message backup tier. 2024-05-17 10:39:09 -04:00
Alex Hart
b59932cd88 Fix compilation error in contacts test app. 2024-05-17 09:33:22 -04:00
Nicholas Tinsley
cfb4377de3 Apply automated ktlint 1.2.1 formatting. 2024-05-17 09:33:22 -04:00
Nicholas Tinsley
e861c022da Disable new ktlint rules with preexisting violations. 2024-05-17 09:33:22 -04:00
Nicholas Tinsley
59006d3182 Upgrade ktlint to 1.2.1. 2024-05-17 09:33:22 -04:00
Nicholas Tinsley
503faea3a9 Support voice verification in registration v2. 2024-05-17 09:33:22 -04:00
1451 changed files with 61733 additions and 30652 deletions

View File

@@ -6,4 +6,15 @@ ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_allow_trailing_comma = false
ktlint_code_style = intellij_idea
twitter_compose_allowed_composition_locals=LocalExtendedColors
ktlint_standard_class-naming = disabled
ktlint_standard_class-naming = disabled
# below rules disabled during ktlint version migration because they were preexisting but should be corrected and re-enabled ASAP
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_property-naming = disabled
ktlint_standard_enum-wrapping = disabled
ktlint_standard_multiline-if-else = disabled
ktlint_standard_backing-property-naming = disabled
ktlint_standard_statement-wrapping = disabled
internal:ktlint-suppression = disabled
ktlint_standard_unnecessary-parentheses-before-trailing-lambda = disabled
ktlint_standard_value-parameter-comment = disabled

View File

@@ -21,17 +21,10 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1421
val canonicalVersionName = "7.8.1"
val postFixSize = 100
val abiPostFix: Map<String, Int> = mapOf(
"universal" to 0,
"armeabi-v7a" to 1,
"arm64-v8a" to 2,
"x86" to 3,
"x86_64" to 4
)
val canonicalVersionCode = 1432
val canonicalVersionName = "7.10.3"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
val keystores: Map<String, Properties?> = mapOf("debug" to loadKeystoreProperties("keystore.debug.properties"))
@@ -79,7 +72,7 @@ wire {
}
ktlint {
version.set("0.49.1")
version.set("1.2.1")
}
android {
@@ -92,6 +85,8 @@ android {
useLibrary("org.apache.http.legacy")
testBuildType = "instrumentation"
android.bundle.language.enableSplit = false
kotlinOptions {
jvmTarget = signalKotlinJvmTarget
}
@@ -152,11 +147,11 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.4"
kotlinCompilerExtensionVersion = "1.5.4"
}
defaultConfig {
versionCode = canonicalVersionCode * postFixSize
versionCode = (canonicalVersionCode * maxHotfixVersions) + currentHotfixVersion
versionName = canonicalVersionName
minSdk = signalMinSdkVersion
@@ -208,6 +203,7 @@ android {
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\"")
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"")
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.PRODUCTION")
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.INFO")
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"")
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"")
@@ -386,6 +382,7 @@ android {
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"")
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"")
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.STAGING")
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.DEBUG")
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
@@ -405,7 +402,6 @@ android {
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
if (output.baseName.contains("nightly")) {
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
var tag = getCurrentGitTag()
if (!tag.isNullOrEmpty()) {
if (tag.startsWith("v")) {
@@ -419,14 +415,9 @@ android {
} else {
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
val abiName: String = output.getFilter("ABI") ?: "universal"
val postFix: Int = abiPostFix[abiName]!!
if (postFix >= postFixSize) {
throw AssertionError("postFix is too large")
if (currentHotfixVersion >= maxHotfixVersions) {
throw AssertionError("Hotfix version is too large!")
}
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
}
@@ -509,6 +500,7 @@ dependencies {
implementation(libs.androidx.profileinstaller)
implementation(libs.androidx.asynclayoutinflater)
implementation(libs.androidx.asynclayoutinflater.appcompat)
implementation(libs.androidx.emoji2)
implementation(libs.firebase.messaging) {
exclude(group = "com.google.firebase", module = "firebase-core")
exclude(group = "com.google.firebase", module = "firebase-analytics")
@@ -540,6 +532,7 @@ dependencies {
}
implementation(libs.stream)
implementation(libs.lottie)
implementation(libs.lottie.compose)
implementation(libs.signal.android.database.sqlcipher)
implementation(libs.androidx.sqlite)
implementation(libs.google.ez.vcard) {
@@ -551,6 +544,7 @@ dependencies {
implementation(libs.accompanist.permissions)
implementation(libs.kotlin.stdlib.jdk8)
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.play.services)
implementation(libs.jackson.module.kotlin)
implementation(libs.rxjava3.rxandroid)
implementation(libs.rxjava3.rxkotlin)
@@ -712,7 +706,8 @@ fun Project.languageList(): List<String> {
.map { valuesFolderName -> valuesFolderName.replace("values-", "") }
.filter { valuesFolderName -> valuesFolderName != "values" }
.map { languageCode -> languageCode.replace("-r", "_") }
.distinct() + "en"
.distinct()
.sorted() + "en"
}
fun String.capitalize(): String {

View File

@@ -16,6 +16,10 @@
-keep class androidx.window.** { *; }
-keepclassmembers class * extends androidx.constraintlayout.motion.widget.Key {
public <init>();
}
# AGP generated dont warns
-dontwarn com.android.org.conscrypt.SSLParametersImpl
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl

View File

@@ -5,7 +5,7 @@ import org.signal.core.util.logging.AndroidLogger
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
@@ -21,8 +21,8 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
override fun initializeAppDependencies() {
val default = ApplicationDependencyProvider(this)
ApplicationDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
ApplicationDependencies.getDeadlockDetector().start()
AppDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
AppDependencies.deadlockDetector.start()
}
override fun initializeLogging() {

View File

@@ -10,6 +10,7 @@ import android.database.Cursor
import androidx.core.content.contentValuesOf
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.signal.core.util.Hex
import org.signal.core.util.SqlUtil
@@ -24,27 +25,30 @@ import org.signal.core.util.toInt
import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.EmojiSearchTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.subscription.Subscriber
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.io.ByteArrayInputStream
import java.util.Currency
import java.util.UUID
import kotlin.random.Random
@@ -79,18 +83,20 @@ class BackupTest {
@Before
fun setup() {
SignalStore.account().setE164(SELF_E164)
SignalStore.account().setAci(SELF_ACI)
SignalStore.account().setPni(SELF_PNI)
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
SignalStore.account.setE164(SELF_E164)
SignalStore.account.setAci(SELF_ACI)
SignalStore.account.setPni(SELF_PNI)
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
}
@Ignore("Will likely be removed soon")
@Test
fun emptyDatabase() {
backupTest { }
}
@Ignore("Will likely be removed soon")
@Test
fun noteToSelf() {
backupTest {
@@ -102,6 +108,7 @@ class BackupTest {
}
}
@Ignore("Will likely be removed soon")
@Test
fun individualChat() {
backupTest {
@@ -118,6 +125,7 @@ class BackupTest {
}
}
@Ignore("Will likely be removed soon")
@Test
fun individualRecipients() {
backupTest {
@@ -149,6 +157,7 @@ class BackupTest {
}
}
@Ignore("Will likely be removed soon")
@Test
fun individualCallLogs() {
backupTest {
@@ -231,9 +240,10 @@ class BackupTest {
}
}
@Ignore("Will likely be removed soon")
@Test
fun accountData() {
val context = ApplicationDependencies.getApplication()
val context = AppDependencies.application
backupTest(validateKeyValue = true) {
val self = Recipient.self()
@@ -241,35 +251,34 @@ class BackupTest {
// TODO note-to-self archived
// TODO note-to-self unread
SignalStore.account().setAci(SELF_ACI)
SignalStore.account().setPni(SELF_PNI)
SignalStore.account().setE164(SELF_E164)
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
SignalStore.account.setAci(SELF_ACI)
SignalStore.account.setPni(SELF_PNI)
SignalStore.account.setE164(SELF_E164)
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
SignalDatabase.recipients.setProfileKey(self.id, ProfileKey(Random.nextBytes(32)))
SignalDatabase.recipients.setProfileName(self.id, ProfileName.fromParts("Peter", "Parker"))
SignalDatabase.recipients.setProfileAvatar(self.id, "https://example.com/")
SignalStore.donationsValues().markUserManuallyCancelled()
SignalStore.donationsValues().setSubscriber(Subscriber(SubscriberId.generate(), "USD"))
SignalStore.donationsValues().setDisplayBadgesOnProfile(false)
InAppPaymentsRepository.setSubscriber(InAppPaymentSubscriberRecord(SubscriberId.generate(), Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION, false, InAppPaymentData.PaymentMethodType.UNKNOWN))
SignalStore.donations.setDisplayBadgesOnProfile(false)
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
SignalStore.settings().isLinkPreviewsEnabled = false
SignalStore.settings().isPreferSystemContactPhotos = true
SignalStore.settings().universalExpireTimer = 42
SignalStore.settings().setKeepMutedChatsArchived(true)
SignalStore.settings.isLinkPreviewsEnabled = false
SignalStore.settings.isPreferSystemContactPhotos = true
SignalStore.settings.universalExpireTimer = 42
SignalStore.settings.setKeepMutedChatsArchived(true)
SignalStore.storyValues().viewedReceiptsEnabled = false
SignalStore.storyValues().userHasViewedOnboardingStory = true
SignalStore.storyValues().isFeatureDisabled = false
SignalStore.storyValues().userHasBeenNotifiedAboutStories = true
SignalStore.storyValues().userHasSeenGroupStoryEducationSheet = true
SignalStore.story.viewedReceiptsEnabled = false
SignalStore.story.userHasViewedOnboardingStory = true
SignalStore.story.isFeatureDisabled = false
SignalStore.story.userHasBeenNotifiedAboutStories = true
SignalStore.story.userHasSeenGroupStoryEducationSheet = true
SignalStore.emojiValues().reactions = listOf("a", "b", "c")
SignalStore.emoji.reactions = listOf("a", "b", "c")
TextSecurePreferences.setTypingIndicatorsEnabled(context, false)
TextSecurePreferences.setReadReceiptsEnabled(context, false)

View File

@@ -5,11 +5,8 @@
package org.thoughtcrime.securesms.backup.v2
import android.Manifest
import android.app.UiAutomation
import android.os.Environment
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import io.mockk.InternalPlatformDsl.toArray
import okio.ByteString.Companion.toByteString
import org.junit.Assert
import org.junit.Before
@@ -17,6 +14,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
import org.signal.core.util.Base64
import org.signal.core.util.test.getObjectDiff
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.messagebackup.MessageBackupKey
import org.signal.libsignal.zkgroup.profiles.ProfileKey
@@ -29,9 +27,11 @@ import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.Contact
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList
import org.thoughtcrime.securesms.backup.v2.proto.DistributionListItem
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge
import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
@@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
@@ -79,7 +80,17 @@ class ImportExportTest {
val defaultBackupInfo = BackupInfo(version = 1L, backupTimeMs = 123456L)
val selfRecipient = Recipient(id = 1, self = Self())
val releaseNotes = Recipient(id = 2, releaseNotes = ReleaseNotes())
val myStory = Recipient(
id = 2,
distributionList = DistributionListItem(
distributionId = DistributionId.MY_STORY.asUuid().toByteArray().toByteString(),
distributionList = DistributionList(
name = DistributionId.MY_STORY.toString(),
privacyMode = DistributionList.PrivacyMode.ALL
)
)
)
val releaseNotes = Recipient(id = 3, releaseNotes = ReleaseNotes())
val standardAccountData = AccountData(
profileKey = SELF_PROFILE_KEY.serialize().toByteString(),
username = "self.01",
@@ -87,9 +98,11 @@ class ImportExportTest {
givenName = "Peter",
familyName = "Parker",
avatarUrlPath = "https://example.com/",
subscriberId = SubscriberId.generate().bytes.toByteString(),
subscriberCurrencyCode = "USD",
subscriptionManuallyCancelled = true,
donationSubscriberData = AccountData.SubscriberData(
subscriberId = SubscriberId.generate().bytes.toByteString(),
currencyCode = "USD",
manuallyCancelled = true
),
accountSettings = AccountData.AccountSettings(
readReceipts = true,
sealedSenderIndicators = true,
@@ -111,16 +124,15 @@ class ImportExportTest {
)
)
val alice = Recipient(
id = 3,
id = 4,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
blocked = false,
hidden = false,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
visibility = Contact.Visibility.VISIBLE,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
@@ -130,23 +142,26 @@ class ImportExportTest {
)
/**
* When using standardFrames you must start recipient ids at 3.
* When using standardFrames you must start recipient ids at 4.
*/
private val standardFrames = arrayOf(defaultBackupInfo, standardAccountData, selfRecipient, releaseNotes)
private val standardFrames = arrayOf(defaultBackupInfo, standardAccountData, selfRecipient, myStory, releaseNotes)
}
private val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext
@JvmField
@Rule
var testName = TestName()
@Before
fun setup() {
SignalStore.svr().setMasterKey(MasterKey(MASTER_KEY), "1234")
SignalStore.account().setE164(SELF_E164)
SignalStore.account().setAci(SELF_ACI)
SignalStore.account().setPni(SELF_PNI)
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234")
SignalStore.account.setE164(SELF_E164)
SignalStore.account.setAci(SELF_ACI)
SignalStore.account.setPni(SELF_PNI)
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
}
@Test
@@ -158,7 +173,7 @@ class ImportExportTest {
fun largeNumberOfRecipientsAndChats() {
val recipients = ArrayList<Recipient>(5000)
val chats = ArrayList<Chat>(5000)
var id = 3L
var id = 4L
for (i in 0..5000) {
val recipientId = id++
recipients.add(
@@ -170,9 +185,8 @@ class ImportExportTest {
username = "rec$i.01",
e164 = 14125550000 + i,
blocked = false,
hidden = false,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
visibility = Contact.Visibility.VISIBLE,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Test",
@@ -216,7 +230,7 @@ class ImportExportTest {
@Test
fun largeNumberOfMessagesAndChats() {
val NUM_INDIVIDUAL_RECIPIENTS = 1000
val numIndividualRecipients = 1000
val numIndividualMessages = 500
val numGroupMessagesPerPerson = 200
@@ -225,7 +239,7 @@ class ImportExportTest {
val recipients = ArrayList<Recipient>(1010)
val chats = ArrayList<Chat>(1010)
var id = 3L
for (i in 0 until NUM_INDIVIDUAL_RECIPIENTS) {
for (i in 0 until numIndividualRecipients) {
val recipientId = id++
recipients.add(
Recipient(
@@ -236,9 +250,8 @@ class ImportExportTest {
username = if (random.trueWithProbability(0.2f)) "rec$i.01" else null,
e164 = 14125550000 + i,
blocked = random.trueWithProbability(0.1f),
hidden = random.trueWithProbability(0.1f),
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
visibility = if (random.trueWithProbability(0.1f)) Contact.Visibility.HIDDEN else Contact.Visibility.VISIBLE,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = random.trueWithProbability(0.9f),
profileGivenName = "Test",
@@ -369,12 +382,12 @@ class ImportExportTest {
}
}
}
val import = exportFrames(
exportFrames(
*standardFrames,
*recipients.toArray(),
*chatItems.toArray()
)
outputFile(import)
}
@Test
@@ -382,16 +395,15 @@ class ImportExportTest {
importExport(
*standardFrames,
Recipient(
id = 3,
id = 4,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
visibility = Contact.Visibility.VISIBLE,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
@@ -400,16 +412,15 @@ class ImportExportTest {
)
),
Recipient(
id = 4,
id = 5,
contact = Contact(
aci = null,
pni = null,
username = null,
e164 = 141255501235,
blocked = true,
hidden = true,
registered = Contact.Registered.NOT_REGISTERED,
unregisteredTimestamp = 1234568927398L,
visibility = Contact.Visibility.HIDDEN,
notRegistered = Contact.NotRegistered(unregisteredTimestamp = 1234568927398L),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = false,
profileGivenName = "Peter",
@@ -425,7 +436,7 @@ class ImportExportTest {
importExport(
*standardFrames,
Recipient(
id = 3,
id = 4,
group = Group(
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = true,
@@ -440,7 +451,7 @@ class ImportExportTest {
)
),
Recipient(
id = 4,
id = 5,
group = Group(
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = false,
@@ -462,16 +473,15 @@ class ImportExportTest {
importExport(
*standardFrames,
Recipient(
id = 3,
id = 4,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
visibility = Contact.Visibility.HIDDEN,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
@@ -479,35 +489,33 @@ class ImportExportTest {
hideStory = true
)
),
Recipient(
id = 4,
contact = Contact(
aci = null,
pni = null,
username = null,
e164 = 141255501235,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Peter",
profileFamilyName = "Kim",
hideStory = true
)
),
Recipient(
id = 5,
contact = Contact(
aci = null,
pni = null,
username = null,
e164 = 141255501235,
blocked = true,
visibility = Contact.Visibility.HIDDEN,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Peter",
profileFamilyName = "Kim",
hideStory = true
)
),
Recipient(
id = 6,
contact = Contact(
aci = null,
pni = null,
username = null,
e164 = 141255501236,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
visibility = Contact.Visibility.HIDDEN,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Father",
@@ -516,14 +524,15 @@ class ImportExportTest {
)
),
Recipient(
id = 6,
distributionList = DistributionList(
name = "Kim Family",
id = 7,
distributionList = DistributionListItem(
distributionId = DistributionId.create().asUuid().toByteArray().toByteString(),
allowReplies = true,
deletionTimestamp = 0L,
privacyMode = DistributionList.PrivacyMode.ONLY_WITH,
memberRecipientIds = listOf(3, 4, 5)
distributionList = DistributionList(
name = "Kim Family",
allowReplies = true,
privacyMode = DistributionList.PrivacyMode.ONLY_WITH,
memberRecipientIds = listOf(3, 4, 5)
)
)
)
)
@@ -532,16 +541,15 @@ class ImportExportTest {
@Test
fun deletedDistributionList() {
val alexa = Recipient(
id = 3,
id = 4,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
visibility = Contact.Visibility.HIDDEN,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
@@ -554,23 +562,19 @@ class ImportExportTest {
alexa,
Recipient(
id = 6,
distributionList = DistributionList(
name = "Deleted list",
distributionList = DistributionListItem(
distributionId = DistributionId.create().asUuid().toByteArray().toByteString(),
allowReplies = true,
deletionTimestamp = 12345L,
privacyMode = DistributionList.PrivacyMode.ONLY_WITH,
memberRecipientIds = listOf(3)
deletionTimestamp = 12345L
)
)
)
import(importData)
val exported = export()
val exported = BackupRepository.export()
val expected = exportFrames(
*standardFrames,
alexa
)
outputFile(importData, expected)
compare(expected, exported)
}
@@ -579,16 +583,15 @@ class ImportExportTest {
importExport(
*standardFrames,
Recipient(
id = 3,
id = 4,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
blocked = false,
hidden = false,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
visibility = Contact.Visibility.VISIBLE,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
@@ -597,7 +600,7 @@ class ImportExportTest {
)
),
Recipient(
id = 4,
id = 5,
group = Group(
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = true,
@@ -607,14 +610,13 @@ class ImportExportTest {
),
Chat(
id = 1,
recipientId = 3,
recipientId = 4,
archived = true,
pinnedOrder = 1,
expirationTimerMs = 1.days.inWholeMilliseconds,
muteUntilMs = System.currentTimeMillis(),
markedUnread = true,
dontNotifyForMentionsIfMuted = true,
wallpaper = null
dontNotifyForMentionsIfMuted = true
)
)
}
@@ -681,16 +683,15 @@ class ImportExportTest {
importExport(
*standardFrames,
Recipient(
id = 3,
id = 4,
contact = Contact(
aci = startedAci,
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
blocked = false,
hidden = false,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
visibility = Contact.Visibility.VISIBLE,
registered = Contact.Registered(),
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
@@ -699,7 +700,7 @@ class ImportExportTest {
)
),
Recipient(
id = 4,
id = 5,
group = Group(
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = true,
@@ -709,14 +710,13 @@ class ImportExportTest {
),
Chat(
id = 1,
recipientId = 3,
recipientId = 4,
archived = true,
pinnedOrder = 1,
expirationTimerMs = 1.days.inWholeMilliseconds,
muteUntilMs = System.currentTimeMillis(),
markedUnread = true,
dontNotifyForMentionsIfMuted = true,
wallpaper = null
dontNotifyForMentionsIfMuted = true
),
*individualCalls.toArray()
)
@@ -994,14 +994,13 @@ class ImportExportTest {
expirationNotStarted
)
import(importData)
val exported = export()
val exported = BackupRepository.export()
val expected = exportFrames(
*standardFrames,
alice,
chat,
expirationNotStarted
)
outputFile(importData, expected)
compare(expected, exported)
}
@@ -1051,23 +1050,6 @@ class ImportExportTest {
incrementalMacChunkSize = 0
),
wasDownloaded = false
),
MessageAttachment(
pointer = FilePointer(
backupLocator = FilePointer.BackupLocator(
"digestherebutimlazy",
cdnNumber = 3,
key = (1..32).map { it.toByte() }.toByteArray().toByteString(),
digest = (1..64).map { it.toByte() }.toByteArray().toByteString(),
size = 12345
),
contentType = "image/png",
width = 100,
height = 200,
caption = "Love this cool picture! Too bad u cant download it",
incrementalMacChunkSize = 0
),
wasDownloaded = true
)
)
)
@@ -1256,6 +1238,76 @@ class ImportExportTest {
)
}
@Test
fun giftBadgeMessage() {
var dateSentStart = 100L
importExport(
*standardFrames,
alice,
buildChat(alice, 1),
ChatItem(
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
giftBadge = GiftBadge(
receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(),
state = GiftBadge.State.OPENED
)
),
ChatItem(
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
giftBadge = GiftBadge(
receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(),
state = GiftBadge.State.FAILED
)
),
ChatItem(
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
giftBadge = GiftBadge(
receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(),
state = GiftBadge.State.REDEEMED
)
),
ChatItem(
chatId = 1,
authorId = alice.id,
dateSent = dateSentStart++,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = dateSentStart,
dateServerSent = dateSentStart,
read = true,
sealedSender = true
),
giftBadge = GiftBadge(
receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(),
state = GiftBadge.State.UNOPENED
)
)
)
}
fun enumerateIncomingMessageDetails(dateSent: Long): List<ChatItem.IncomingMessageDetails> {
val details = mutableListOf<ChatItem.IncomingMessageDetails>()
details.add(
@@ -1361,8 +1413,7 @@ class ImportExportTest {
expirationTimerMs = 0,
muteUntilMs = 0,
markedUnread = false,
dontNotifyForMentionsIfMuted = false,
wallpaper = null
dontNotifyForMentionsIfMuted = false
)
}
@@ -1373,8 +1424,8 @@ class ImportExportTest {
private fun exportFrames(vararg objects: Any): ByteArray {
val outputStream = ByteArrayOutputStream()
val writer = EncryptedBackupWriter(
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
aci = SignalStore.account().aci!!,
key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
aci = SignalStore.account.aci!!,
outputStream = outputStream,
append = { mac -> outputStream.write(mac) }
)
@@ -1396,46 +1447,31 @@ class ImportExportTest {
return outputStream.toByteArray()
}
/**
* Exports the passed in frames as a backup and then attempts to
* import them.
*/
private fun import(vararg objects: Any) {
val importData = exportFrames(*objects)
import(importData)
}
private fun import(importData: ByteArray) {
BackupRepository.import(length = importData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(importData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
}
/**
* Export our current database as a backup.
*/
private fun export(): ByteArray {
val exportData = BackupRepository.export()
return exportData
}
private fun validate(importData: ByteArray): MessageBackup.ValidationResult {
val factory = { ByteArrayInputStream(importData) }
val masterKey = SignalStore.svr().getOrCreateMasterKey()
val masterKey = SignalStore.svr.getOrCreateMasterKey()
val key = MessageBackupKey(masterKey.serialize(), org.signal.libsignal.protocol.ServiceId.Aci.parseFromBinary(SELF_ACI.toByteArray()))
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, factory, importData.size.toLong())
}
/**
* Imports the passed in frames and then exports them.
* Given some [Frame]s, this will do the following:
*
* It will do a comparison to assert that the import and export
* are equal.
* 1. Write the frames using an [EncryptedBackupWriter] and keep the result in memory (A).
* 2. Import those frames back into the local database.
* 3. Export the state of the local database and keep the result in memory (B).
* 4. Assert that (A) and (B) are identical. Or, in other words, assert that importing and exporting again results in the original backup data.
*/
private fun importExport(vararg objects: Any) {
val outputStream = ByteArrayOutputStream()
val writer = EncryptedBackupWriter(
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
aci = SignalStore.account().aci!!,
key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
aci = SignalStore.account.aci!!,
outputStream = outputStream,
append = { mac -> outputStream.write(mac) }
)
@@ -1454,12 +1490,13 @@ class ImportExportTest {
}
}
}
val importData = outputStream.toByteArray()
outputFile(importData)
BackupRepository.import(length = importData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(importData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
val export = export()
compare(importData, export)
val originalBackupData = outputStream.toByteArray()
BackupRepository.import(length = originalBackupData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(originalBackupData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
val generatedBackupData = BackupRepository.export()
compare(originalBackupData, generatedBackupData)
}
private fun compare(import: ByteArray, export: ByteArray) {
@@ -1498,7 +1535,14 @@ class ImportExportTest {
for (f in framesExported) {
when {
f.account != null -> accountImported.add(f.account!!)
f.recipient != null -> recipientsExported.add(f.recipient!!)
f.recipient != null -> {
val frameRecipient = f.recipient!!
if (frameRecipient.distributionList != null && frameRecipient.distributionList!!.distributionId == DistributionId.MY_STORY.asUuid().toByteArray().toByteString()) {
recipientsExported.add(frameRecipient.copy(distributionList = frameRecipient.distributionList!!.copyWithoutMembers()))
} else {
recipientsExported.add(f.recipient!!)
}
}
f.chat != null -> chatsExported.add(f.chat!!)
f.chatItem != null -> chatItemsExported.add(f.chatItem!!)
f.adHocCall != null -> callsExported.add(f.adHocCall!!)
@@ -1513,11 +1557,19 @@ class ImportExportTest {
prettyAssertEquals(stickersImported, stickersExported) { it.packId }
}
private fun <T> prettyAssertEquals(import: List<T>, export: List<T>) {
private fun DistributionListItem.copyWithoutMembers(): DistributionListItem {
return this.copy(
distributionList = this.distributionList?.copy(
memberRecipientIds = emptyList()
)
)
}
private inline fun <reified T : Any> prettyAssertEquals(import: List<T>, export: List<T>) {
Assert.assertEquals(import.size, export.size)
import.zip(export).forEach { (a1, a2) ->
if (a1 != a2) {
Assert.fail("Items do not match: \n $a1 \n $a2")
Assert.fail("Items do not match:\n\n-- Pretty diff\n${getObjectDiff(a1, a2)}\n-- Full objects\n$a1\n$a2")
}
}
}
@@ -1526,19 +1578,31 @@ class ImportExportTest {
return nextFloat() < prob
}
private fun <T, R : Comparable<R>> prettyAssertEquals(import: List<T>, export: List<T>, selector: (T) -> R?) {
private inline fun <reified T : Any, R : Comparable<R>> prettyAssertEquals(import: List<T>, export: List<T>, crossinline selector: (T) -> R?) {
if (import.size != export.size) {
var msg = StringBuilder()
val msg = StringBuilder()
msg.append("There's a different number of items in the lists!\n\n")
msg.append("Imported:\n")
for (i in import) {
msg.append(i)
msg.append("\n")
}
if (import.isEmpty()) {
msg.append("<None>")
}
msg.append("\n")
msg.append("Exported:\n")
for (i in export) {
msg.append(i)
msg.append("\n")
}
if (export.isEmpty()) {
msg.append("<None>")
}
Assert.fail(msg.toString())
}
Assert.assertEquals(import.size, export.size)
val sortedImport = import.sortedBy(selector)
val sortedExport = export.sortedBy(selector)
@@ -1549,9 +1613,9 @@ class ImportExportTest {
private fun readAllFrames(import: ByteArray, selfData: BackupRepository.SelfData): List<Frame> {
val inputFactory = { ByteArrayInputStream(import) }
val frameReader = EncryptedBackupReader(
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
aci = selfData.aci,
streamLength = import.size.toLong(),
length = import.size.toLong(),
dataStream = inputFactory
)
val frames = ArrayList<Frame>()
@@ -1562,9 +1626,8 @@ class ImportExportTest {
return frames
}
private fun outputFile(importBytes: ByteArray, resultBytes: ByteArray? = null) {
grantPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
val dir = File(Environment.getExternalStorageDirectory(), "backup-tests")
private fun writeToOutputFile(importBytes: ByteArray, resultBytes: ByteArray? = null) {
val dir = File(context.filesDir, "backup-tests")
if (dir.mkdirs() || dir.exists()) {
FileOutputStream(File(dir, testName.methodName + ".import")).use {
it.write(importBytes)
@@ -1579,11 +1642,4 @@ class ImportExportTest {
}
}
}
private fun grantPermissions(vararg permissions: String?) {
val auto: UiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
for (perm in permissions) {
auto.grantRuntimePermissionAsUser(InstrumentationRegistry.getInstrumentation().targetContext.packageName, perm, android.os.Process.myUserHandle())
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.signal.core.util.Base64
import org.signal.core.util.StreamUtil
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.ByteArrayInputStream
import java.util.UUID
import kotlin.random.Random
@RunWith(Parameterized::class)
class ImportExportTestSuite(private val path: String) {
companion object {
val SELF_ACI = ServiceId.ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
val SELF_PNI = ServiceId.PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
const val SELF_E164 = "+10000000000"
val SELF_PROFILE_KEY = ProfileKey(Random.nextBytes(32))
val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do")
const val TESTS_FOLDER = "backupTests"
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun data(): Collection<Array<String>> {
val testFiles = InstrumentationRegistry.getInstrumentation().context.resources.assets.list(TESTS_FOLDER)
return testFiles?.map { arrayOf(it) }!!.toList()
}
}
@Before
fun setup() {
SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234")
SignalStore.account.setE164(SELF_E164)
SignalStore.account.setAci(SELF_ACI)
SignalStore.account.setPni(SELF_PNI)
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
}
@Test
fun testBinProto() {
val binProtoBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("${TESTS_FOLDER}/$path").use {
StreamUtil.readFully(it)
}
import(binProtoBytes)
val generatedBackupData = BackupRepository.export()
compare(binProtoBytes, generatedBackupData)
}
private fun import(importData: ByteArray) {
BackupRepository.import(
length = importData.size.toLong(),
inputStreamFactory = { ByteArrayInputStream(importData) },
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY),
plaintext = true
)
}
// TODO compare with libsignal's library
private fun compare(import: ByteArray, export: ByteArray) {
}
}

View File

@@ -1,393 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.MockProvider
import org.thoughtcrime.securesms.testing.Post
import org.thoughtcrime.securesms.testing.Put
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsNull
import org.thoughtcrime.securesms.testing.assertIsSize
import org.thoughtcrime.securesms.testing.connectionFailure
import org.thoughtcrime.securesms.testing.failure
import org.thoughtcrime.securesms.testing.parsedRequestBody
import org.thoughtcrime.securesms.testing.success
import org.thoughtcrime.securesms.testing.timeout
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.internal.push.MismatchedDevices
import org.whispersystems.signalservice.internal.push.PreKeyState
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class ChangeNumberViewModelTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var viewModel: ChangeNumberViewModel
@Before
fun setUp() {
ThreadUtil.runOnMainSync {
viewModel = ChangeNumberViewModel(
localNumber = harness.self.requireE164(),
changeNumberRepository = ChangeNumberRepository(),
savedState = SavedStateHandle(),
password = SignalStore.account().servicePassword!!,
verifyAccountRepository = VerifyAccountRepository(harness.application)
)
viewModel.setNewCountry(1)
viewModel.setNewNationalNumber("5555550102")
}
}
@After
fun tearDown() {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@Test
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
/**
* If we encounter a server error, this means the server ack our request and rejected it. In this
* case we know the change *did not* take on the server and can reset to a clean state.
*/
@Test
fun testChangeNumber_givenServerFailedApiCall() {
// GIVEN
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { MockResponse().failure(500) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs true
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
/**
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
* number on the server side. We have to do a whoami call to query the server for our details and then
* respond accordingly.
*
* In this case, the whoami is our old details, so we can know the change *did not* take on the server
* and can reset to a clean state.
*/
@Test
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToServer() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { MockResponse().connectionFailure() },
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().isChangeNumberLocked assertIs false
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
/**
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
* number on the server side. We have to do a whoami call to query the server for our details and then
* respond accordingly.
*
* In this case, the whoami is our new details, so we can know the change *did* take on the server
* and need to keep the app in a locked state. The test then uses the ChangeNumberLockActivity to unlock
* and apply the pending state after confirming the change on the server.
*/
@Test
@FlakyTest
@Ignore("Test sometimes requires manual intervention to continue.")
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
MockResponse().timeout()
},
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, newPni, "+15555550102")) },
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().isChangeNumberLocked assertIs true
SignalStore.misc().pendingChangeNumberMetadata.assertIsNotNull()
// WHEN AGAIN Processing lock
val scenario = harness.launchActivity<ChangeNumberLockActivity>()
scenario.onActivity {}
ThreadUtil.sleep(500)
// THEN AGAIN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
processor.registrationLock() assertIs true
Recipient.self().requirePni() assertIsNot newPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.deviceMessages.isEmpty()) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2)
extraDevices = emptyList()
}
)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Get("/v2/keys/$aci/2") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
} else if (changeNumberRequest.deviceMessages.isEmpty()) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2)
extraDevices = emptyList()
}
)
} else if (changeNumberRequest.deviceMessages.size == 1) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2, 3)
extraDevices = emptyList()
}
)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Get("/v2/keys/$aci/2") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
},
Get("/v2/keys/$aci/3") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 3))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
processor.registrationLock() assertIs true
Recipient.self().requirePni() assertIsNot newPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) {
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore = SignalStore.account().pniPreKeys
Recipient.self().requireE164() assertIs "+15555550102"
Recipient.self().requirePni() assertIs newPni
SignalStore.account().pniRegistrationId assertIs changeNumberRequest.pniRegistrationIds["1"]!!
SignalStore.account().pniIdentityKey.publicKey assertIs changeNumberRequest.pniIdentityKey
pniMetadataStore.activeSignedPreKeyId assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.keyId
val activeSignedPreKey: SignedPreKeyRecord = pniProtocolStore.loadSignedPreKey(pniMetadataStore.activeSignedPreKeyId)
activeSignedPreKey.keyPair.publicKey assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.publicKey
activeSignedPreKey.signature assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.signature
setPreKeysRequest.signedPreKey.publicKey assertIs activeSignedPreKey.keyPair.publicKey
setPreKeysRequest.preKeys assertIsSize 100
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
}

View File

@@ -154,7 +154,8 @@ class ConversationItemPreviewer {
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
System.currentTimeMillis(),
null
)
}
}

View File

@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
@@ -64,7 +64,7 @@ class SafetyNumberChangeDialogPreviewer {
scenario.onActivity { conversationActivity ->
SafetyNumberBottomSheet
.forIdentityRecordsAndDestinations(
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
identityRecords = AppDependencies.protocolStore.aci().identities().getIdentityRecords(othersRecipients).identityRecords,
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
)
.show(conversationActivity.supportFragmentManager)

View File

@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
import android.net.Uri
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestManager
import io.mockk.mockk
@@ -203,8 +204,8 @@ class V2ConversationItemShapeTest {
private val colorizer = Colorizer()
override val lifecycleOwner: LifecycleOwner = mockk(relaxed = true)
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.Standard
override val clickListener: ConversationAdapter.ItemClickListener = FakeConversationItemClickListener
override val selectedItems: Set<MultiselectPart> = emptySet()
override val isMessageRequestAccepted: Boolean = true
@@ -313,7 +314,7 @@ class V2ConversationItemShapeTest {
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
@@ -330,5 +331,6 @@ class V2ConversationItemShapeTest {
override fun onMessageRequestAcceptOptionsClicked() = Unit
override fun onItemDoubleClick(item: MultiselectPart) = Unit
override fun onPaymentTombstoneClicked() = Unit
}
}

View File

@@ -16,6 +16,7 @@ import org.signal.core.util.update
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaName
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.MediaStream
@@ -25,6 +26,8 @@ import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.backup.MediaId
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.File
import java.util.UUID
@@ -47,9 +50,9 @@ class AttachmentTableTest_deduping {
@Before
fun setUp() {
SignalStore.account().setAci(ServiceId.ACI.from(UUID.randomUUID()))
SignalStore.account().setPni(ServiceId.PNI.from(UUID.randomUUID()))
SignalStore.account().setE164("+15558675309")
SignalStore.account.setAci(ServiceId.ACI.from(UUID.randomUUID()))
SignalStore.account.setPni(ServiceId.PNI.from(UUID.randomUUID()))
SignalStore.account.setE164("+15558675309")
SignalDatabase.attachments.deleteAllAttachments()
}
@@ -195,6 +198,8 @@ class AttachmentTableTest_deduping {
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Mimics sending two files at once. Ensures all fields are kept in sync as we compress and upload.
@@ -220,6 +225,7 @@ class AttachmentTableTest_deduping {
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Re-use the upload when uploaded recently
@@ -234,6 +240,7 @@ class AttachmentTableTest_deduping {
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
}
@@ -253,6 +260,7 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id2, true)
assertDoesNotHaveRemoteFields(id2)
assertArchiveFieldsMatch(id1, id2)
}
// This isn't so much "desirable behavior" as it is documenting how things work.
@@ -282,6 +290,7 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// This represents what would happen if you edited a video, sent it, then forwarded it. We should match, skip transform, and skip upload.
@@ -297,6 +306,7 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// This represents what would happen if you edited a video, sent it, then forwarded it, but *edited the forwarded video*. We should not dedupe.
@@ -327,6 +337,7 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// This represents what would happen if you sent an image using high quality, then forwarded it using standard quality.
@@ -343,6 +354,7 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Make sure that files marked as unhashable are all updated together
@@ -457,6 +469,7 @@ class AttachmentTableTest_deduping {
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Making sure things work for quotes of videos, which have trickier transform properties
@@ -470,6 +483,7 @@ class AttachmentTableTest_deduping {
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
}
@@ -648,6 +662,15 @@ class AttachmentTableTest_deduping {
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createPointerAttachment(attachmentId, uploadTimestamp), uploadTimestamp)
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
SignalDatabase.attachments.setArchiveData(
attachmentId = attachmentId,
archiveCdn = Cdn.CDN_3.cdnNumber,
archiveMediaName = attachment.getMediaName().name,
archiveThumbnailMediaId = MediaId(Util.getSecretBytes(15)).encode(),
archiveMediaId = MediaId(Util.getSecretBytes(15)).encode()
)
}
fun delete(attachmentId: AttachmentId) {
@@ -746,6 +769,15 @@ class AttachmentTableTest_deduping {
assertEquals(lhsAttachment.cdn.cdnNumber, rhsAttachment.cdn.cdnNumber)
}
fun assertArchiveFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
assertEquals(lhsAttachment.archiveCdn, rhsAttachment.archiveCdn)
assertEquals(lhsAttachment.archiveMediaName, rhsAttachment.archiveMediaName)
assertEquals(lhsAttachment.archiveMediaId, rhsAttachment.archiveMediaId)
}
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
assertEquals(0, databaseAttachment.uploadTimestamp)
@@ -792,7 +824,8 @@ class AttachmentTableTest_deduping {
uploadTimestamp,
databaseAttachment.caption,
databaseAttachment.stickerLocator,
databaseAttachment.blurHash
databaseAttachment.blurHash,
databaseAttachment.uuid
)
}
}

View File

@@ -56,7 +56,7 @@ class CallTableTest {
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
SignalDatabase.calls.deleteGroupCall(call!!)
SignalDatabase.calls.markCallDeletedFromSyncEvent(call!!)
val deletedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
@@ -69,9 +69,10 @@ class CallTableTest {
@Test
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
val callId = 1L
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
callId,
groupRecipientId,
CallTable.Type.GROUP_CALL,
CallTable.Direction.OUTGOING,
System.currentTimeMillis()
)
@@ -438,11 +439,12 @@ class CallTableTest {
@Test
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
val callId = 1L
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
callId = callId,
recipientId = groupRecipientId,
direction = CallTable.Direction.INCOMING,
timestamp = System.currentTimeMillis()
timestamp = System.currentTimeMillis(),
type = CallTable.Type.GROUP_CALL
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(

View File

@@ -15,7 +15,7 @@ import org.signal.core.util.getIndexes
import org.signal.core.util.readToList
import org.signal.core.util.requireNonNullString
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.testing.SignalActivityRule
/**
@@ -30,7 +30,7 @@ class DatabaseConsistencyTest {
@Test
fun testUpgradeConsistency() {
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
val testHelper = InMemoryTestHelper(ApplicationDependencies.getApplication()).also {
val testHelper = InMemoryTestHelper(AppDependencies.application).also {
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
}

View File

@@ -8,7 +8,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@@ -26,7 +26,7 @@ class DatabaseObserverTest {
@Before
fun setup() {
db = SignalDatabase.instance!!.signalWritableDatabase
observer = ApplicationDependencies.getDatabaseObserver()
observer = AppDependencies.databaseObserver
}
@Test

View File

@@ -25,15 +25,6 @@ class DistributionListTablesTest {
Assert.assertNotNull(id)
}
@Test
fun createList_whenNameConflict_failToInsert() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val id2: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNull(id2)
}
@Test
fun getList_returnCorrectList() {
createRecipients(3)

View File

@@ -167,8 +167,8 @@ class GroupTableTest {
@Test
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
val g1 = insertPushGroup(listOf())
val g2 = insertPushGroup(listOf())
val g1 = insertPushGroup(members = emptyList())
val g2 = insertPushGroup(members = emptyList())
val gr1 = groupTable.getGroup(g1)
val gr2 = groupTable.getGroup(g2)
@@ -195,6 +195,85 @@ class GroupTableTest {
assertEquals(groups[0].id, groupInCommon)
}
@Test
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForTheSharedToken_thenIExpectBothGroups() {
insertPushGroup("Group Alice")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(2, it.cursor?.count)
val firstGroup = it.getNext()
val secondGroup = it.getNext()
assertEquals("Group Alice", firstGroup?.title)
assertEquals("Group Bob", secondGroup?.title)
}
}
@Test
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForAnUnsharedToken_thenIExpectOneGroup() {
insertPushGroup("Group Alice")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Alice",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(1, it.cursor?.count)
val firstGroup = it.getNext()
assertEquals("Group Alice", firstGroup?.title)
}
}
@Test
fun givenAGroupWithThreeTokens_whenISearchForTheFirstAndLastToken_thenIExpectThatGroup() {
insertPushGroup("Group & Alice")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group Alice",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(1, it.cursor?.count)
val firstGroup = it.getNext()
assertEquals("Group & Alice", firstGroup?.title)
}
}
@Test
fun givenTwoGroupsWithSharedTokens_whenISearchForAnExactMatch_thenIExpectThatGroupFirst() {
insertPushGroup("Group Alice Bob")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group Bob",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(2, it.cursor?.count)
val firstGroup = it.getNext()
val second = it.getNext()
assertEquals("Group Bob", firstGroup?.title)
assertEquals("Group Alice Bob", second?.title)
}
}
private fun insertThread(groupId: GroupId): Long {
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
@@ -214,6 +293,7 @@ class GroupTableTest {
}
private fun insertPushGroup(
title: String = "Test Group",
members: List<DecryptedMember> = listOf(
DecryptedMember.Builder()
.aciBytes(harness.self.requireAci().toByteString())
@@ -229,6 +309,7 @@ class GroupTableTest {
): GroupId {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.Builder()
.title(title)
.members(members)
.revision(0)
.build()

View File

@@ -159,7 +159,7 @@ class KyberPreKeyTableTest {
val count = SignalDatabase.rawDatabase
.update(KyberPreKeyTable.TABLE_NAME)
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
assertEquals(1, count)
@@ -169,8 +169,15 @@ class KyberPreKeyTableTest {
return SignalDatabase.rawDatabase
.select(KyberPreKeyTable.STALE_TIMESTAMP)
.from(KyberPreKeyTable.TABLE_NAME)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
}
private fun ServiceId.toAccountId(): String {
return when (this) {
is ACI -> this.toString()
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
}
}
}

View File

@@ -12,12 +12,12 @@ import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.updateAll
import org.thoughtcrime.securesms.crash.CrashConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.testing.assertIs
class LogDatabaseTest {
private val db: LogDatabase = LogDatabase.getInstance(ApplicationDependencies.getApplication())
private val db: LogDatabase = LogDatabase.getInstance(AppDependencies.application)
@Test
fun crashTable_matchesNamePattern() {

View File

@@ -30,8 +30,8 @@ class MessageTableTest_gifts {
mms.deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
}

View File

@@ -40,14 +40,14 @@ class MmsTableTest_stories {
mms.deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelRecipient.id)
SignalStore.releaseChannel.setReleaseChannelRecipientId(releaseChannelRecipient.id)
}
@Test

View File

@@ -120,7 +120,7 @@ class OneTimePreKeyTableTest {
val count = SignalDatabase.rawDatabase
.update(OneTimePreKeyTable.TABLE_NAME)
.values(OneTimePreKeyTable.STALE_TIMESTAMP to staleTime)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
assertEquals(1, count)
@@ -130,8 +130,15 @@ class OneTimePreKeyTableTest {
return SignalDatabase.rawDatabase
.select(OneTimePreKeyTable.STALE_TIMESTAMP)
.from(OneTimePreKeyTable.TABLE_NAME)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
.readToSingleObject { it.requireLongOrNull(OneTimePreKeyTable.STALE_TIMESTAMP) }
}
private fun ServiceId.toAccountId(): String {
return when (this) {
is ACI -> this.toString()
is PNI -> OneTimePreKeyTable.PNI_ACCOUNT_ID
}
}
}

View File

@@ -9,7 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
import org.thoughtcrime.securesms.storage.StorageSyncModels
@@ -28,7 +28,7 @@ class RecipientTableTest_applyStorageSyncContactUpdate {
@Test
fun insertMessageOnVerifiedToDefault() {
// GIVEN
val identities = ApplicationDependencies.getProtocolStore().aci().identities()
val identities = AppDependencies.protocolStore.aci().identities()
val other = Recipient.resolved(harness.others[0])
MmsHelper.insert(recipient = other)

View File

@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMessage
@@ -53,9 +53,9 @@ class RecipientTableTest_getAndPossiblyMerge {
@Before
fun setup() {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
SignalStore.account.setE164(E164_SELF)
SignalStore.account.setAci(ACI_SELF)
SignalStore.account.setPni(PNI_SELF)
}
@Test
@@ -1113,8 +1113,8 @@ class RecipientTableTest_getAndPossiblyMerge {
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
}
ApplicationDependencies.getRecipientCache().clear()
ApplicationDependencies.getRecipientCache().clearSelf()
AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.clearSelf()
RecipientId.clearCache()
}

View File

@@ -10,7 +10,9 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Hex
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
@@ -44,8 +46,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
recipients = SignalDatabase.recipients
sms = SignalDatabase.messages
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
bob = recipients.getOrInsertFromServiceId(bobServiceId)
@@ -286,11 +288,18 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingMessage {
wallClock++
val updateDescription = GV2UpdateDescription(
gv2ChangeDescription = groupContext,
groupChangeUpdate = GroupsV2UpdateMessageConverter.translateDecryptedChangeUpdate(SignalStore.account.getServiceIds(), groupContext)
)
return IncomingMessage.groupUpdate(
from = sender,
timestamp = wallClock,
groupId = groupId,
groupContext = groupContext,
update = updateDescription,
isGroupAdd = false,
serverGuid = null
)
}

View File

@@ -5,6 +5,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.stickers.StickerLocator
import java.util.UUID
object UriAttachmentBuilder {
fun build(
@@ -22,23 +23,28 @@ object UriAttachmentBuilder {
stickerLocator: StickerLocator? = null,
blurHash: BlurHash? = null,
audioHash: AudioHash? = null,
transformProperties: AttachmentTable.TransformProperties? = null
transformProperties: AttachmentTable.TransformProperties? = null,
uuid: UUID? = UUID.randomUUID()
): UriAttachment {
return UriAttachment(
uri,
contentType,
transferState,
size,
fileName,
voiceNote,
borderless,
videoGif,
quote,
caption,
stickerLocator,
blurHash,
audioHash,
transformProperties
dataUri = uri,
contentType = contentType,
transferState = transferState,
size = size,
width = 0,
height = 0,
fileName = fileName,
fastPreflightId = null,
voiceNote = voiceNote,
borderless = borderless,
videoGif = videoGif,
quote = quote,
caption = caption,
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = audioHash,
transformProperties = transformProperties,
uuid = uuid
)
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toDecimalValue
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.FiatValue
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.testing.assertIs
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.math.BigDecimal
import java.util.Currency
@RunWith(AndroidJUnit4::class)
class FixInAppCurrencyIfAbleTest {
@get:Rule
val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
@Test
fun givenNoSubscribers_whenIMigrate_thenIDoNothing() {
migrate()
}
@Test
fun givenASubscriberButNoPayment_whenIMigrate_thenIDoNothing() {
val subscriber = insertSubscriber("USD")
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndMismatchedPayment_whenIMigrate_thenIDoNothing() {
val subscriber = insertSubscriber("USD")
val otherSubscriber = insertSubscriber("EUR")
insertPayment(otherSubscriber)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndPaymentWithNoSubscriber_whenIMigrate_thenDoNothing() {
val subscriber = insertSubscriber("USD")
insertPayment(null)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndMatchingPayment_whenIMigrate_thenUpdateCurrencyCode() {
val subscriber = insertSubscriber("USD")
insertPayment(subscriber)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs "USD"
}
@Test
fun givenASupercededSubscriber_whenIMigrate_thenIDoNothing() {
val oldSubscriber = insertSubscriber("USD")
insertPayment(oldSubscriber)
clearCurrencyCode(oldSubscriber)
insertSubscriber("USD")
migrate()
}
private fun migrate() {
V236_FixInAppSubscriberCurrencyIfAble.migrate(
context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
db = SignalDatabase.rawDatabase,
oldVersion = 0,
newVersion = 0
)
}
private fun insertSubscriber(currencyCode: String): InAppPaymentSubscriberRecord {
val record = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = Currency.getInstance(currencyCode),
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(record)
return record
}
private fun clearCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord) {
SignalDatabase.rawDatabase.update(InAppPaymentSubscriberTable.TABLE_NAME)
.values(InAppPaymentSubscriberTable.CURRENCY_CODE to "")
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
.run()
}
private fun getCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord): String {
return SignalDatabase.rawDatabase.select(InAppPaymentSubscriberTable.CURRENCY_CODE)
.from(InAppPaymentSubscriberTable.TABLE_NAME)
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
.run()
.readToSingleObject { it.requireNonNullString(InAppPaymentSubscriberTable.CURRENCY_CODE) }!!
}
private fun insertPayment(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord?): InAppPaymentTable.InAppPayment {
val id = SignalDatabase.inAppPayments.insert(
type = InAppPaymentType.RECURRING_DONATION,
state = InAppPaymentTable.State.END,
subscriberId = inAppPaymentSubscriberRecord?.subscriberId,
endOfPeriod = null,
inAppPaymentData = InAppPaymentData(
amount = FiatValue(
currencyCode = inAppPaymentSubscriberRecord?.currency?.currencyCode ?: "USD",
amount = BigDecimal.ONE.toDecimalValue()
),
level = 200,
paymentMethodType = inAppPaymentSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN
)
)
return SignalDatabase.inAppPayments.getById(id)!!
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.dependencies
import android.app.Application
import io.mockk.spyk
import okhttp3.ConnectionSpec
import okhttp3.Response
import okhttp3.WebSocket
@@ -23,6 +24,9 @@ import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.Verb
import org.thoughtcrime.securesms.testing.runSync
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.SignalServiceDataStore
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.SignalWebSocket
import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
@@ -37,12 +41,13 @@ import java.util.Optional
*
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess].
*/
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : AppDependencies.Provider by default {
private val serviceTrustStore: TrustStore
private val uncensoredConfiguration: SignalServiceConfiguration
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
private val recipientCache: LiveRecipientCache
private var signalServiceMessageSender: SignalServiceMessageSender? = null
init {
runSync {
@@ -53,18 +58,21 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
Get("/v1/websocket/?login=") {
MockResponse().success().withWebSocketUpgrade(mockIdentifiedWebSocket)
},
Get("/v1/websocket", { !it.path.contains("login") }) {
Get("/v1/websocket", {
val path = it.path
return@Get path == null || !path.contains("login")
}) {
MockResponse().success().withWebSocketUpgrade(object : WebSocketListener() {})
}
)
}
webServer.setDispatcher(object : Dispatcher() {
webServer.dispatcher = object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
val handler = handlers.firstOrNull { it.requestPredicate(request) }
return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500)
}
})
}
serviceTrustStore = SignalServiceTrustStore(application)
uncensoredConfiguration = SignalServiceConfiguration(
@@ -101,6 +109,17 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
return recipientCache
}
override fun provideSignalServiceMessageSender(
signalWebSocket: SignalWebSocket,
protocolStore: SignalServiceDataStore,
signalServiceConfiguration: SignalServiceConfiguration
): SignalServiceMessageSender {
if (signalServiceMessageSender == null) {
signalServiceMessageSender = spyk(objToCopy = default.provideSignalServiceMessageSender(signalWebSocket, protocolStore, signalServiceConfiguration))
}
return signalServiceMessageSender!!
}
class MockWebSocket : WebSocketListener() {
private val TAG = "MockWebSocket"

View File

@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.providers.BlobProvider
@@ -38,7 +38,7 @@ class AttachmentCompressionJobTest {
StreamUtil.readFully(it)
}
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(ApplicationDependencies.getApplication())
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(AppDependencies.application)
val firstPreUpload = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
val firstDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(firstPreUpload)
@@ -51,12 +51,12 @@ class AttachmentCompressionJobTest {
val secondJobLatch = CountDownLatch(1)
val jobThread = Thread {
firstCompressionJob.setContext(ApplicationDependencies.getApplication())
firstCompressionJob.setContext(AppDependencies.application)
firstJobResult = firstCompressionJob.run()
secondJobLatch.await()
secondCompressionJob!!.setContext(ApplicationDependencies.getApplication())
secondCompressionJob!!.setContext(AppDependencies.application)
secondJobResult = secondCompressionJob!!.run()
}

View File

@@ -0,0 +1,290 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import android.net.Uri
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.MediaUtil
import java.util.UUID
import kotlin.random.Random
/**
* Makes inserting messages through the "normal" code paths simpler. Mostly focused on incoming messages.
*/
class MessageHelper(private val harness: SignalActivityRule, var startTime: Long = System.currentTimeMillis()) {
val alice: RecipientId = harness.others[0]
val bob: RecipientId = harness.others[1]
val group: GroupTestingUtils.TestGroupInfo = harness.group!!
val processor: MessageContentProcessor = MessageContentProcessor(harness.context)
init {
val threadIdSlot = slot<Long>()
mockkStatic(ThreadUpdateJob::class)
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
SignalDatabase.threads.update(threadIdSlot.captured, false)
}
}
fun tearDown() {
unmockkStatic(ThreadUpdateJob::class)
}
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = sender, timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null,
allowExpireTimeChanges = false
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun outgoingText(conversationId: RecipientId = alice, successfulSend: Boolean = true, updateMessage: ((OutgoingMessage) -> OutgoingMessage)? = null): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
val threadRecipient = Recipient.resolved(conversationId)
val message = OutgoingMessage(
threadRecipient = threadRecipient,
body = MessageContentFuzzer.string(),
sentTimeMillis = messageData.timestamp,
isUrgent = true,
isSecure = true
).let { updateMessage?.invoke(it) ?: it }
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
if (successfulSend) {
SignalDatabase.messages.markAsSent(messageId, true)
}
return messageData.copy(messageId = messageId)
}
fun outgoingMessage(conversationId: RecipientId = alice, updateMessage: OutgoingMessage.() -> OutgoingMessage): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
val threadRecipient = Recipient.resolved(conversationId)
val message = OutgoingMessage(
threadRecipient = threadRecipient,
sentTimeMillis = messageData.timestamp,
isUrgent = true,
isSecure = true
).apply { updateMessage() }
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
return messageData.copy(messageId = messageId)
}
fun outgoingAttachment(data: ByteArray, uuid: UUID? = UUID.randomUUID()): Attachment {
val uri: Uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
val attachment: UriAttachment = UriAttachmentBuilder.build(
id = Random.nextLong(),
uri = uri,
contentType = MediaUtil.IMAGE_JPEG,
transformProperties = AttachmentTable.TransformProperties(),
uuid = uuid
)
return attachment
}
fun outgoingGroupChange(): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
val groupRecipient = Recipient.resolved(group.recipientId)
val decryptedGroupV2Context = DecryptedGroupV2Context(
context = group.groupV2Context,
groupState = SignalDatabase.groups.getGroup(group.groupId).get().requireV2GroupProperties().decryptedGroup
)
val updateDescription = GV2UpdateDescription.Builder()
.gv2ChangeDescription(decryptedGroupV2Context)
.groupChangeUpdate(GroupsV2UpdateMessageConverter.translateDecryptedChange(SignalStore.account.getServiceIds(), decryptedGroupV2Context))
.build()
val outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, startTime)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(messageId, true)
return messageData.copy(messageId = messageId)
}
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = sender, timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzStickerMediaMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = sender, timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.editTextMessage(
targetTimestamp = targetTimestamp,
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
).dataMessage!!
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeMessage(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeMessage(deletes.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeConversation(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeConversation(deletes.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeLocalOnlyConversation(vararg conversations: RecipientId): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeLocalOnlyConversation(conversations.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeAttachment(conversationId, message, uuid, digest, plainTextHash),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
/**
* Get the next "sentTimestamp" for current + [nextMessageOffset]th message. Useful for early message processing and future message timestamps.
*/
fun nextStartTime(nextMessageOffset: Int = 1): Long {
return startTime + 1000 * nextMessageOffset
}
data class MessageData(
val author: RecipientId = RecipientId.UNKNOWN,
val serverGuid: UUID = UUID.randomUUID(),
val timestamp: Long,
val messageId: Long = -1L
)
}

View File

@@ -6,23 +6,14 @@
package org.thoughtcrime.securesms.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
@@ -31,43 +22,28 @@ class SyncMessageProcessorTest_readSyncs {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
private lateinit var group: GroupTestingUtils.TestGroupInfo
private lateinit var processor: MessageContentProcessor
private lateinit var messageHelper: MessageHelper
@Before
fun setUp() {
alice = harness.others[0]
bob = harness.others[1]
group = harness.group!!
processor = MessageContentProcessor(harness.context)
val threadIdSlot = slot<Long>()
mockkStatic(ThreadUpdateJob::class)
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
SignalDatabase.threads.update(threadIdSlot.captured, false)
}
messageHelper = MessageHelper(harness)
}
@After
fun tearDown() {
unmockkStatic(ThreadUpdateJob::class)
messageHelper.tearDown()
}
@Test
fun handleSynchronizeReadMessage() {
val messageHelper = MessageHelper()
val message1Timestamp = messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(alice to message1Timestamp, alice to message2Timestamp)
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
@@ -75,16 +51,14 @@ class SyncMessageProcessorTest_readSyncs {
@Test
fun handleSynchronizeReadMessageMissingTimestamp() {
val messageHelper = MessageHelper()
messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(alice to message2Timestamp)
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
@@ -92,21 +66,19 @@ class SyncMessageProcessorTest_readSyncs {
@Test
fun handleSynchronizeReadWithEdits() {
val messageHelper = MessageHelper()
val message1Timestamp = messageHelper.incomingText().timestamp
messageHelper.syncReadMessage(alice to message1Timestamp)
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp
val message2Timestamp = messageHelper.incomingMedia().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(alice to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
@@ -114,112 +86,22 @@ class SyncMessageProcessorTest_readSyncs {
@Test
fun handleSynchronizeReadWithEditsInGroup() {
val messageHelper = MessageHelper()
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
val message1Timestamp = messageHelper.incomingText(sender = alice, destination = group.recipientId).timestamp
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
messageHelper.syncReadMessage(alice to message1Timestamp)
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = alice, destination = group.recipientId).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = alice, destination = group.recipientId).timestamp
val message2Timestamp = messageHelper.incomingMedia(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
val message2Timestamp = messageHelper.incomingMedia(sender = bob, destination = group.recipientId).timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(group.recipientId)!!
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(bob to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
messageHelper.syncReadMessage(messageHelper.bob to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
private inner class MessageHelper(var startTime: Long = System.currentTimeMillis()) {
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzStickerMediaMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.editTextMessage(
targetTimestamp = targetTimestamp,
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
).dataMessage!!
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
}
private data class MessageData(val serverGuid: UUID = UUID.randomUUID(), val timestamp: Long)
}

View File

@@ -0,0 +1,723 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.Matchers.greaterThan
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.logging.Log
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.MessageContentFuzzer.DeleteForMeSync
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assert
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsSize
import org.thoughtcrime.securesms.util.IdentityUtil
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class SyncMessageProcessorTest_synchronizeDeleteForMe {
companion object {
private val TAG = "SyncDeleteForMeTest"
}
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var messageHelper: MessageHelper
@Before
fun setUp() {
messageHelper = MessageHelper(harness)
}
@After
fun tearDown() {
messageHelper.tearDown()
}
@Test
fun singleMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText().timestamp
messageHelper.incomingText()
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun singleOutgoingMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.outgoingText().timestamp
messageHelper.incomingText()
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, harness.self.id to message1Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun singleGroupMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 3
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
}
@Test
fun multipleGroupMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
val message3Timestamp = messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 3
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp, messageHelper.bob to message3Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun allMessagesDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 0
val threadRecord = SignalDatabase.threads.getThreadRecord(threadId)
threadRecord assertIs null
}
@Test
fun earlyMessagesDelete() {
// GIVEN
messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
// WHEN
val nextTextMessageTimestamp = messageHelper.nextStartTime(2)
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to nextTextMessageTimestamp)
)
messageHelper.incomingText()
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun multipleConversationMessagesDelete() {
// GIVEN
messageHelper.incomingText(sender = messageHelper.alice)
val aliceMessage2 = messageHelper.incomingText(sender = messageHelper.alice).timestamp
messageHelper.incomingText(sender = messageHelper.bob)
val bobMessage2 = messageHelper.incomingText(sender = messageHelper.bob).timestamp
val aliceThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
aliceMessageCount assertIs 2
val bobThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.bob)!!
var bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
bobMessageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to aliceMessage2),
DeleteForMeSync(conversationId = messageHelper.bob, messageHelper.bob to bobMessage2)
)
// THEN
aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
aliceMessageCount assertIs 1
bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
bobMessageCount assertIs 1
}
@Test
fun singleConversationDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.alice,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = true
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
@Test
fun singleConversationNoRecentsFoundDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
val randomFutureMessages = (1..5).map {
messageHelper.alice to messageHelper.nextStartTime(it)
}
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, isFullDelete = true)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
harness.inMemoryLogger.flush()
harness.inMemoryLogger.entries().filter { it.message?.contains("Unable to find most recent received at timestamp") == true }.size assertIs 1
}
@Test
fun singleConversationNoRecentsFoundNonExpiringRecentsFoundDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
val nonExpiringMessages = messages.takeLast(5).map { it.recipientId to it.timetamp }
val randomFutureMessages = (1..5).map {
messageHelper.alice to messageHelper.nextStartTime(it)
}
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, nonExpiringMessages, true)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
harness.inMemoryLogger.flush()
harness.inMemoryLogger.entries().filter { it.message?.contains("Using backup non-expiring messages") == true }.size assertIs 1
}
@Test
fun localOnlyRemainingAfterConversationDeleteWithFullDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
Log.v(TAG, "Adding normal messages")
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val alice = Recipient.resolved(messageHelper.alice)
Log.v(TAG, "Adding identity message")
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
Log.v(TAG, "Adding profile message")
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
Log.v(TAG, "Adding call message")
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
// WHEN
Log.v(TAG, "Processing sync message")
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.alice,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = true
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
@Test
fun localOnlyRemainingAfterConversationDeleteWithoutFullDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val alice = Recipient.resolved(messageHelper.alice)
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.alice,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = false
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 3
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
}
@Test
fun groupConversationDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 50) {
messages += when (i % 3) {
1 -> MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp)
2 -> MessageTable.SyncMessageId(messageHelper.bob, messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp)
else -> MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(messageHelper.group.recipientId).timestamp)
}
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.group.recipientId,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = true
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
@Test
fun multipleConversationDelete() {
// GIVEN
val allMessages = mapOf<RecipientId, MutableList<MessageTable.SyncMessageId>>(
messageHelper.alice to mutableListOf(),
messageHelper.bob to mutableListOf()
)
allMessages.forEach { (conversation, messages) ->
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(conversation, messageHelper.incomingText(sender = conversation).timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(conversationId = conversation).timestamp)
}
}
val threadIds = allMessages.keys.map { SignalDatabase.threads.getThreadIdFor(it)!! }
threadIds.forEach { SignalDatabase.messages.getMessageCountForThread(it) assertIs 20 }
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(conversationId = messageHelper.alice, allMessages[messageHelper.alice]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true),
DeleteForMeSync(conversationId = messageHelper.bob, allMessages[messageHelper.bob]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true)
)
// THEN
threadIds.forEach {
SignalDatabase.messages.getMessageCountForThread(it) assertIs 0
SignalDatabase.threads.getThreadRecord(it) assertIs null
}
}
@Test
fun singleLocalOnlyConversation() {
// GIVEN
val alice = Recipient.resolved(messageHelper.alice)
// Insert placeholder message to prevent early thread update deletes
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
// Cleanup and confirm setup
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assert greaterThan(0)
// WHEN
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
// THEN
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
}
@Test
fun multipleLocalOnlyConversation() {
// GIVEN
val alice = Recipient.resolved(messageHelper.alice)
// Insert placeholder messages in group and alice thread to prevent early thread update deletes
val groupPlaceholderMessage = messageHelper.outgoingText(conversationId = messageHelper.group.recipientId).messageId
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
val groupThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.group.recipientId, isGroup = true)
// Identity changes
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
IdentityUtil.markIdentityVerified(harness.context, alice, false, true)
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
IdentityUtil.markIdentityVerified(harness.context, alice, false, false)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 5
IdentityUtil.markIdentityUpdate(harness.context, alice.id)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 6
// Calls
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
SignalDatabase.calls.insertOneToOneCall(2, System.currentTimeMillis(), alice.id, CallTable.Type.VIDEO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED)
SignalDatabase.calls.insertOneToOneCall(3, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED_NOTIFICATION_PROFILE)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 9
SignalDatabase.calls.insertAcceptedGroupCall(4, messageHelper.group.recipientId, CallTable.Direction.INCOMING, System.currentTimeMillis())
SignalDatabase.calls.insertDeclinedGroupCall(5, messageHelper.group.recipientId, System.currentTimeMillis())
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 8
// Detected changes
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 10
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 9
SignalDatabase.messages.insertLearnedProfileNameChangeMessage(alice, null, "username.42")
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 11
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 9
SignalDatabase.messages.insertNumberChangeMessages(alice.id)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 12
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
SignalDatabase.messages.insertSmsExportMessage(alice.id, SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 13
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
SignalDatabase.messages.insertSessionSwitchoverEvent(alice.id, aliceThreadId, SessionSwitchoverEvent())
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 14
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
// Sent failed
SignalDatabase.messages.markAsSending(messageHelper.outgoingText().messageId)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 15
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 16
messageHelper.outgoingText().let {
SignalDatabase.messages.markAsSending(it.messageId)
SignalDatabase.messages.markAsRateLimited(it.messageId)
}
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 17
// Group change
messageHelper.outgoingGroupChange()
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 11
// Cleanup and confirm setup
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
SignalDatabase.messages.deleteMessage(messageId = groupPlaceholderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
SignalDatabase.rawDatabase.withinTransaction {
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 16
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
}
// WHEN
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice, messageHelper.group.recipientId)
// THEN
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 0
SignalDatabase.threads.getThreadRecord(groupThreadId) assertIs null
}
@Test
fun singleLocalOnlyConversationHasAddressable() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
harness.inMemoryLogger.flush()
harness.inMemoryLogger.entries().filter { it.message?.contains("Thread is not local only") == true }.size assertIs 1
}
@Test
fun singleAttachmentDeletes() {
// GIVEN
val message1 = messageHelper.outgoingText { message ->
message.copy(
attachments = listOf(
messageHelper.outgoingAttachment(byteArrayOf(1, 2, 3)),
messageHelper.outgoingAttachment(byteArrayOf(2, 3, 4), null),
messageHelper.outgoingAttachment(byteArrayOf(5, 6, 7), null),
messageHelper.outgoingAttachment(byteArrayOf(10, 11, 12))
)
)
}
var attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
attachments assertIsSize 4
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
// Has all three
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[0].attachmentId,
attachment = attachments[0].copy(digest = byteArrayOf(attachments[0].attachmentId.id.toByte())),
uploadTimestamp = message1.timestamp + 1
)
// Missing uuid and digest
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[1].attachmentId,
attachment = attachments[1],
uploadTimestamp = message1.timestamp + 1
)
// Missing uuid and plain text
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[2].attachmentId,
attachment = attachments[2].copy(digest = byteArrayOf(attachments[2].attachmentId.id.toByte())),
uploadTimestamp = message1.timestamp + 1
)
SignalDatabase.rawDatabase.update(AttachmentTable.TABLE_NAME).values(AttachmentTable.DATA_HASH_END to null).where("${AttachmentTable.ID} = ?", attachments[2].attachmentId).run()
// Different has all three
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[3].attachmentId,
attachment = attachments[3].copy(digest = byteArrayOf(attachments[3].attachmentId.id.toByte())),
uploadTimestamp = message1.timestamp + 1
)
attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
// WHEN
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[0].uuid,
attachments[0].remoteDigest,
attachments[0].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
var updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 3
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[0].attachmentId }
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[1].uuid,
attachments[1].remoteDigest,
attachments[1].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 2
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[1].attachmentId }
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[2].uuid,
attachments[2].remoteDigest,
attachments[2].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 1
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[2].attachmentId }
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[3].uuid,
attachments[3].remoteDigest,
attachments[3].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
private fun DatabaseAttachment.copy(
uuid: UUID? = this.uuid,
digest: ByteArray? = this.remoteDigest
): Attachment {
return DatabaseAttachment(
attachmentId = this.attachmentId,
mmsId = this.mmsId,
hasData = this.hasData,
hasThumbnail = false,
hasArchiveThumbnail = false,
contentType = this.contentType,
transferProgress = this.transferState,
size = this.size,
fileName = this.fileName,
cdn = this.cdn,
location = this.remoteLocation,
key = this.remoteKey,
digest = digest,
incrementalDigest = this.incrementalDigest,
incrementalMacChunkSize = this.incrementalMacChunkSize,
fastPreflightId = this.fastPreflightId,
voiceNote = this.voiceNote,
borderless = this.borderless,
videoGif = this.videoGif,
width = this.width,
height = this.height,
quote = this.quote,
caption = this.caption,
stickerLocator = this.stickerLocator,
blurHash = this.blurHash,
audioHash = this.audioHash,
transformProperties = this.transformProperties,
displayOrder = this.displayOrder,
uploadTimestamp = this.uploadTimestamp,
dataHash = this.dataHash,
archiveCdn = this.archiveCdn,
archiveThumbnailCdn = this.archiveThumbnailCdn,
archiveMediaName = this.archiveMediaName,
archiveMediaId = this.archiveMediaId,
thumbnailRestoreState = this.thumbnailRestoreState,
uuid = uuid
)
}
}

View File

@@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.migrations
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.count
import org.signal.core.util.readToSingleInt
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.util.Currency
@RunWith(AndroidJUnit4::class)
class SubscriberIdMigrationJobTest {
private val testSubject = SubscriberIdMigrationJob()
@Test
fun givenNoSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectNoDatabaseEntries() {
testSubject.run()
val actual = SignalDatabase.inAppPaymentSubscribers.readableDatabase.count()
.from(InAppPaymentSubscriberTable.TABLE_NAME)
.run()
.readToSingleInt()
actual assertIs 0
}
@Test
fun givenUSDSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectASingleEntry() {
val subscriberId = SubscriberId.generate()
SignalStore.donations.setSubscriberCurrency(Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION)
SignalStore.donations.setSubscriber("USD", subscriberId)
SignalStore.donations.setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
SignalStore.donations.shouldCancelSubscriptionBeforeNextSubscribeAttempt = true
testSubject.run()
val actual = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode("USD", InAppPaymentSubscriberRecord.Type.DONATION)
actual.assertIsNotNull()
actual!!.subscriberId.bytes assertIs subscriberId.bytes
actual.paymentMethodType assertIs InAppPaymentData.PaymentMethodType.PAYPAL
actual.requiresCancel assertIs true
actual.currency assertIs Currency.getInstance("USD")
actual.type assertIs InAppPaymentSubscriberRecord.Type.DONATION
}
}

View File

@@ -90,10 +90,10 @@ class SafetyNumberBottomSheetRepositoryTest {
subjectUnderTest.removeFromStories(toRemove, listOf(destinationKey)).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(3)
testSubscriber.awaitCount(2)
// THEN
testSubscriber.assertValueAt(2) { map ->
testSubscriber.assertValueAt(1) { map ->
assertMatch(
map,
mapOf(
@@ -116,10 +116,10 @@ class SafetyNumberBottomSheetRepositoryTest {
subjectUnderTest.removeAllFromStory(distributionListMembers, distributionList).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(3)
testSubscriber.awaitCount(2)
// THEN
testSubscriber.assertValueAt(2) { map ->
testSubscriber.assertValueAt(1) { map ->
assertMatch(map, mapOf())
}
}

View File

@@ -24,9 +24,9 @@ class ContactRecordProcessorTest {
@Before
fun setup() {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
SignalStore.account.setE164(E164_SELF)
SignalStore.account.setAci(ACI_SELF)
SignalStore.account.setPni(PNI_SELF)
}
@Test

View File

@@ -4,7 +4,7 @@ import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
import org.thoughtcrime.securesms.recipients.Recipient
@@ -30,14 +30,14 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
uuid = serviceId.rawUuid,
e164 = e164,
deviceId = 1,
identityKey = SignalStore.account().aciIdentityKey.publicKey.publicKey,
identityKey = SignalStore.account.aciIdentityKey.publicKey.publicKey,
expires = 31337
)
fun process(envelope: Envelope, serverDeliveredTimestamp: Long) {
val start = System.currentTimeMillis()
val bufferedStore = BufferedProtocolStore.create()
ApplicationDependencies.getIncomingMessageObserver()
AppDependencies.incomingMessageObserver
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
?.mapNotNull { it.run() }
?.forEach { it.enqueue() }
@@ -48,7 +48,7 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
}
fun encrypt(now: Long, destination: Recipient): Envelope {
return ApplicationDependencies.getSignalServiceMessageSender().getEncryptedMessage(
return AppDependencies.signalServiceMessageSender.getEncryptedMessage(
SignalServiceAddress(destination.requireServiceId(), destination.requireE164()),
FakeClientHelpers.getTargetUnidentifiedAccess(ProfileKeyUtil.getSelfProfileKey(), ProfileKey(destination.profileKey), aliceSenderCertificate),
1,

View File

@@ -80,7 +80,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
}
private fun getAliceServiceId(): ServiceId {
return SignalStore.account().requireAci()
return SignalStore.account.requireAci()
}
private fun getAlicePreKeyBundle(): PreKeyBundle {
@@ -103,7 +103,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
val selfSignedPreKeyRecord = SignalDatabase.signedPreKeys.get(getAliceServiceId(), selfSignedPreKeyId)!!
return PreKeyBundle(
SignalStore.account().registrationId,
SignalStore.account.registrationId,
1,
selfPreKeyId,
selfPreKeyRecord.keyPair.publicKey,
@@ -115,11 +115,11 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
}
private fun getAliceProtocolAddress(): SignalProtocolAddress {
return SignalProtocolAddress(SignalStore.account().requireAci().toString(), 1)
return SignalProtocolAddress(SignalStore.account.requireAci().toString(), 1)
}
private fun getAlicePublicKey(): IdentityKey {
return SignalStore.account().aciIdentityKey.publicKey
return SignalStore.account.aciIdentityKey.publicKey
}
private fun getAliceProfileKey(): ProfileKey {
@@ -139,10 +139,12 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean = true
override fun loadSession(address: SignalProtocolAddress?): SessionRecord = aliceSessionRecord ?: SessionRecord()
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): Boolean = false
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) { aliceSessionRecord = record }
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) {
aliceSessionRecord = record
}
override fun getSubDeviceSessions(name: String?): List<Int> = emptyList()
override fun containsSession(address: SignalProtocolAddress?): Boolean = aliceSessionRecord != null
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account().aciIdentityKey.publicKey
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account.aciIdentityKey.publicKey
override fun loadPreKey(preKeyId: Int): PreKeyRecord = throw UnsupportedOperationException()
override fun storePreKey(preKeyId: Int, record: PreKeyRecord?) = throw UnsupportedOperationException()
override fun containsPreKey(preKeyId: Int): Boolean = throw UnsupportedOperationException()

View File

@@ -2,12 +2,15 @@ package org.thoughtcrime.securesms.testing
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.thoughtcrime.securesms.messages.TestMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.internal.push.AttachmentPointer
import org.whispersystems.signalservice.internal.push.BodyRange
import org.whispersystems.signalservice.internal.push.Content
@@ -61,13 +64,13 @@ object MessageContentFuzzer {
* - An expire timer value
* - Bold style body ranges
*/
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null, allowExpireTimeChanges: Boolean = true): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
timestamp = sentTimestamp
body = string()
if (random.nextBoolean()) {
if (allowExpireTimeChanges && random.nextBoolean()) {
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
}
if (random.nextBoolean()) {
@@ -150,6 +153,121 @@ object MessageContentFuzzer {
).build()
}
fun syncDeleteForMeMessage(allDeletes: List<DeleteForMeSync>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
messageDeletes = allDeletes.map { (conversationId, conversationDeletes) ->
val conversation = Recipient.resolved(conversationId)
SyncMessage.DeleteForMe.MessageDeletes(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
},
messages = conversationDeletes.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
}
)
}
)
)
).build()
}
fun syncDeleteForMeConversation(allDeletes: List<DeleteForMeSync>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
conversationDeletes = allDeletes.map { delete ->
val conversation = Recipient.resolved(delete.conversationId)
SyncMessage.DeleteForMe.ConversationDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
},
mostRecentMessages = delete.messages.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
},
mostRecentNonExpiringMessages = delete.nonExpiringMessages.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
},
isFullDelete = delete.isFullDelete
)
}
)
)
).build()
}
fun syncDeleteForMeLocalOnlyConversation(conversations: List<RecipientId>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
localOnlyConversationDeletes = conversations.map { conversationId ->
val conversation = Recipient.resolved(conversationId)
SyncMessage.DeleteForMe.LocalOnlyConversationDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
}
)
}
)
)
).build()
}
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): Content {
val conversation = Recipient.resolved(conversationId)
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
attachmentDeletes = listOf(
SyncMessage.DeleteForMe.AttachmentDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
},
targetMessage = SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(message.first).requireAci().toString(),
sentTimestamp = message.second
),
uuid = uuid?.let { UuidUtil.toByteString(it) },
fallbackDigest = digest?.toByteString(),
fallbackPlaintextHash = plainTextHash?.let { Base64.decodeOrNull(it)?.toByteString() }
)
)
)
)
).build()
}
/**
* Create a random media message that may be:
* - A text body
@@ -278,7 +396,7 @@ object MessageContentFuzzer {
caption = string(allowNullString = true)
blurHash = string()
uploadTimestamp = random.nextLong()
cdnNumber = 1
cdnNumber = 2
build()
}
@@ -290,4 +408,14 @@ object MessageContentFuzzer {
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
return envelopeTimestamp + 10
}
data class DeleteForMeSync(
val conversationId: RecipientId,
val messages: List<Pair<RecipientId, Long>>,
val nonExpiringMessages: List<Pair<RecipientId, Long>> = emptyList(),
val isFullDelete: Boolean = true,
val attachments: List<Pair<Long, AttachmentTable.SyncAttachmentId>> = emptyList()
) {
constructor(conversationId: RecipientId, vararg messages: Pair<RecipientId, Long>) : this(conversationId, messages.toList())
}
}

View File

@@ -31,7 +31,7 @@ object MockProvider {
val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply {
svr1Credentials = AuthCredentials.create("username", "password")
svr2Credentials = null
svr2Credentials = AuthCredentials.create("username", "password")
}
val primaryOnlyDeviceList = DeviceInfoList().apply {
@@ -68,7 +68,7 @@ object MockProvider {
}
}
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account().aciIdentityKey, deviceId: Int): PreKeyResponse {
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account.aciIdentityKey, deviceId: Int): PreKeyResponse {
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())

View File

@@ -55,5 +55,5 @@ inline fun <reified T> RecordedRequest.parsedRequestBody(): T {
}
private fun defaultRequestPredicate(verb: String, path: String, predicate: RequestPredicate = { true }): RequestPredicate = { request ->
request.method == verb && request.path.startsWith("/$path") && predicate(request)
request.method == verb && request.path?.startsWith("/$path") == true && predicate(request)
}

View File

@@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.ProfileName
@@ -47,7 +47,7 @@ import java.util.UUID
*/
class SignalActivityRule(private val othersCount: Int = 4, private val createGroup: Boolean = false) : ExternalResource() {
val application: Application = ApplicationDependencies.getApplication()
val application: Application = AppDependencies.application
lateinit var context: Context
private set
@@ -90,8 +90,8 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
preferences.edit().putBoolean("passphrase_initialized", true).commit()
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
val registrationRepository = RegistrationRepository(application)
@@ -111,19 +111,19 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
verifyAccountResponse = VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
masterKey = null,
pin = null,
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
),
false
).blockingGet()
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
SignalStore.svr().optOut()
SignalStore.svr.optOut()
RegistrationUtil.maybeMarkRegistrationComplete()
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
SignalStore.settings().isMessageNotificationsEnabled = false
SignalStore.settings.isMessageNotificationsEnabled = false
return Recipient.self()
}
@@ -141,11 +141,11 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, false))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
others += recipientId
othersKeys += otherIdentity
}
@@ -158,14 +158,14 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
}
fun changeIdentityKey(recipient: Recipient, identityKey: IdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey) {
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
}
fun getIdentity(recipient: Recipient): IdentityKey {
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
return AppDependencies.protocolStore.aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
}
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
AppDependencies.protocolStore.aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
}
}

View File

@@ -26,8 +26,8 @@ class SignalDatabaseRule(
override fun starting(description: Description?) {
deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
}
override fun finished(description: Description?) {

View File

@@ -1,13 +1,14 @@
package org.thoughtcrime.securesms.testing
import android.database.Cursor
import android.util.Base64
import org.hamcrest.Matcher
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.notNullValue
import org.hamcrest.Matchers.nullValue
import org.signal.core.util.Hex
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.select
@@ -56,33 +57,41 @@ infix fun <E, T : Collection<E>> T.assertIsSize(expected: Int) {
assertThat(this, hasSize(expected))
}
infix fun <T : Any> T.assert(matcher: Matcher<T>) {
assertThat(this, matcher)
}
fun CountDownLatch.awaitFor(duration: Duration) {
if (!await(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS)) {
throw TimeoutException("Latch await took longer than ${duration.inWholeMilliseconds}ms")
}
}
fun dumpTableToLogs(tag: String = "TestUtils", table: String) {
dumpTable(table).forEach { Log.d(tag, it.toString()) }
fun dumpTableToLogs(tag: String = "TestUtils", table: String, columns: Set<String>? = null) {
dumpTable(table, columns).forEach { Log.d(tag, it.toString()) }
}
fun dumpTable(table: String): List<List<Pair<String, String?>>> {
fun dumpTable(table: String, columns: Set<String>?): List<List<Pair<String, String?>>> {
return SignalDatabase.rawDatabase
.select()
.from(table)
.run()
.readToList { cursor ->
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
val index = cursor.getColumnIndex(column)
var data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
}
val map: List<Pair<String, String?>> = cursor.columnNames.mapNotNull { column ->
if (columns == null || columns.contains(column)) {
val index = cursor.getColumnIndex(column)
var data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Hex.toStringCondensed(cursor.getBlob(index))
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
}
column to data
column to data
} else {
null
}
}
map
}

View File

@@ -1,11 +0,0 @@
package org.thoughtcrime.securesms.util;
/**
* A class that allows us to inject feature flags during tests.
*/
public final class FeatureFlagsAccessor {
public static void forceValue(String key, Object value) {
FeatureFlags.FORCED_VALUES.put(key, value);
}
}

View File

@@ -2,9 +2,9 @@ package org.signal.benchmark
import android.content.Context
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.account.PreKeyUpload
import org.whispersystems.signalservice.api.push.ServiceId.ACI
@@ -16,15 +16,15 @@ import java.util.Optional
class DummyAccountManagerFactory : AccountManagerFactory() {
override fun createAuthenticated(context: Context, aci: ACI, pni: PNI, number: String, deviceId: Int, password: String): SignalServiceAccountManager {
return DummyAccountManager(
ApplicationDependencies.getSignalServiceNetworkAccess().getConfiguration(number),
AppDependencies.signalServiceNetworkAccess.getConfiguration(number),
aci,
pni,
number,
deviceId,
password,
BuildConfig.SIGNAL_AGENT,
FeatureFlags.okHttpAutomaticRetry(),
FeatureFlags.groupLimits().hardLimit
RemoteConfig.okHttpAutomaticRetry,
RemoteConfig.groupLimits.hardLimit
)
}

View File

@@ -161,7 +161,8 @@ object TestMessages {
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
System.currentTimeMillis(),
null
)
}
@@ -184,7 +185,8 @@ object TestMessages {
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
System.currentTimeMillis(),
null
)
}

View File

@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.profiles.ProfileName
@@ -35,7 +35,7 @@ object TestUsers {
private var generatedOthers: Int = 0
fun setupSelf(): Recipient {
val application: Application = ApplicationDependencies.getApplication()
val application: Application = AppDependencies.application
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
@@ -44,8 +44,8 @@ object TestUsers {
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
preferences.edit().putBoolean("passphrase_initialized", true).commit()
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
val registrationRepository = RegistrationRepository(application)
val registrationData = RegistrationData(
@@ -63,8 +63,8 @@ object TestUsers {
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
masterKey = null,
pin = null,
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
)
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
@@ -77,7 +77,7 @@ object TestUsers {
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
SignalStore.svr().optOut()
SignalStore.svr.optOut()
RegistrationUtil.maybeMarkRegistrationComplete()
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
@@ -100,11 +100,11 @@ object TestUsers {
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
others += recipientId
}

View File

@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
@@ -123,7 +123,7 @@ class ConversationElementGenerator {
)
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
ApplicationDependencies.getApplication(),
AppDependencies.application,
record,
Recipient.UNKNOWN
)

View File

@@ -280,7 +280,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
@@ -304,6 +304,10 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onPaymentTombstoneClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onShowSafetyTips(forGroup: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}

View File

@@ -748,13 +748,6 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".backup.v2.ui.subscription.MessageBackupsFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".stories.settings.StorySettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -797,13 +790,6 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".badges.gifts.flow.GiftFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -830,14 +816,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".registration.RegistrationNavigationActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".registration.v2.ui.RegistrationV2Activity"
<activity android:name=".registration.ui.RegistrationActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
@@ -982,8 +961,8 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".backup.v2.ui.subscription.MessageBackupsTestRestoreActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
<activity android:name=".registration.ui.restore.RemoteRestoreActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:exported="false"/>
<activity android:name=".profiles.manage.EditProfileActivity"
@@ -1099,7 +1078,7 @@
android:theme="@style/Theme.Signal.WallpaperCropper"
android:exported="false"/>
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrImageSelectionActivity"
<activity android:name=".components.settings.app.usernamelinks.main.QrImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar"
android:exported="false"/>
@@ -1114,10 +1093,10 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".components.settings.app.subscription.donate.CheckoutFlowActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<service
android:enabled="true"

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.account.AccountAttributes
object AppCapabilities {
@@ -17,7 +18,8 @@ object AppCapabilities {
stories = true,
giftBadges = true,
pni = true,
paymentActivation = true
paymentActivation = true,
deleteSync = RemoteConfig.deleteSyncEnabled
)
}
}

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
@@ -37,29 +37,29 @@ public final class AppInitialization {
TextSecurePreferences.setReadReceiptsEnabled(context, true);
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
public static void onPostBackupRestore(@NonNull Context context) {
Log.i(TAG, "onPostBackupRestore()");
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onPostBackupRestore();
SignalStore.onFirstEverAppLaunch();
SignalStore.onboarding().clearAll();
TextSecurePreferences.onPostBackupRestore(context);
TextSecurePreferences.setPasswordDisabled(context, true);
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
EmojiSearchIndexDownloadJob.scheduleImmediately();
}
@@ -74,12 +74,12 @@ public final class AppInitialization {
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
TextSecurePreferences.setPasswordDisabled(context, true);
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
}

View File

@@ -47,20 +47,22 @@ import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmFetchManager;
import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
import org.thoughtcrime.securesms.jobs.BuildExpirationConfirmationJob;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.ExternalLaunchDonationJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.InAppPaymentAuthCheckJob;
import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
@@ -70,7 +72,6 @@ import org.thoughtcrime.securesms.jobs.RefreshSvrCredentialsJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
@@ -97,7 +98,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -143,7 +144,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
long startTime = System.currentTimeMillis();
if (FeatureFlags.internalUser()) {
if (RemoteConfig.internalUser()) {
Tracer.getInstance().setMaxBufferSize(35_000);
}
@@ -168,7 +169,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addBlocking("scrubber", () -> Scrubber.setIdentifierHmacKeyProvider(() -> SignalStore.svr().getOrCreateMasterKey().deriveLoggingKey()))
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
.addBlocking("app-migrations", this::initializeApplicationMigrations)
.addBlocking("lifecycle-observer", () -> ApplicationDependencies.getAppForegroundObserver().addListener(this))
.addBlocking("lifecycle-observer", () -> AppDependencies.getAppForegroundObserver().addListener(this))
.addBlocking("message-retriever", this::initializeMessageRetrieval)
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
.addBlocking("proxy-init", () -> {
@@ -178,7 +179,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
})
.addBlocking("blob-provider", this::initializeBlobProvider)
.addBlocking("feature-flags", FeatureFlags::init)
.addBlocking("remote-config", RemoteConfig::init)
.addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete())
@@ -196,27 +197,27 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(this::beginJobLoop)
.addNonBlocking(EmojiSource::refresh)
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(() -> AppDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(this::ensureProfileUploaded)
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
.addNonBlocking(() -> AppDependencies.getExpireStoriesManager().scheduleIfNecessary())
.addPostRender(() -> AppDependencies.getDeletedCallEventManager().scheduleIfNecessary())
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(this::initializeTrimThreadsByDateManager)
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), RemoteConfig.retryRespondMaxAge()))
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
.addPostRender(() -> AppDependencies.getJobManager().add(new FontDownloaderJob()))
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
.addPostRender(() -> AppDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
.addPostRender(() -> AppDependencies.getRecipientCache().warmUp())
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
.addPostRender(GroupRingCleanupJob::enqueue)
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
@@ -233,20 +234,20 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
long startTime = System.currentTimeMillis();
Log.i(TAG, "App is now visible.");
ApplicationDependencies.getFrameRateTracker().start();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
ApplicationDependencies.getDeadlockDetector().start();
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
ExternalLaunchDonationJob.enqueueIfNecessary();
AppDependencies.getFrameRateTracker().start();
AppDependencies.getMegaphoneRepository().onAppForegrounded();
AppDependencies.getDeadlockDetector().start();
InAppPaymentKeepAliveJob.enqueueAndTrackTimeIfNecessary();
FcmFetchManager.onForeground(this);
startAnrDetector();
SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary();
InAppPaymentAuthCheckJob.enqueueIfNeeded();
RemoteConfig.refreshIfNecessary();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary();
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getShakeToReport().enable();
AppDependencies.getShakeToReport().enable();
checkBuildExpiration();
MemoryTracker.start();
@@ -255,7 +256,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
long timeDiff = currentTime - lastForegroundTime;
if (timeDiff < 0) {
Log.w(TAG, "Time travel! The system clock has moved backwards. (currentTime: " + currentTime + " ms, lastForegroundTime: " + lastForegroundTime + " ms, diff: " + timeDiff + " ms)");
Log.w(TAG, "Time travel! The system clock has moved backwards. (currentTime: " + currentTime + " ms, lastForegroundTime: " + lastForegroundTime + " ms, diff: " + timeDiff + " ms)", true);
}
SignalStore.misc().setLastForegroundTime(currentTime);
@@ -268,18 +269,18 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
public void onBackground() {
Log.i(TAG, "App is no longer visible.");
KeyCachingService.onAppBackgrounded(this);
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
ApplicationDependencies.getFrameRateTracker().stop();
ApplicationDependencies.getShakeToReport().disable();
ApplicationDependencies.getDeadlockDetector().stop();
AppDependencies.getMessageNotifier().clearVisibleThread();
AppDependencies.getFrameRateTracker().stop();
AppDependencies.getShakeToReport().disable();
AppDependencies.getDeadlockDetector().stop();
MemoryTracker.stop();
AnrDetector.stop();
}
public void checkBuildExpiration() {
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
Log.w(TAG, "Build expired!");
SignalStore.misc().setClientDeprecated(true);
if (Util.getTimeUntilBuildExpiry(SignalStore.misc().getEstimatedServerTime()) <= 0 && !SignalStore.misc().isClientDeprecated()) {
Log.w(TAG, "Build potentially expired! Enqueing job to check.", true);
AppDependencies.getJobManager().add(new BuildExpirationConfirmationJob());
}
}
@@ -288,7 +289,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
* This is so we can capture ANR's that happen on boot before the foreground event.
*/
private void startAnrDetector() {
AnrDetector.start(TimeUnit.SECONDS.toMillis(5), FeatureFlags::internalUser, (dumps) -> {
AnrDetector.start(TimeUnit.SECONDS.toMillis(5), RemoteConfig::internalUser, (dumps) -> {
LogDatabase.getInstance(this).anrs().save(System.currentTimeMillis(), dumps);
return Unit.INSTANCE;
});
@@ -313,9 +314,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
@VisibleForTesting
protected void initializeLogging() {
Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), new PersistentLogger(this));
Log.initialize(RemoteConfig::internalUser, new AndroidLogger(), new PersistentLogger(this));
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
SignalProtocolLoggerProvider.initializeLogging(BuildConfig.LIBSIGNAL_LOG_LEVEL);
SignalExecutors.UNBOUNDED.execute(() -> {
Log.blockUntilAllWritesFinished();
@@ -354,16 +356,16 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
private void initializeApplicationMigrations() {
ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager());
ApplicationMigrations.onApplicationCreate(this, AppDependencies.getJobManager());
}
public void initializeMessageRetrieval() {
ApplicationDependencies.getIncomingMessageObserver();
AppDependencies.getIncomingMessageObserver();
}
@VisibleForTesting
void initializeAppDependencies() {
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this));
AppDependencies.init(this, new ApplicationDependencyProvider(this));
}
private void initializeFirstEverAppLaunch() {
@@ -386,34 +388,36 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void initializeFcmCheck() {
if (SignalStore.account().isRegistered()) {
long nextSetTime = SignalStore.account().getFcmTokenLastSetTime() + TimeUnit.HOURS.toMillis(6);
long lastSetTime = SignalStore.account().getFcmTokenLastSetTime();
long nextSetTime = lastSetTime + TimeUnit.HOURS.toMillis(6);
long now = System.currentTimeMillis();
if (SignalStore.account().getFcmToken() == null || nextSetTime <= System.currentTimeMillis()) {
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
if (SignalStore.account().getFcmToken() == null || nextSetTime <= now || lastSetTime > now) {
AppDependencies.getJobManager().add(new FcmRefreshJob());
}
}
}
private void initializeExpiringMessageManager() {
ApplicationDependencies.getExpiringMessageManager().checkSchedule();
AppDependencies.getExpiringMessageManager().checkSchedule();
}
private void initializeRevealableMessageManager() {
ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
AppDependencies.getViewOnceMessageManager().scheduleIfNecessary();
}
private void initializePendingRetryReceiptManager() {
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
AppDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
}
private void initializeScheduledMessageManager() {
ApplicationDependencies.getScheduledMessageManager().scheduleIfNecessary();
AppDependencies.getScheduledMessageManager().scheduleIfNecessary();
}
private void initializeTrimThreadsByDateManager() {
KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration();
if (keepMessagesDuration != KeepMessagesDuration.FOREVER) {
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
AppDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
}
}
@@ -434,10 +438,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void initializeRingRtc() {
try {
Map<String, String> fieldTrials = new HashMap<>();
if (FeatureFlags.callingFieldTrialAnyAddressPortsKillSwitch()) {
if (RemoteConfig.callingFieldTrialAnyAddressPortsKillSwitch()) {
fieldTrials.put("RingRTC-AnyAddressPortsKillSwitch", "Enabled");
}
if (!SignalStore.internalValues().callingDisableLBRed()) {
if (!SignalStore.internal().callingDisableLBRed()) {
fieldTrials.put("RingRTC-Audio-LBRed-For-Opus", "Enabled,bitrate_pri:22000");
}
CallManager.initialize(this, new RingRtcLogger(), fieldTrials);
@@ -448,7 +452,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
@WorkerThread
private void initializeCircumvention() {
if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) {
if (AppDependencies.getSignalServiceNetworkAccess().isCensored()) {
try {
ProviderInstaller.installIfNeeded(ApplicationContext.this);
} catch (Throwable t) {
@@ -458,21 +462,21 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
private void ensureProfileUploaded() {
if (SignalStore.account().isRegistered() && !SignalStore.registrationValues().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
if (SignalStore.account().isRegistered() && !SignalStore.registration().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
Log.w(TAG, "User has a profile, but has not uploaded one. Uploading now.");
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
AppDependencies.getJobManager().add(new ProfileUploadJob());
}
}
private void executePendingContactSync() {
if (TextSecurePreferences.needsFullContactSync(this)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
AppDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
}
}
@VisibleForTesting
protected void beginJobLoop() {
ApplicationDependencies.getJobManager().beginJobLoop();
AppDependencies.getJobManager().beginJobLoop();
}
@WorkerThread

View File

@@ -28,11 +28,11 @@ import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar;
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FullscreenHelper;
@@ -91,16 +91,18 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
Recipient.live(recipientId).observe(this, recipient -> {
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient)
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isSelf() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();
FallbackAvatar fallbackAvatar = recipient.isSelf() ? new FallbackAvatar.Resource.Person(recipient.getAvatarColor())
: recipient.getFallbackAvatar();
Drawable fallbackDrawable = new FallbackAvatarDrawable(context, fallbackAvatar);
Resources resources = this.getResources();
Glide.with(this)
.asBitmap()
.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this))
.error(fallbackPhoto.asCallCard(this))
.fallback(fallbackDrawable)
.error(fallbackDrawable)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.addListener(new RequestListener<Bitmap>() {
@Override

View File

@@ -14,7 +14,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.ConfigurationUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
@@ -47,7 +47,7 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onStart() {
logEvent("onStart()");
ApplicationDependencies.getShakeToReport().registerActivity(this);
AppDependencies.getShakeToReport().registerActivity(this);
super.onStart();
}

View File

@@ -125,12 +125,13 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onEditedIndicatorClicked(@NonNull ConversationMessage conversationMessage);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
void onShowSafetyTips(boolean forGroup);
void onReportSpamLearnMoreClicked();
void onMessageRequestAcceptOptionsClicked();
void onItemDoubleClick(MultiselectPart multiselectPart);
void onPaymentTombstoneClicked();
}
}

View File

@@ -35,6 +35,10 @@ class BiometricDeviceAuthentication(
private val DISALLOWED_BIOMETRIC_VERSIONS = setOf(28, 29)
}
fun canAuthenticate(): Boolean {
return biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS
}
fun authenticate(context: Context, force: Boolean, showConfirmDeviceCredentialIntent: () -> Unit): Boolean {
val isKeyGuardSecure = ServiceUtil.getKeyguardManager(context).isKeyguardSecure

View File

@@ -29,14 +29,10 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DisplayMetricsUtil;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.lang.ref.WeakReference;

View File

@@ -337,7 +337,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
selectionLimit,
new ContactSearchAdapter.DisplayOptions(
isMulti,
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
newCallCallback != null,
false

View File

@@ -28,7 +28,7 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -191,7 +191,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
try {
Context context = DeviceActivity.this;
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
SignalServiceAccountManager accountManager = AppDependencies.getSignalServiceAccountManager();
String verificationCode = accountManager.getNewDeviceVerificationCode();
String ephemeralId = uri.getQueryParameter("uuid");
String publicKeyEncoded = uri.getQueryParameter("pub_key");

View File

@@ -25,7 +25,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.devicelist.Device;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -60,7 +60,7 @@ public class DeviceListFragment extends ListFragment
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
this.accountManager = AppDependencies.getSignalServiceAccountManager();
}
@Override

View File

@@ -17,9 +17,8 @@ import org.signal.core.util.tracing.Tracer;
import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
import org.thoughtcrime.securesms.keyvalue.InternalValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
@@ -28,12 +27,11 @@ import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2Activity;
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
import org.thoughtcrime.securesms.restore.RestoreActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
@@ -55,7 +53,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_TRANSFER_ONGOING = 8;
private static final int STATE_TRANSFER_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private static final int STATE_RESTORE_BACKUP = 11;
private static final int STATE_TRANSFER_OR_RESTORE = 11;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -64,7 +62,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
protected final void onCreate(Bundle savedInstanceState) {
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
AppStartup.getInstance().onCriticalRenderEventStart();
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
this.networkAccess = AppDependencies.getSignalServiceNetworkAccess();
onPreCreate();
final boolean locked = KeyCachingService.isLocked(this);
@@ -93,7 +91,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
@Override
public void onMasterSecretCleared() {
Log.d(TAG, "onMasterSecretCleared()");
if (ApplicationDependencies.getAppForegroundObserver().isForegrounded()) routeApplicationState(true);
if (AppDependencies.getAppForegroundObserver().isForegrounded()) routeApplicationState(true);
else finish();
}
@@ -153,7 +151,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
case STATE_RESTORE_BACKUP: return getRestoreIntent();
case STATE_TRANSFER_OR_RESTORE: return getTransferOrRestoreIntent();
default: return null;
}
}
@@ -167,12 +165,12 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.internalValues().enterRestoreV2Flow()) {
return STATE_RESTORE_BACKUP;
} else if (SignalStore.storageService().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userHasSkippedOrForgottenPin()) {
return STATE_CREATE_SIGNAL_PIN;
} else if (userCanTransferOrRestore()) {
return STATE_TRANSFER_OR_RESTORE;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;
} else if (userMustCreateSignalPin()) {
@@ -188,16 +186,20 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
}
private boolean userCanTransferOrRestore() {
return !SignalStore.registration().isRegistrationComplete() && RemoteConfig.restoreAfterRegistration() && !SignalStore.registration().hasSkippedTransferOrRestore();
}
private boolean userMustCreateSignalPin() {
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().lastPinCreateFailed() && !SignalStore.svr().hasOptedOut();
return !SignalStore.registration().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().lastPinCreateFailed() && !SignalStore.svr().hasOptedOut();
}
private boolean userHasSkippedOrForgottenPin() {
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().hasOptedOut() && SignalStore.svr().isPinForgottenOrSkipped();
return !SignalStore.registration().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().hasOptedOut() && SignalStore.svr().isPinForgottenOrSkipped();
}
private boolean userMustSetProfileName() {
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
return !SignalStore.registration().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
}
private Intent getCreatePassphraseIntent() {
@@ -206,7 +208,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private Intent getPromptPassphraseIntent() {
Intent intent = getRoutedIntent(PassphrasePromptActivity.class, getIntent());
intent.putExtra(PassphrasePromptActivity.FROM_FOREGROUND, ApplicationDependencies.getAppForegroundObserver().isForegrounded());
intent.putExtra(PassphrasePromptActivity.FROM_FOREGROUND, AppDependencies.getAppForegroundObserver().isForegrounded());
return intent;
}
@@ -218,11 +220,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private Intent getPushRegistrationIntent() {
if (FeatureFlags.registrationV2()) {
return RegistrationV2Activity.newIntentForNewRegistration(this, getIntent());
} else {
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
}
return RegistrationActivity.newIntentForNewRegistration(this, getIntent());
}
private Intent getEnterSignalPinIntent() {
@@ -241,8 +239,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return getRoutedIntent(CreateSvrPinActivity.class, intent);
}
private Intent getRestoreIntent() {
Intent intent = RestoreActivity.getIntentForRestore(this);
private Intent getTransferOrRestoreIntent() {
Intent intent = RestoreActivity.getIntentForTransferOrRestore(this);
return getRoutedIntent(intent, getIntent());
}

View File

@@ -29,13 +29,14 @@ import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Rational;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.PictureInPictureModeChangedInfo;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.lifecycle.LiveDataReactiveStreams;
@@ -80,7 +81,7 @@ import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoView
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -93,7 +94,7 @@ import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
@@ -116,6 +117,7 @@ import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
import static org.thoughtcrime.securesms.permissions.PermissionDeniedBottomSheet.showPermissionFragment;
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback, ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
@@ -165,7 +167,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private boolean enterPipOnResume;
private long lastProcessedIntentTimestamp;
private WebRtcViewModel previousEvent = null;
private boolean isAskingForPermission;
private Disposable ephemeralStateDisposable = Disposable.empty();
@Override
@@ -237,6 +239,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
initializePendingParticipantFragmentListener();
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface));
if (!hasCameraPermission() & !hasAudioPermission()) {
askCameraAudioPermissions(() -> handleSetMuteVideo(false));
} else if (!hasAudioPermission()) {
askAudioPermissions(() -> {});
}
}
private void registerSystemPipChangeListeners() {
@@ -250,10 +258,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
protected void onStart() {
super.onStart();
ephemeralStateDisposable = ApplicationDependencies.getSignalCallManager()
.ephemeralStates()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(viewModel::updateFromEphemeralState);
ephemeralStateDisposable = AppDependencies.getSignalCallManager()
.ephemeralStates()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(viewModel::updateFromEphemeralState);
}
@Override
@@ -299,7 +307,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
Log.i(TAG, "onPause");
super.onPause();
if (!viewModel.isCallStarting()) {
if (!isAskingForPermission && !viewModel.isCallStarting()) {
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
finish();
@@ -319,15 +327,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
requestNewSizesThrottle.clear();
}
ApplicationDependencies.getSignalCallManager().setEnableVideo(false);
AppDependencies.getSignalCallManager().setEnableVideo(false);
if (!viewModel.isCallStarting()) {
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null) {
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
AppDependencies.getSignalCallManager().cancelPreJoin();
} else if (state.getCallState().getInOngoingCall() && isInPipMode()) {
ApplicationDependencies.getSignalCallManager().relaunchPipOnForeground();
AppDependencies.getSignalCallManager().relaunchPipOnForeground();
}
}
}
@@ -407,7 +415,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void initializePendingParticipantFragmentListener() {
if (!FeatureFlags.adHocCalling()) {
if (!RemoteConfig.adHocCalling()) {
return;
}
@@ -432,7 +440,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.WebRtcCallActivity__approve_all, (dialog, which) -> {
for (RecipientId id : recipientIds) {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id);
AppDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id);
}
})
.show();
@@ -444,7 +452,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.WebRtcCallActivity__deny_all, (dialog, which) -> {
for (RecipientId id : recipientIds) {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id);
AppDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id);
}
})
.show();
@@ -512,12 +520,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null) {
if (state.needsNewRequestSizes()) {
requestNewSizesThrottle.publish(() -> ApplicationDependencies.getSignalCallManager().updateRenderedResolutions());
requestNewSizesThrottle.publish(() -> AppDependencies.getSignalCallManager().updateRenderedResolutions());
}
}
});
viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> AppDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
viewModel.getControlsRotation().observe(this, callScreen::rotateControls);
addOnPictureInPictureModeChangedListener(info -> {
@@ -643,83 +651,66 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void handleSetAudioHandset() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.EARPIECE));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.EARPIECE));
}
private void handleSetAudioSpeaker() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.SPEAKER_PHONE));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.SPEAKER_PHONE));
}
private void handleSetAudioBluetooth() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.BLUETOOTH));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.BLUETOOTH));
}
private void handleSetAudioWiredHeadset() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.WIRED_HEADSET));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.WIRED_HEADSET));
}
private void handleSetMuteAudio(boolean enabled) {
ApplicationDependencies.getSignalCallManager().setMuteAudio(enabled);
AppDependencies.getSignalCallManager().setMuteAudio(enabled);
}
private void handleSetMuteVideo(boolean muted) {
Recipient recipient = viewModel.getRecipient().get();
if (!recipient.equals(Recipient.UNKNOWN)) {
String recipientDisplayName = recipient.getDisplayName(this);
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName), R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName))
.onAllGranted(() -> ApplicationDependencies.getSignalCallManager().setEnableVideo(!muted))
.execute();
Runnable onGranted = () -> AppDependencies.getSignalCallManager().setEnableVideo(!muted);
askCameraPermissions(onGranted);
}
}
private void handleFlipCamera() {
ApplicationDependencies.getSignalCallManager().flipCamera();
AppDependencies.getSignalCallManager().flipCamera();
}
private void handleAnswerWithAudio() {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone),
R.drawable.ic_mic_solid_24)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
ApplicationDependencies.getSignalCallManager().acceptCall(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
Runnable onGranted = () -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
AppDependencies.getSignalCallManager().acceptCall(false);
};
askAudioPermissions(onGranted);
}
private void handleAnswerWithVideo() {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone_and_camera), R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
ApplicationDependencies.getSignalCallManager().acceptCall(true);
handleSetMuteVideo(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
Runnable onGranted = () -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
AppDependencies.getSignalCallManager().acceptCall(true);
handleSetMuteVideo(false);
};
if (!hasCameraPermission() &!hasAudioPermission()) {
askCameraAudioPermissions(onGranted);
} else if (!hasAudioPermission()) {
askAudioPermissions(onGranted);
} else {
askCameraPermissions(onGranted);
}
}
private void handleDenyCall() {
Recipient recipient = viewModel.getRecipient().get();
if (!recipient.equals(Recipient.UNKNOWN)) {
ApplicationDependencies.getSignalCallManager().denyCall();
AppDependencies.getSignalCallManager().denyCall();
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_ending_call));
@@ -729,7 +720,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void handleEndCall() {
Log.i(TAG, "Hangup pressed, handling termination now...");
ApplicationDependencies.getSignalCallManager().localHangup();
AppDependencies.getSignalCallManager().localHangup();
}
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
@@ -827,13 +818,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void updateGroupMembersForGroupCall() {
ApplicationDependencies.getSignalCallManager().requestUpdateGroupMembers();
AppDependencies.getSignalCallManager().requestUpdateGroupMembers();
}
public void handleGroupMemberCountChange(int count) {
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize();
boolean canRing = count <= RemoteConfig.maxGroupCallRingSize();
callScreen.enableRingGroup(canRing);
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
AppDependencies.getSignalCallManager().setRingGroup(canRing);
}
private void updateSpeakerHint(boolean showSpeakerHint) {
@@ -857,7 +848,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
if (state.getGroupCallState().isConnected()) {
ApplicationDependencies.getSignalCallManager().groupApproveSafetyChange(changedRecipients);
AppDependencies.getSignalCallManager().groupApproveSafetyChange(changedRecipients);
} else {
viewModel.startCall(state.getLocalParticipant().isVideoEnabled());
}
@@ -871,7 +862,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null && state.getGroupCallState().isNotIdle()) {
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
AppDependencies.getSignalCallManager().cancelPreJoin();
finish();
} else {
handleEndCall();
@@ -991,18 +982,97 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
callScreen.setRingGroup(event.shouldRingGroup());
if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) {
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
AppDependencies.getSignalCallManager().setRingGroup(false);
}
}
}
private boolean hasCameraPermission() {
return Permissions.hasAll(this, Manifest.permission.CAMERA);
}
private boolean hasAudioPermission() {
return Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO);
}
private void askCameraPermissions(@NonNull Runnable onGranted) {
if (!isAskingForPermission) {
isAskingForPermission = true;
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity__allow_access_camera), getString(R.string.WebRtcCallActivity__to_enable_video_allow_camera), false, R.drawable.symbol_video_24)
.onAnyResult(() -> isAskingForPermission = false)
.onAllGranted(() -> {
onGranted.run();
findViewById(R.id.missing_permissions_container).setVisibility(View.GONE);
})
.onAnyDenied(() -> Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_camera_access_enable_video, Toast.LENGTH_LONG).show())
.onAnyPermanentlyDenied(() -> showPermissionFragment(R.string.WebRtcCallActivity__allow_access_camera, R.string.WebRtcCallActivity__to_enable_video).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG))
.execute();
}
}
private void askAudioPermissions(@NonNull Runnable onGranted) {
if (!isAskingForPermission) {
isAskingForPermission = true;
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity__allow_access_microphone), getString(R.string.WebRtcCallActivity__to_start_call_microphone), false, R.drawable.ic_mic_24)
.onAnyResult(() -> isAskingForPermission = false)
.onAllGranted(onGranted)
.onAnyDenied(() -> {
Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_microphone_start_call, Toast.LENGTH_LONG).show();
handleDenyCall();
})
.onAnyPermanentlyDenied(() -> showPermissionFragment(R.string.WebRtcCallActivity__allow_access_microphone, R.string.WebRtcCallActivity__to_start_call).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG))
.execute();
}
}
public void askCameraAudioPermissions(@NonNull Runnable onGranted) {
if (!isAskingForPermission) {
isAskingForPermission = true;
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity__allow_access_camera_microphone), getString(R.string.WebRtcCallActivity__to_start_call_camera_microphone), false, R.drawable.ic_mic_24, R.drawable.symbol_video_24)
.onAnyResult(() -> isAskingForPermission = false)
.onSomePermanentlyDenied(deniedPermissions -> {
if (deniedPermissions.containsAll(List.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))) {
showPermissionFragment(R.string.WebRtcCallActivity__allow_access_camera_microphone, R.string.WebRtcCallActivity__to_start_call).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
} else if (deniedPermissions.contains(Manifest.permission.CAMERA)) {
showPermissionFragment(R.string.WebRtcCallActivity__allow_access_camera, R.string.WebRtcCallActivity__to_enable_video).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
} else {
showPermissionFragment(R.string.WebRtcCallActivity__allow_access_microphone, R.string.WebRtcCallActivity__to_start_call).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
})
.onAllGranted(onGranted)
.onSomeGranted(permissions -> {
if (permissions.contains(Manifest.permission.CAMERA)) {
findViewById(R.id.missing_permissions_container).setVisibility(View.GONE);
}
})
.onSomeDenied(deniedPermissions -> {
if (deniedPermissions.contains(Manifest.permission.RECORD_AUDIO)) {
Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_microphone_start_call, Toast.LENGTH_LONG).show();
handleDenyCall();
} else {
Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_camera_access_enable_video, Toast.LENGTH_LONG).show();
}
})
.execute();
}
}
private void startCall(boolean isVideoCall) {
enableVideoIfAvailable = isVideoCall;
if (isVideoCall) {
ApplicationDependencies.getSignalCallManager().startOutgoingVideoCall(viewModel.getRecipient().get());
AppDependencies.getSignalCallManager().startOutgoingVideoCall(viewModel.getRecipient().get());
} else {
ApplicationDependencies.getSignalCallManager().startOutgoingAudioCall(viewModel.getRecipient().get());
AppDependencies.getSignalCallManager().startOutgoingAudioCall(viewModel.getRecipient().get());
}
MessageSender.onMessageSent();
@@ -1013,7 +1083,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
ApplicationDependencies.getSignalCallManager().react(emoji);
AppDependencies.getSignalCallManager().react(emoji);
callOverflowPopupWindow.dismiss();
}
@@ -1032,11 +1102,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void toggleControls() {
WebRtcControls controlState = viewModel.getWebRtcControls().getValue();
if (controlState != null && !controlState.displayIncomingCallButtons()) {
if (controlState != null && !controlState.displayIncomingCallButtons() && !controlState.displayErrorControls()) {
controlsAndInfo.toggleControls();
}
}
@Override
public void onAudioPermissionsRequested(Runnable onGranted) {
askAudioPermissions(onGranted);
}
@Override
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
maybeDisplaySpeakerphonePopup(audioOutput);
@@ -1062,7 +1137,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput) {
maybeDisplaySpeakerphonePopup(audioOutput.getWebRtcAudioOutput());
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId()));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId()));
}
@Override
@@ -1072,9 +1147,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onMicChanged(boolean isMicEnabled) {
callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallStateUpdatePopupWindow.CallStateUpdate.MIC_ON
: CallStateUpdatePopupWindow.CallStateUpdate.MIC_OFF);
handleSetMuteAudio(!isMicEnabled);
Runnable onGranted = () -> {
callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallStateUpdatePopupWindow.CallStateUpdate.MIC_ON
: CallStateUpdatePopupWindow.CallStateUpdate.MIC_OFF);
handleSetMuteAudio(!isMicEnabled);
};
askAudioPermissions(onGranted);
}
@Override
@@ -1125,11 +1203,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed) {
if (ringingAllowed) {
ApplicationDependencies.getSignalCallManager().setRingGroup(ringGroup);
AppDependencies.getSignalCallManager().setRingGroup(ringGroup);
callStateUpdatePopupWindow.onCallStateUpdate(ringGroup ? CallStateUpdatePopupWindow.CallStateUpdate.RINGING_ON
: CallStateUpdatePopupWindow.CallStateUpdate.RINGING_OFF);
} else {
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
AppDependencies.getSignalCallManager().setRingGroup(false);
callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.RINGING_DISABLED);
}
}
@@ -1158,12 +1236,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onAllowPendingRecipient(@NonNull Recipient pendingRecipient) {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId());
AppDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId());
}
@Override
public void onRejectPendingRecipient(@NonNull Recipient pendingRecipient) {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId());
AppDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId());
}
@Override
@@ -1209,9 +1287,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onHidden() {
fullscreenHelper.hideSystemUI();
if (videoTooltip != null) {
videoTooltip.dismiss();
WebRtcControls controlState = viewModel.getWebRtcControls().getValue();
if (controlState == null || !controlState.displayErrorControls()) {
fullscreenHelper.hideSystemUI();
if (videoTooltip != null) {
videoTooltip.dismiss();
}
}
}
}

View File

@@ -21,6 +21,7 @@ class SignalBackupAgent : BackupAgent() {
)
override fun onBackup(oldState: ParcelFileDescriptor?, data: BackupDataOutput, newState: ParcelFileDescriptor) {
Log.i(TAG, "Performing backup to Android Backup Service.")
val contentsHash = cumulativeHashCode()
if (oldState == null) {
performBackup(data)
@@ -36,9 +37,11 @@ class SignalBackupAgent : BackupAgent() {
}
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(contentsHash) }
Log.i(TAG, "Backup finished.")
}
private fun performBackup(data: BackupDataOutput) {
Log.i(TAG, "Creating new backup data.")
items.forEach {
val backupData = it.getDataForBackup()
data.writeEntityHeader(it.getKey(), backupData.size)
@@ -54,7 +57,7 @@ class SignalBackupAgent : BackupAgent() {
items.find { dataInput.key == it.getKey() }?.restoreData(buffer)
}
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) }
Log.i(TAG, "Android Backup Service complete.")
Log.i(TAG, "Android Backup Service restore complete.")
}
private fun cumulativeHashCode(): Int {
@@ -62,6 +65,6 @@ class SignalBackupAgent : BackupAgent() {
}
companion object {
private const val TAG = "SignalBackupAgent"
private val TAG = Log.tag(SignalBackupAgent::class)
}
}

View File

@@ -17,19 +17,19 @@ object SvrAuthTokens : AndroidBackupItem {
}
override fun getDataForBackup(): ByteArray {
val proto = SvrAuthToken(tokens = SignalStore.svr().authTokenList)
val proto = SvrAuthToken(svr2Tokens = SignalStore.svr.svr2AuthTokens)
return proto.encode()
}
override fun restoreData(data: ByteArray) {
if (SignalStore.svr().authTokenList.isNotEmpty()) {
if (SignalStore.svr.svr2AuthTokens.isNotEmpty()) {
return
}
try {
val proto = SvrAuthToken.ADAPTER.decode(data)
SignalStore.svr().putAuthTokenList(proto.tokens)
SignalStore.svr.putSvr2AuthTokens(proto.svr2Tokens)
} catch (e: IOException) {
Log.w(TAG, "Cannot restore KbsAuthToken from backup service.")
}

View File

@@ -32,7 +32,7 @@ class ApkUpdateDownloadManagerReceiver : BroadcastReceiver() {
}
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2)
if (downloadId != SignalStore.apkUpdate().downloadId) {
if (downloadId != SignalStore.apkUpdate.downloadId) {
Log.w(TAG, "downloadId doesn't match the one we're waiting for! Ignoring.")
return
}

View File

@@ -14,7 +14,7 @@ import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.StreamUtil
import org.signal.core.util.getDownloadManager
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.ApkUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Environment
@@ -38,30 +38,30 @@ object ApkUpdateInstaller {
* [userInitiated] = true, and then everything installs.
*/
fun installOrPromptForInstall(context: Context, downloadId: Long, userInitiated: Boolean) {
if (downloadId != SignalStore.apkUpdate().downloadId) {
Log.w(TAG, "DownloadId doesn't match the one we're waiting for (current: $downloadId, expected: ${SignalStore.apkUpdate().downloadId})! We likely have newer data. Ignoring.")
if (downloadId != SignalStore.apkUpdate.downloadId) {
Log.w(TAG, "DownloadId doesn't match the one we're waiting for (current: $downloadId, expected: ${SignalStore.apkUpdate.downloadId})! We likely have newer data. Ignoring.")
ApkUpdateNotifications.dismissInstallPrompt(context)
ApplicationDependencies.getJobManager().add(ApkUpdateJob())
AppDependencies.jobManager.add(ApkUpdateJob())
return
}
val digest = SignalStore.apkUpdate().digest
val digest = SignalStore.apkUpdate.digest
if (digest == null) {
Log.w(TAG, "DownloadId matches, but digest is null! Inconsistent state. Failing and clearing state.")
SignalStore.apkUpdate().clearDownloadAttributes()
SignalStore.apkUpdate.clearDownloadAttributes()
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
return
}
if (!isMatchingDigest(context, downloadId, digest)) {
Log.w(TAG, "DownloadId matches, but digest does not! Bad download or inconsistent state. Failing and clearing state.")
SignalStore.apkUpdate().clearDownloadAttributes()
SignalStore.apkUpdate.clearDownloadAttributes()
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
return
}
if (!userInitiated && !shouldAutoUpdate()) {
Log.w(TAG, "Not user-initiated and not eligible for auto-update. Prompting. (API=${Build.VERSION.SDK_INT}, Foreground=${ApplicationDependencies.getAppForegroundObserver().isForegrounded}, AutoUpdate=${SignalStore.apkUpdate().autoUpdate})")
Log.w(TAG, "Not user-initiated and not eligible for auto-update. Prompting. (API=${Build.VERSION.SDK_INT}, Foreground=${AppDependencies.appForegroundObserver.isForegrounded}, AutoUpdate=${SignalStore.apkUpdate.autoUpdate})")
ApkUpdateNotifications.showInstallPrompt(context, downloadId)
return
}
@@ -70,11 +70,11 @@ object ApkUpdateInstaller {
installApk(context, downloadId, userInitiated)
} catch (e: IOException) {
Log.w(TAG, "Hit IOException when trying to install APK!", e)
SignalStore.apkUpdate().clearDownloadAttributes()
SignalStore.apkUpdate.clearDownloadAttributes()
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
} catch (e: SecurityException) {
Log.w(TAG, "Hit SecurityException when trying to install APK!", e)
SignalStore.apkUpdate().clearDownloadAttributes()
SignalStore.apkUpdate.clearDownloadAttributes()
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
}
}
@@ -145,6 +145,6 @@ object ApkUpdateInstaller {
private fun shouldAutoUpdate(): Boolean {
// TODO Auto-updates temporarily restricted to nightlies. Once we have designs for allowing users to opt-out of auto-updates, we can re-enable this
return Environment.IS_NIGHTLY && Build.VERSION.SDK_INT >= 31 && SignalStore.apkUpdate().autoUpdate && !ApplicationDependencies.getAppForegroundObserver().isForegrounded
return Environment.IS_NIGHTLY && Build.VERSION.SDK_INT >= 31 && SignalStore.apkUpdate.autoUpdate && !AppDependencies.appForegroundObserver.isForegrounded
}
}

View File

@@ -36,9 +36,9 @@ class ApkUpdatePackageInstallerReceiver : BroadcastReceiver() {
when (statusCode) {
PackageInstaller.STATUS_SUCCESS -> {
if (SignalStore.apkUpdate().lastApkUploadTime != SignalStore.apkUpdate().pendingApkUploadTime) {
Log.i(TAG, "Update installed successfully! Updating our lastApkUploadTime to ${SignalStore.apkUpdate().pendingApkUploadTime}")
SignalStore.apkUpdate().lastApkUploadTime = SignalStore.apkUpdate().pendingApkUploadTime
if (SignalStore.apkUpdate.lastApkUploadTime != SignalStore.apkUpdate.pendingApkUploadTime) {
Log.i(TAG, "Update installed successfully! Updating our lastApkUploadTime to ${SignalStore.apkUpdate.pendingApkUploadTime}")
SignalStore.apkUpdate.lastApkUploadTime = SignalStore.apkUpdate.pendingApkUploadTime
ApkUpdateNotifications.showAutoUpdateSuccess(context)
} else {
Log.i(TAG, "Spurious 'success' notification?")

View File

@@ -10,7 +10,7 @@ import android.content.Context;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.ApkUpdateJob;
import org.thoughtcrime.securesms.service.PersistentAlarmManagerListener;
import org.thoughtcrime.securesms.util.Environment;
@@ -35,7 +35,7 @@ public class ApkUpdateRefreshListener extends PersistentAlarmManagerListener {
if (scheduledTime != 0 && BuildConfig.MANAGES_APP_UPDATES) {
Log.i(TAG, "Queueing APK update job...");
ApplicationDependencies.getJobManager().add(new ApkUpdateJob());
AppDependencies.getJobManager().add(new ApkUpdateJob());
}
long newTime = System.currentTimeMillis() + INTERVAL;

View File

@@ -10,6 +10,8 @@ import android.os.Parcel
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.stickers.StickerLocator
import java.util.UUID
class ArchivedAttachment : Attachment {
@@ -44,8 +46,10 @@ class ArchivedAttachment : Attachment {
blurHash: String?,
voiceNote: Boolean,
borderless: Boolean,
stickerLocator: StickerLocator?,
gif: Boolean,
quote: Boolean
quote: Boolean,
uuid: UUID?
) : super(
contentType = contentType ?: "",
quote = quote,
@@ -66,10 +70,11 @@ class ArchivedAttachment : Attachment {
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
uploadTimestamp = 0,
caption = caption,
stickerLocator = null,
stickerLocator = stickerLocator,
blurHash = BlurHash.parseOrNull(blurHash),
audioHash = null,
transformProperties = null
transformProperties = null,
uuid = uuid
) {
this.archiveCdn = archiveCdn ?: Cdn.CDN_3.cdnNumber
this.archiveMediaName = archiveMediaName

View File

@@ -14,6 +14,8 @@ import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.ParcelUtil
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.UUID
/**
* Note: We have to use our own Parcelable implementation because we need to do custom stuff to preserve
@@ -65,12 +67,16 @@ abstract class Attachment(
@JvmField
val audioHash: AudioHash?,
@JvmField
val transformProperties: TransformProperties?
val transformProperties: TransformProperties?,
@JvmField
val uuid: UUID?
) : Parcelable {
abstract val uri: Uri?
abstract val publicUri: Uri?
abstract val thumbnailUri: Uri?
val displayUri: Uri?
get() = uri ?: thumbnailUri
protected constructor(parcel: Parcel) : this(
contentType = parcel.readString()!!,
@@ -95,7 +101,8 @@ abstract class Attachment(
stickerLocator = ParcelCompat.readParcelable(parcel, StickerLocator::class.java.classLoader, StickerLocator::class.java),
blurHash = ParcelCompat.readParcelable(parcel, BlurHash::class.java.classLoader, BlurHash::class.java),
audioHash = ParcelCompat.readParcelable(parcel, AudioHash::class.java.classLoader, AudioHash::class.java),
transformProperties = ParcelCompat.readParcelable(parcel, TransformProperties::class.java.classLoader, TransformProperties::class.java)
transformProperties = ParcelCompat.readParcelable(parcel, TransformProperties::class.java.classLoader, TransformProperties::class.java),
uuid = UuidUtil.parseOrNull(parcel.readString())
)
override fun writeToParcel(dest: Parcel, flags: Int) {
@@ -123,6 +130,7 @@ abstract class Attachment(
dest.writeParcelable(blurHash, 0)
dest.writeParcelable(audioHash, 0)
dest.writeParcelable(transformProperties, 0)
dest.writeString(uuid?.toString())
}
override fun describeContents(): Int {

View File

@@ -55,6 +55,7 @@ object AttachmentUploadUtil {
.withResumableUploadSpec(ResumableUploadSpec.from(uploadSpec))
.withCancelationSignal(cancellationSignal)
.withListener(progressListener)
.withUuid(attachment.uuid)
if (MediaUtil.isImageType(attachment.contentType)) {
builder.withBlurHash(getImageBlurHash(context, attachment))

View File

@@ -5,11 +5,12 @@ import android.os.Parcel
import androidx.core.os.ParcelCompat
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ParcelUtil
import java.util.UUID
class DatabaseAttachment : Attachment {
@@ -37,6 +38,9 @@ class DatabaseAttachment : Attachment {
@JvmField
val archiveMediaId: String?
@JvmField
val thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState
private val hasArchiveThumbnail: Boolean
private val hasThumbnail: Boolean
val displayOrder: Int
@@ -75,7 +79,9 @@ class DatabaseAttachment : Attachment {
archiveCdn: Int,
archiveThumbnailCdn: Int,
archiveMediaName: String?,
archiveMediaId: String?
archiveMediaId: String?,
thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState,
uuid: UUID?
) : super(
contentType = contentType!!,
transferState = transferProgress,
@@ -98,7 +104,8 @@ class DatabaseAttachment : Attachment {
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = audioHash,
transformProperties = transformProperties
transformProperties = transformProperties,
uuid = uuid
) {
this.attachmentId = attachmentId
this.mmsId = mmsId
@@ -111,6 +118,7 @@ class DatabaseAttachment : Attachment {
this.archiveThumbnailCdn = archiveThumbnailCdn
this.archiveMediaName = archiveMediaName
this.archiveMediaId = archiveMediaId
this.thumbnailRestoreState = thumbnailRestoreState
}
constructor(parcel: Parcel) : super(parcel) {
@@ -125,6 +133,7 @@ class DatabaseAttachment : Attachment {
archiveMediaName = parcel.readString()
archiveMediaId = parcel.readString()
hasArchiveThumbnail = ParcelUtil.readBoolean(parcel)
thumbnailRestoreState = AttachmentTable.ThumbnailRestoreState.deserialize(parcel.readInt())
}
override fun writeToParcel(dest: Parcel, flags: Int) {
@@ -140,10 +149,11 @@ class DatabaseAttachment : Attachment {
dest.writeString(archiveMediaName)
dest.writeString(archiveMediaId)
ParcelUtil.writeBoolean(dest, hasArchiveThumbnail)
dest.writeInt(thumbnailRestoreState.value)
}
override val uri: Uri?
get() = if (hasData || FeatureFlags.instantVideoPlayback() && getIncrementalDigest() != null) {
get() = if (hasData || getIncrementalDigest() != null) {
PartAuthority.getAttachmentDataUri(attachmentId)
} else {
null
@@ -165,7 +175,7 @@ class DatabaseAttachment : Attachment {
override fun equals(other: Any?): Boolean {
return other != null &&
other is DatabaseAttachment && other.attachmentId == attachmentId
other is DatabaseAttachment && other.attachmentId == attachmentId && other.uri == uri
}
override fun hashCode(): Int {

View File

@@ -12,6 +12,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil
import org.whispersystems.signalservice.internal.push.DataMessage
import java.util.Optional
import java.util.UUID
class PointerAttachment : Attachment {
@VisibleForTesting
@@ -35,7 +36,8 @@ class PointerAttachment : Attachment {
uploadTimestamp: Long,
caption: String?,
stickerLocator: StickerLocator?,
blurHash: BlurHash?
blurHash: BlurHash?,
uuid: UUID?
) : super(
contentType = contentType,
transferState = transferState,
@@ -59,7 +61,8 @@ class PointerAttachment : Attachment {
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = null,
transformProperties = null
transformProperties = null,
uuid = uuid
)
constructor(parcel: Parcel) : super(parcel)
@@ -115,7 +118,8 @@ class PointerAttachment : Attachment {
uploadTimestamp = pointer.get().asPointer().uploadTimestamp,
caption = pointer.get().asPointer().caption.orElse(null),
stickerLocator = stickerLocator,
blurHash = BlurHash.parseOrNull(pointer.get().asPointer().blurHash.orElse(null))
blurHash = BlurHash.parseOrNull(pointer.get().asPointer().blurHash.orElse(null)),
uuid = pointer.get().asPointer().uuid
)
)
}
@@ -152,7 +156,8 @@ class PointerAttachment : Attachment {
uploadTimestamp = thumbnail?.asPointer()?.uploadTimestamp ?: 0,
caption = thumbnail?.asPointer()?.caption?.orElse(null),
stickerLocator = null,
blurHash = null
blurHash = null,
uuid = thumbnail?.asPointer()?.uuid
)
)
}

View File

@@ -4,6 +4,7 @@ import android.net.Uri
import android.os.Parcel
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
import java.util.UUID
/**
* An attachment that represents where an attachment used to be. Useful when you need to know that
@@ -35,7 +36,8 @@ class TombstoneAttachment : Attachment {
stickerLocator = null,
blurHash = null,
audioHash = null,
transformProperties = null
transformProperties = null,
uuid = null
)
constructor(
@@ -49,7 +51,8 @@ class TombstoneAttachment : Attachment {
voiceNote: Boolean = false,
borderless: Boolean = false,
gif: Boolean = false,
quote: Boolean
quote: Boolean,
uuid: UUID?
) : super(
contentType = contentType ?: "",
quote = quote,
@@ -73,7 +76,8 @@ class TombstoneAttachment : Attachment {
stickerLocator = null,
blurHash = BlurHash.parseOrNull(blurHash),
audioHash = null,
transformProperties = null
transformProperties = null,
uuid = uuid
)
constructor(parcel: Parcel) : super(parcel)

View File

@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.stickers.StickerLocator
import java.util.Objects
import java.util.UUID
class UriAttachment : Attachment {
@@ -46,6 +47,7 @@ class UriAttachment : Attachment {
transformProperties = transformProperties
)
@JvmOverloads
constructor(
dataUri: Uri,
contentType: String,
@@ -63,7 +65,8 @@ class UriAttachment : Attachment {
stickerLocator: StickerLocator?,
blurHash: BlurHash?,
audioHash: AudioHash?,
transformProperties: TransformProperties?
transformProperties: TransformProperties?,
uuid: UUID? = UUID.randomUUID()
) : super(
contentType = contentType,
transferState = transferState,
@@ -87,7 +90,8 @@ class UriAttachment : Attachment {
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = audioHash,
transformProperties = transformProperties
transformProperties = transformProperties,
uuid = uuid
) {
uri = Objects.requireNonNull(dataUri)
}

View File

@@ -14,7 +14,7 @@ import androidx.annotation.RequiresApi
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioHandler
internal const val TAG = "BluetoothVoiceNoteUtil"
@@ -34,7 +34,7 @@ sealed interface BluetoothVoiceNoteUtil {
@RequiresApi(31)
private class BluetoothVoiceNoteUtil31(val listener: (Boolean) -> Unit) : BluetoothVoiceNoteUtil {
override fun connectBluetoothScoConnection() {
val audioManager = ApplicationDependencies.getAndroidCallAudioManager()
val audioManager = AppDependencies.androidCallAudioManager
val device: AudioDeviceInfo? = audioManager.connectedBluetoothDevice
if (device != null) {
val result: Boolean = audioManager.setCommunicationDevice(device)
@@ -53,7 +53,7 @@ private class BluetoothVoiceNoteUtil31(val listener: (Boolean) -> Unit) : Blueto
override fun disconnectBluetoothScoConnection() {
Log.d(TAG, "Clearing call manager communication device.")
ApplicationDependencies.getAndroidCallAudioManager().clearCommunicationDevice()
AppDependencies.androidCallAudioManager.clearCommunicationDevice()
}
override fun destroy() = Unit

View File

@@ -16,7 +16,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioHandler
import java.util.concurrent.TimeUnit
@@ -49,7 +49,7 @@ class SignalBluetoothManager(
private var bluetoothHeadset: BluetoothHeadset? = null
private var scoConnectionAttempts = 0
private val androidAudioManager = ApplicationDependencies.getAndroidCallAudioManager()
private val androidAudioManager = AppDependencies.androidCallAudioManager
private val bluetoothListener = BluetoothServiceListener()
private var bluetoothReceiver: BluetoothHeadsetBroadcastReceiver? = null

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.avatar.fallback
import androidx.annotation.DrawableRes
import androidx.annotation.Px
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.util.NameUtil
/**
* Specifies what kind of avatar should be generated for a given recipient.
*/
sealed interface FallbackAvatar {
val color: AvatarColor
/**
* Transparent avatar
*/
data object Transparent : FallbackAvatar {
override val color: AvatarColor = AvatarColor.UNKNOWN
}
/**
* Generated avatars utilize the initials of the given recipient
*/
data class Text(val content: String, override val color: AvatarColor) : FallbackAvatar {
init {
check(content.isNotEmpty())
}
}
/**
* Fallback avatars that are backed by resources.
*/
sealed interface Resource : FallbackAvatar {
@DrawableRes
fun getIconBySize(size: Size): Int
/**
* Local user
*/
data class Local(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_note_compact_16
Size.MEDIUM -> R.drawable.symbol_note_24
Size.LARGE -> R.drawable.symbol_note_display_bold_40
}
}
}
/**
* Individual user without a display name.
*/
data class Person(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_person_compact_16
Size.MEDIUM -> R.drawable.symbol_person_24
Size.LARGE -> R.drawable.symbol_person_display_bold_40
}
}
}
/**
* A group
*/
data class Group(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_group_compact_16
Size.MEDIUM -> R.drawable.symbol_group_24
Size.LARGE -> R.drawable.symbol_group_display_bold_40
}
}
}
/**
* Story distribution lists
*/
data class DistributionList(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_stories_compact_16
Size.MEDIUM -> R.drawable.symbol_stories_24
Size.LARGE -> R.drawable.symbol_stories_display_bold_40
}
}
}
/**
* Call Links
*/
data class CallLink(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_video_compact_16
Size.MEDIUM -> R.drawable.symbol_video_24
Size.LARGE -> R.drawable.symbol_video_display_bold_40
}
}
}
}
enum class Size {
/**
* Smaller than 32dp
*/
SMALL,
/**
* 32dp and larger
*/
MEDIUM,
/**
* 80dp and larger
*/
LARGE
}
companion object {
const val ICON_TO_BACKGROUND_SCALE = 0.625
@JvmStatic
@JvmOverloads
fun forTextOrDefault(text: String, avatarColor: AvatarColor, default: FallbackAvatar = Resource.Person(avatarColor)): FallbackAvatar {
val abbreviation = NameUtil.getAbbreviation(text)
return if (abbreviation != null) {
Text(abbreviation, avatarColor)
} else {
default
}
}
fun getSizeByPx(@Px px: Int): Size {
return getSizeByDp(DimensionUnit.PIXELS.toDp(px.toFloat()).dp)
}
fun getSizeByDp(dp: Dp): Size {
val rawDp = dp.value
return when {
rawDp >= 80.0 -> Size.LARGE
rawDp < 32.0 -> Size.SMALL
else -> Size.MEDIUM
}
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.avatar.fallback
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.airbnb.lottie.SimpleColorFilter
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.RelativeCornerSize
import com.google.android.material.shape.RoundedCornerTreatment
import com.google.android.material.shape.ShapeAppearanceModel
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.avatar.TextAvatarDrawable
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
class FallbackAvatarDrawable(
private val context: Context,
private val fallbackAvatar: FallbackAvatar
) : MaterialShapeDrawable() {
private val avatarColorPair: AvatarColorPair = AvatarColorPair.create(context, fallbackAvatar.color)
private var avatarSize: FallbackAvatar.Size = FallbackAvatar.Size.SMALL
private var icon: Drawable? = null
init {
fillColor = ColorStateList.valueOf(avatarColorPair.backgroundColor)
}
fun circleCrop(): FallbackAvatarDrawable {
shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(RoundedCornerTreatment())
.setAllCornerSizes(RelativeCornerSize(0.5f))
.build()
return this
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
avatarSize = FallbackAvatar.getSizeByPx(bounds.width())
icon = when (fallbackAvatar) {
is FallbackAvatar.Resource -> {
val resourceIcon = ContextCompat.getDrawable(context, fallbackAvatar.getIconBySize(avatarSize))!!
val iconBounds = Rect(bounds)
iconBounds.inset(
((bounds.width() - (bounds.width() * FallbackAvatar.ICON_TO_BACKGROUND_SCALE)) / 2f).toInt(),
((bounds.height() - (bounds.height() * FallbackAvatar.ICON_TO_BACKGROUND_SCALE)) / 2f).toInt()
)
resourceIcon.bounds = iconBounds
resourceIcon
}
is FallbackAvatar.Text -> TextAvatarDrawable(
context = context,
avatar = Avatar.Text(
fallbackAvatar.content,
Avatars.ColorPair(avatarColorPair.backgroundColor, avatarColorPair.foregroundColor, ""),
Avatar.DatabaseId.DoNotPersist
),
size = bounds.width()
)
FallbackAvatar.Transparent -> null
}
icon?.alpha = alpha
icon?.colorFilter = SimpleColorFilter(avatarColorPair.foregroundColor)
}
override fun draw(canvas: Canvas) {
if (icon == null) return
super.draw(canvas)
icon?.draw(canvas)
}
override fun setAlpha(alpha: Int) {
super.setAlpha(alpha)
icon?.alpha = alpha
invalidateSelf()
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.avatar.fallback
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
@Composable
fun FallbackAvatarImage(
fallbackAvatar: FallbackAvatar,
modifier: Modifier = Modifier,
shape: Shape = CircleShape
) {
if (fallbackAvatar is FallbackAvatar.Transparent) {
Box(modifier = modifier)
return
}
val context = LocalContext.current
val colorPair = remember(fallbackAvatar) {
AvatarColorPair.create(context, fallbackAvatar.color)
}
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = modifier
.background(Color(colorPair.backgroundColor), shape)
) {
when (fallbackAvatar) {
is FallbackAvatar.Resource -> {
val size = remember(maxWidth) {
FallbackAvatar.getSizeByDp(maxWidth)
}
val padding = remember(maxWidth) {
((maxWidth.value - (maxWidth.value * FallbackAvatar.ICON_TO_BACKGROUND_SCALE)) / 2).dp
}
Icon(
painter = painterResource(fallbackAvatar.getIconBySize(size)),
contentDescription = null,
tint = Color(colorPair.foregroundColor),
modifier = Modifier
.fillMaxSize()
.padding(padding)
)
}
is FallbackAvatar.Text -> {
val size = DimensionUnit.DP.toPixels(maxWidth.value) * 0.8f
val textSize = DimensionUnit.PIXELS.toDp(Avatars.getTextSizeForLength(context, fallbackAvatar.content, size, size))
// TODO [alex] -- Handle emoji
Text(
text = fallbackAvatar.content,
color = Color(colorPair.foregroundColor),
fontSize = TextUnit(textSize, TextUnitType.Sp),
fontFamily = FontFamily(AvatarRenderer.getTypeface(context))
)
}
FallbackAvatar.Transparent -> {}
}
}
}
@SignalPreview
@Composable
fun FallbackAvatarImagePreview() {
Previews.Preview {
Column {
Text(text = "Compose - Large")
FallbackAvatarImage(
fallbackAvatar = FallbackAvatar.Text("AE", AvatarColor.A100),
modifier = Modifier.size(160.dp)
)
Text(text = "Compose - Medium")
FallbackAvatarImage(
fallbackAvatar = FallbackAvatar.Text("AE", AvatarColor.A100),
modifier = Modifier.size(64.dp)
)
Text(text = "Compose - Small")
FallbackAvatarImage(
fallbackAvatar = FallbackAvatar.Text("AE", AvatarColor.A100),
modifier = Modifier.size(24.dp)
)
}
}
}

View File

@@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.groups.ParcelableGroupId
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -243,18 +242,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
@Suppress("DEPRECATION")
private fun openGallery() {
Permissions.with(this)
.request(*PermissionCompat.forImages())
.ifNecessary()
.onAllGranted {
val intent = AvatarSelectionActivity.getIntentForGallery(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
}
.onAnyDenied {
Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__viewing_your_gallery_requires_the_storage_permission, Toast.LENGTH_SHORT)
.show()
}
.execute()
val intent = AvatarSelectionActivity.getIntentForGallery(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
}
@Deprecated("Deprecated in Java")

View File

@@ -87,8 +87,8 @@ class AvatarView @JvmOverloads constructor(
avatar.setRecipient(recipient)
}
fun setFallbackPhotoProvider(fallbackPhotoProvider: Recipient.FallbackPhotoProvider) {
avatar.setFallbackPhotoProvider(fallbackPhotoProvider)
fun setFallbackAvatarProvider(fallbackAvatarProvider: AvatarImageView.FallbackAvatarProvider?) {
avatar.setFallbackAvatarProvider(fallbackAvatarProvider)
}
fun disableQuickContact() {

View File

@@ -28,7 +28,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
import org.thoughtcrime.securesms.restore.restorelocalbackup.PassphraseAsYouTypeFormatter;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -170,7 +170,7 @@ public class BackupDialog {
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setEnabled(false);
RestoreBackupFragment.PassphraseAsYouTypeFormatter formatter = new RestoreBackupFragment.PassphraseAsYouTypeFormatter();
PassphraseAsYouTypeFormatter formatter = new PassphraseAsYouTypeFormatter();
prompt.addTextChangedListener(new AfterTextChanged(editable -> {
formatter.afterTextChanged(editable);

View File

@@ -45,7 +45,7 @@ import org.thoughtcrime.securesms.database.SessionTable;
import org.thoughtcrime.securesms.database.SignedPreKeyTable;
import org.thoughtcrime.securesms.database.StickerTable;
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
@@ -235,7 +235,7 @@ public class FullBackupExporter extends FullBackupBase {
count += TextSecurePreferences.getPreferencesToSaveToBackupCount(context);
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication())
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(AppDependencies.getApplication())
.getDataSet();
for (String key : SignalStore.getKeysToIncludeInBackup()) {
if (dataSet.containsKey(key)) {
@@ -536,7 +536,7 @@ public class FullBackupExporter extends FullBackupBase {
long estimatedCount,
BackupCancellationSignal cancellationSignal) throws IOException
{
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication())
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(AppDependencies.getApplication())
.getDataSet();
for (String key : keysToIncludeInBackup) {

View File

@@ -31,13 +31,12 @@ import org.thoughtcrime.securesms.database.EmojiSearchTable;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.SearchTable;
import org.thoughtcrime.securesms.database.StickerTable;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayOutputStream;
@@ -99,7 +98,7 @@ public class FullBackupImporter extends FullBackupBase {
{
int count = 0;
SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).getSqlCipherDatabase();
SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(AppDependencies.getApplication()).getSqlCipherDatabase();
db.setForeignKeyConstraintsEnabled(false);
db.beginTransaction();
@@ -288,7 +287,7 @@ public class FullBackupImporter extends FullBackupBase {
return;
}
KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).writeDataSet(dataSet, Collections.emptyList());
KeyValueDatabase.getInstance(AppDependencies.getApplication()).writeDataSet(dataSet, Collections.emptyList());
}
@SuppressLint("ApplySharedPref")

View File

@@ -5,20 +5,28 @@
package org.thoughtcrime.securesms.backup.v2
import androidx.annotation.WorkerThread
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.Base64
import org.signal.core.util.EventTimer
import org.signal.core.util.fullWalCheckpoint
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.core.util.withinTransaction
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
import org.signal.libsignal.messagebackup.MessageBackupKey
import org.signal.libsignal.protocol.ServiceId.Aci
import org.signal.libsignal.zkgroup.backups.BackupLevel
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getThumbnailMediaName
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
@@ -26,18 +34,29 @@ import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.StickerBackupProcessor
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.toMillis
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.StatusCodeErrorAction
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
@@ -53,84 +72,181 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.Pro
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.math.BigDecimal
import java.time.ZonedDateTime
import java.util.Currency
import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds
object BackupRepository {
private val TAG = Log.tag(BackupRepository::class.java)
private const val VERSION = 1L
private const val MAIN_DB_SNAPSHOT_NAME = "signal-snapshot.db"
private const val KEYVALUE_DB_SNAPSHOT_NAME = "key-value-snapshot.db"
private val resetInitializedStateErrorAction: StatusCodeErrorAction = { error ->
when (error.code) {
401 -> {
Log.i(TAG, "Resetting initialized state due to 401.")
SignalStore.backup().backupsInitialized = false
SignalStore.backup.backupsInitialized = false
}
403 -> {
Log.i(TAG, "Bad auth credential. Clearing stored credentials.")
SignalStore.backup().clearAllCredentials()
SignalStore.backup.clearAllCredentials()
}
}
}
@WorkerThread
fun turnOffAndDeleteBackup() {
RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
SignalStore.backup.areBackupsEnabled = false
SignalStore.backup.backupTier = null
}
private fun createSignalDatabaseSnapshot(): SignalDatabase {
// Need to do a WAL checkpoint to ensure that the database file we're copying has all pending writes
if (!SignalDatabase.rawDatabase.fullWalCheckpoint()) {
Log.w(TAG, "Failed to checkpoint WAL for main database! Not guaranteed to be using the most recent data.")
}
// We make a copy of the database within a transaction to ensure that no writes occur while we're copying the file
return SignalDatabase.rawDatabase.withinTransaction {
val context = AppDependencies.application
val existingDbFile = context.getDatabasePath(SignalDatabase.DATABASE_NAME)
val targetFile = File(existingDbFile.parentFile, MAIN_DB_SNAPSHOT_NAME)
try {
existingDbFile.copyTo(targetFile, overwrite = true)
} catch (e: IOException) {
// TODO [backup] Gracefully handle this error
throw IllegalStateException("Failed to copy database file!", e)
}
SignalDatabase(
context = context,
databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context),
attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
name = MAIN_DB_SNAPSHOT_NAME
)
}
}
private fun createSignalStoreSnapshot(): SignalStore {
val context = AppDependencies.application
// Need to do a WAL checkpoint to ensure that the database file we're copying has all pending writes
if (!KeyValueDatabase.getInstance(context).writableDatabase.fullWalCheckpoint()) {
Log.w(TAG, "Failed to checkpoint WAL for KeyValueDatabase! Not guaranteed to be using the most recent data.")
}
// We make a copy of the database within a transaction to ensure that no writes occur while we're copying the file
return KeyValueDatabase.getInstance(context).writableDatabase.withinTransaction {
val existingDbFile = context.getDatabasePath(KeyValueDatabase.DATABASE_NAME)
val targetFile = File(existingDbFile.parentFile, KEYVALUE_DB_SNAPSHOT_NAME)
try {
existingDbFile.copyTo(targetFile, overwrite = true)
} catch (e: IOException) {
// TODO [backup] Gracefully handle this error
throw IllegalStateException("Failed to copy database file!", e)
}
val db = KeyValueDatabase.createWithName(context, KEYVALUE_DB_SNAPSHOT_NAME)
SignalStore(KeyValueStore(db))
}
}
private fun deleteDatabaseSnapshot() {
val targetFile = AppDependencies.application.getDatabasePath(MAIN_DB_SNAPSHOT_NAME)
if (!targetFile.delete()) {
Log.w(TAG, "Failed to delete main database snapshot!")
}
}
private fun deleteSignalStoreSnapshot() {
val targetFile = AppDependencies.application.getDatabasePath(KEYVALUE_DB_SNAPSHOT_NAME)
if (!targetFile.delete()) {
Log.w(TAG, "Failed to delete key value database snapshot!")
}
}
fun export(outputStream: OutputStream, append: (ByteArray) -> Unit, plaintext: Boolean = false) {
val eventTimer = EventTimer()
val writer: BackupExportWriter = if (plaintext) {
PlainTextBackupWriter(outputStream)
} else {
EncryptedBackupWriter(
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
aci = SignalStore.account().aci!!,
outputStream = outputStream,
append = append
)
}
val dbSnapshot: SignalDatabase = createSignalDatabaseSnapshot()
val signalStoreSnapshot: SignalStore = createSignalStoreSnapshot()
val exportState = ExportState(backupTime = System.currentTimeMillis(), allowMediaBackup = true)
writer.use {
writer.write(
BackupInfo(
version = VERSION,
backupTimeMs = exportState.backupTime
try {
val writer: BackupExportWriter = if (plaintext) {
PlainTextBackupWriter(outputStream)
} else {
EncryptedBackupWriter(
key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
aci = SignalStore.account.aci!!,
outputStream = outputStream,
append = append
)
)
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
// writes from other threads are blocked. This is something to think more about.
SignalDatabase.rawDatabase.withinTransaction {
AccountDataProcessor.export {
writer.write(it)
eventTimer.emit("account")
}
}
RecipientBackupProcessor.export(exportState) {
writer.write(it)
eventTimer.emit("recipient")
}
val exportState = ExportState(backupTime = System.currentTimeMillis(), allowMediaBackup = SignalStore.backup.backsUpMedia)
ChatBackupProcessor.export(exportState) { frame ->
writer.write(frame)
eventTimer.emit("thread")
}
writer.use {
writer.write(
BackupInfo(
version = VERSION,
backupTimeMs = exportState.backupTime
)
)
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
// writes from other threads are blocked. This is something to think more about.
dbSnapshot.rawWritableDatabase.withinTransaction {
AccountDataProcessor.export(dbSnapshot, signalStoreSnapshot) {
writer.write(it)
eventTimer.emit("account")
}
AdHocCallBackupProcessor.export { frame ->
writer.write(frame)
eventTimer.emit("call")
}
RecipientBackupProcessor.export(dbSnapshot, signalStoreSnapshot, exportState) {
writer.write(it)
eventTimer.emit("recipient")
}
ChatItemBackupProcessor.export(exportState) { frame ->
writer.write(frame)
eventTimer.emit("message")
ChatBackupProcessor.export(dbSnapshot, exportState) { frame ->
writer.write(frame)
eventTimer.emit("thread")
}
AdHocCallBackupProcessor.export(dbSnapshot) { frame ->
writer.write(frame)
eventTimer.emit("call")
}
StickerBackupProcessor.export(dbSnapshot) { frame ->
writer.write(frame)
eventTimer.emit("sticker-pack")
}
ChatItemBackupProcessor.export(dbSnapshot, exportState) { frame ->
writer.write(frame)
eventTimer.emit("message")
}
}
}
}
Log.d(TAG, "export() ${eventTimer.stop().summary}")
Log.d(TAG, "export() ${eventTimer.stop().summary}")
} finally {
deleteDatabaseSnapshot()
deleteSignalStoreSnapshot()
}
}
fun export(plaintext: Boolean = false): ByteArray {
@@ -140,7 +256,7 @@ object BackupRepository {
}
fun validate(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData): ValidationResult {
val masterKey = SignalStore.svr().getOrCreateMasterKey()
val masterKey = SignalStore.svr.getOrCreateMasterKey()
val key = MessageBackupKey(masterKey.serialize(), Aci.parseFromBinary(selfData.aci.toByteArray()))
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, inputStreamFactory, length)
@@ -149,15 +265,15 @@ object BackupRepository {
fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false) {
val eventTimer = EventTimer()
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val frameReader = if (plaintext) {
PlainTextBackupReader(inputStreamFactory())
PlainTextBackupReader(inputStreamFactory(), length)
} else {
EncryptedBackupReader(
key = backupKey,
aci = selfData.aci,
streamLength = length,
length = length,
dataStream = inputStreamFactory
)
}
@@ -180,16 +296,21 @@ object BackupRepository {
SignalDatabase.threads.clearAllDataForBackupRestore()
SignalDatabase.messages.clearAllDataForBackupRestore()
SignalDatabase.attachments.clearAllDataForBackupRestore()
SignalDatabase.stickers.clearAllDataForBackupRestore()
// Add back self after clearing data
val selfId: RecipientId = SignalDatabase.recipients.getAndPossiblyMerge(selfData.aci, selfData.pni, selfData.e164, pniVerified = true, changeSelf = true)
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
SignalDatabase.recipients.setProfileSharing(selfId, true)
// Add back my story after clearing data
DistributionListTables.insertInitialDistributionListAtCreationTime(it)
eventTimer.emit("setup")
val backupState = BackupState(backupKey)
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
val totalLength = frameReader.getStreamLength()
for (frame in frameReader) {
when {
frame.account != null -> {
@@ -212,6 +333,11 @@ object BackupRepository {
eventTimer.emit("call")
}
frame.stickerPack != null -> {
StickerBackupProcessor.import(frame.stickerPack)
eventTimer.emit("sticker-pack")
}
frame.chatItem != null -> {
chatItemInserter.insert(frame.chatItem)
eventTimer.emit("chatItem")
@@ -220,6 +346,7 @@ object BackupRepository {
else -> Log.w(TAG, "Unrecognized frame")
}
EventBus.getDefault().post(RestoreV2Event(RestoreV2Event.Type.PROGRESS_RESTORE, frameReader.getBytesRead(), totalLength))
}
if (chatItemInserter.flush()) {
@@ -235,7 +362,7 @@ object BackupRepository {
while (groups.hasNext()) {
val group = groups.next()
if (group.id.isV2) {
ApplicationDependencies.getJobManager().add(RequestGroupV2InfoJob(group.id as GroupId.V2))
AppDependencies.jobManager.add(RequestGroupV2InfoJob(group.id as GroupId.V2))
}
}
@@ -243,8 +370,8 @@ object BackupRepository {
}
fun listRemoteMediaObjects(limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -253,8 +380,8 @@ object BackupRepository {
}
fun getRemoteBackupUsedSpace(): NetworkResult<Long?> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -263,12 +390,27 @@ object BackupRepository {
}
}
private fun getBackupTier(): NetworkResult<MessageBackupTier> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.map { credential ->
val zkCredential = api.getZkCredential(backupKey, credential)
if (zkCredential.backupLevel == BackupLevel.MEDIA) {
MessageBackupTier.PAID
} else {
MessageBackupTier.FREE
}
}
}
/**
* Returns an object with details about the remote backup state.
*/
fun getRemoteBackupState(): NetworkResult<BackupMetadata> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -294,8 +436,8 @@ object BackupRepository {
* @return True if successful, otherwise false.
*/
fun uploadBackupFile(backupStream: InputStream, backupStreamLength: Long): Boolean {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -316,8 +458,8 @@ object BackupRepository {
}
fun downloadBackupFile(destination: File, listener: ProgressListener? = null): Boolean {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -326,17 +468,35 @@ object BackupRepository {
.then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
.map { pair ->
val (cdnCredentials, info) = pair
val messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver()
val messageReceiver = AppDependencies.signalServiceMessageReceiver
messageReceiver.retrieveBackup(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}", destination, listener)
} is NetworkResult.Success
}
fun getBackupFileLastModified(): NetworkResult<ZonedDateTime?> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.getBackupInfo(backupKey, credential)
}
.then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
.then { pair ->
val (cdnCredentials, info) = pair
val messageReceiver = AppDependencies.signalServiceMessageReceiver
NetworkResult.fromFetch {
messageReceiver.getCdnLastModifiedTime(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}")
}
}
}
/**
* Returns an object with details about the remote backup state.
*/
fun debugGetArchivedMediaState(): NetworkResult<List<ArchiveGetMediaItemsResponse.StoredMediaObject>> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -348,8 +508,8 @@ object BackupRepository {
* Retrieves an upload spec that can be used to upload attachment media.
*/
fun getMediaUploadSpec(secretKey: ByteArray? = null): NetworkResult<ResumableUploadSpec> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -361,8 +521,8 @@ object BackupRepository {
}
fun archiveThumbnail(thumbnailAttachment: Attachment, parentAttachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val request = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey)
return initBackupAndFetchAuth(backupKey)
@@ -376,8 +536,8 @@ object BackupRepository {
}
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -399,8 +559,8 @@ object BackupRepository {
}
fun archiveMedia(databaseAttachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResult> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -439,8 +599,8 @@ object BackupRepository {
}
fun deleteArchivedMedia(attachments: List<DatabaseAttachment>): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val mediaToDelete = attachments
.filter { it.archiveMediaId != null }
@@ -471,8 +631,8 @@ object BackupRepository {
}
fun deleteAbandonedMediaObjects(mediaObjects: Collection<ArchivedMediaObject>): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
val mediaToDelete = mediaObjects
.map {
@@ -499,8 +659,8 @@ object BackupRepository {
}
fun debugDeleteAllArchivedMedia(): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return debugGetArchivedMediaState()
.then { archivedMedia ->
@@ -536,13 +696,13 @@ object BackupRepository {
* Retrieve credentials for reading from the backup cdn.
*/
fun getCdnReadCredentials(cdnNumber: Int): NetworkResult<GetArchiveCdnCredentialsResponse> {
val cached = SignalStore.backup().cdnReadCredentials
val cached = SignalStore.backup.cdnReadCredentials
if (cached != null) {
return NetworkResult.Success(cached)
}
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
@@ -554,20 +714,41 @@ object BackupRepository {
}
.also {
if (it is NetworkResult.Success) {
SignalStore.backup().cdnReadCredentials = it.result
SignalStore.backup.cdnReadCredentials = it.result
}
}
.also { Log.i(TAG, "getCdnReadCredentialsResult: $it") }
}
fun restoreBackupTier(): MessageBackupTier? {
// TODO: more complete error handling
try {
val lastModified = getBackupFileLastModified().successOrThrow()
if (lastModified != null) {
SignalStore.backup.lastBackupTime = lastModified.toMillis()
}
} catch (e: Exception) {
Log.i(TAG, "Could not check for backup file.", e)
SignalStore.backup.backupTier = null
return null
}
SignalStore.backup.backupTier = try {
getBackupTier().successOrThrow()
} catch (e: Exception) {
Log.i(TAG, "Could not retrieve backup tier.", e)
null
}
return SignalStore.backup.backupTier
}
/**
* Retrieves backupDir and mediaDir, preferring cached value if available.
*
* These will only ever change if the backup expires.
*/
fun getCdnBackupDirectories(): NetworkResult<BackupDirectories> {
val cachedBackupDirectory = SignalStore.backup().cachedBackupDirectory
val cachedBackupMediaDirectory = SignalStore.backup().cachedBackupMediaDirectory
val cachedBackupDirectory = SignalStore.backup.cachedBackupDirectory
val cachedBackupMediaDirectory = SignalStore.backup.cachedBackupMediaDirectory
if (cachedBackupDirectory != null && cachedBackupMediaDirectory != null) {
return NetworkResult.Success(
@@ -578,39 +759,115 @@ object BackupRepository {
)
}
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.getBackupInfo(backupKey, credential).map {
SignalStore.backup().usedBackupMediaSpace = it.usedSpace ?: 0L
SignalStore.backup.usedBackupMediaSpace = it.usedSpace ?: 0L
BackupDirectories(it.backupDir!!, it.mediaDir!!)
}
}
.also {
if (it is NetworkResult.Success) {
SignalStore.backup().cachedBackupDirectory = it.result.backupDir
SignalStore.backup().cachedBackupMediaDirectory = it.result.mediaDir
SignalStore.backup.cachedBackupDirectory = it.result.backupDir
SignalStore.backup.cachedBackupMediaDirectory = it.result.mediaDir
}
}
}
suspend fun getAvailableBackupsTypes(availableBackupTiers: List<MessageBackupTier>): List<MessageBackupsType> {
return availableBackupTiers.map { getBackupsType(it) }
}
suspend fun getBackupsType(tier: MessageBackupTier): MessageBackupsType {
val backupCurrency = SignalStore.donations.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
return when (tier) {
MessageBackupTier.FREE -> getFreeType(backupCurrency)
MessageBackupTier.PAID -> getPaidType(backupCurrency)
}
}
private fun getFreeType(currency: Currency): MessageBackupsType {
return MessageBackupsType(
tier = MessageBackupTier.FREE,
pricePerMonth = FiatMoney(BigDecimal.ZERO, currency),
title = "Text + 30 days of media", // TODO [message-backups] Finalize text (does this come from server?)
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Last 30 days of media" // TODO [message-backups] Finalize text (does this come from server?)
)
)
)
}
private suspend fun getPaidType(currency: Currency): MessageBackupsType {
val serviceResponse = withContext(Dispatchers.IO) {
AppDependencies
.donationsService
.getDonationsConfiguration(Locale.getDefault())
}
if (serviceResponse.result.isEmpty) {
if (serviceResponse.applicationError.isPresent) {
throw serviceResponse.applicationError.get()
}
if (serviceResponse.executionError.isPresent) {
throw serviceResponse.executionError.get()
}
error("Unhandled error occurred while downloading configuration.")
}
val config = serviceResponse.result.get()
return MessageBackupsType(
tier = MessageBackupTier.PAID,
pricePerMonth = FiatMoney(config.currencies[currency.currencyCode.lowercase()]!!.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]!!, currency),
title = "Text + All your media", // TODO [message-backups] Finalize text (does this come from server?)
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup" // TODO [message-backups] Finalize text (does this come from server?)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)" // TODO [message-backups] Finalize text (does this come from server?)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!" // TODO [message-backups] Finalize text (does this come from server?)
)
)
)
}
/**
* Ensures that the backupId has been reserved and that your public key has been set, while also returning an auth credential.
* Should be the basis of all backup operations.
*/
private fun initBackupAndFetchAuth(backupKey: BackupKey): NetworkResult<ArchiveServiceCredential> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val api = AppDependencies.signalServiceAccountManager.archiveApi
return if (SignalStore.backup().backupsInitialized) {
return if (SignalStore.backup.backupsInitialized) {
getAuthCredential().runOnStatusCodeError(resetInitializedStateErrorAction)
} else {
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential -> api.setPublicKey(backupKey, credential).map { credential } }
.runIfSuccessful { SignalStore.backup().backupsInitialized = true }
.runIfSuccessful { SignalStore.backup.backupsInitialized = true }
.runOnStatusCodeError(resetInitializedStateErrorAction)
}
}
@@ -621,7 +878,7 @@ object BackupRepository {
private fun getAuthCredential(): NetworkResult<ArchiveServiceCredential> {
val currentTime = System.currentTimeMillis()
val credential = SignalStore.backup().credentialsByDay.getForCurrentTime(currentTime.milliseconds)
val credential = SignalStore.backup.credentialsByDay.getForCurrentTime(currentTime.milliseconds)
if (credential != null) {
return NetworkResult.Success(credential)
@@ -629,10 +886,10 @@ object BackupRepository {
Log.w(TAG, "No credentials found for today, need to fetch new ones! This shouldn't happen under normal circumstances. We should ensure the routine fetch is running properly.")
return ApplicationDependencies.getSignalServiceAccountManager().archiveApi.getServiceCredentials(currentTime).map { result ->
SignalStore.backup().addCredentials(result.credentials.toList())
SignalStore.backup().clearCredentialsOlderThan(currentTime)
SignalStore.backup().credentialsByDay.getForCurrentTime(currentTime.milliseconds)!!
return AppDependencies.signalServiceAccountManager.archiveApi.getServiceCredentials(currentTime).map { result ->
SignalStore.backup.addCredentials(result.credentials.toList())
SignalStore.backup.clearCredentialsOlderThan(currentTime)
SignalStore.backup.credentialsByDay.getForCurrentTime(currentTime.milliseconds)!!
}
}
@@ -688,8 +945,3 @@ class BackupMetadata(
val usedSpace: Long,
val mediaCount: Long
)
enum class MessageBackupTier {
FREE,
PAID
}

View File

@@ -11,7 +11,8 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.RestoreAttachmentThumbnailJob
/**
* Responsible for managing logic around restore prioritization
@@ -28,17 +29,24 @@ object BackupRestoreManager {
fun prioritizeAttachmentsIfNeeded(messageRecords: List<MessageRecord>) {
SignalExecutors.BOUNDED.execute {
synchronized(this) {
val restoringAttachments: List<AttachmentId> = messageRecords
val restoringAttachments = messageRecords
.mapNotNull { (it as? MmsMessageRecord?)?.slideDeck?.slides }
.flatten()
.mapNotNull { it.asAttachment() as? DatabaseAttachment }
.filter { it.transferState == AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS && !reprioritizedAttachments.contains(it.attachmentId) }
.map { it.attachmentId }
.filter {
val needThumbnail = it.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.NEEDS_RESTORE && it.transferState == AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS
(needThumbnail || it.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.IN_PROGRESS) && !reprioritizedAttachments.contains(it.attachmentId)
}
.map { it.attachmentId to it.mmsId }
.toSet()
reprioritizedAttachments += restoringAttachments
if (restoringAttachments.isNotEmpty()) {
RestoreAttachmentJob.modifyPriorities(restoringAttachments.toSet(), 1)
reprioritizedAttachments += restoringAttachments.map { it.first }
val thumbnailJobs = restoringAttachments.map {
val (attachmentId, mmsId) = it
RestoreAttachmentThumbnailJob(attachmentId = attachmentId, messageId = mmsId, highPriority = true)
}
if (thumbnailJobs.isNotEmpty()) {
AppDependencies.jobManager.addAll(thumbnailJobs)
}
}
}

View File

@@ -7,6 +7,8 @@ package org.thoughtcrime.securesms.backup.v2
class BackupV2Event(val type: Type, val count: Long, val estimatedTotalCount: Long) {
enum class Type {
PROGRESS_MESSAGES, PROGRESS_ATTACHMENTS, FINISHED
PROGRESS_MESSAGES,
PROGRESS_ATTACHMENTS,
FINISHED
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.signal.core.util.LongSerializer
/**
* Serializable enum value for what we think a user's current backup tier is.
*
* We should not trust the stored value on its own, we should also verify it
* against what the server knows, but it is a useful flag that helps avoid a
* network call in some cases.
*/
enum class MessageBackupTier(val value: Int) {
FREE(0),
PAID(1);
companion object Serializer : LongSerializer<MessageBackupTier?> {
override fun serialize(data: MessageBackupTier?): Long {
return data?.value?.toLong() ?: -1
}
override fun deserialize(data: Long): MessageBackupTier? {
return entries.firstOrNull { it.value == data.toInt() }
}
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
class RestoreV2Event(val type: Type, val count: Long, val estimatedTotalCount: Long) {
enum class Type {
PROGRESS_DOWNLOAD,
PROGRESS_RESTORE,
PROGRESS_MEDIA_RESTORE,
FINISHED
}
fun getProgress(): Float {
if (estimatedTotalCount == 0L) {
return 0f
}
return count.toFloat() / estimatedTotalCount.toFloat()
}
}

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.select
import org.signal.ringrtc.CallLinkRootKey
@@ -63,8 +64,8 @@ class BackupCallLinkIterator(private val cursor: Cursor) : Iterator<BackupRecipi
return BackupRecipient(
id = callLink.recipientId.toLong(),
callLink = CallLink(
rootKey = callLink.credentials!!.linkKeyBytes.toByteString(),
adminKey = callLink.credentials.adminPassBytes?.toByteString(),
rootKey = callLink.credentials?.linkKeyBytes?.toByteString() ?: ByteString.EMPTY,
adminKey = callLink.credentials?.adminPassBytes?.toByteString(),
name = callLink.state.name,
expirationMs = callLink.state.expiration.toEpochMilli(),
restrictions = callLink.state.restrictions.toBackup()

View File

@@ -38,7 +38,7 @@ fun CallTable.restoreCallLogFromBackup(call: AdHocCall, backupState: BackupState
CallTable.TYPE to CallTable.Type.serialize(CallTable.Type.AD_HOC_CALL),
CallTable.DIRECTION to CallTable.Direction.serialize(CallTable.Direction.OUTGOING),
CallTable.EVENT to CallTable.Event.serialize(event),
CallTable.TIMESTAMP to call.startedCallTimestamp
CallTable.TIMESTAMP to call.callTimestamp
)
writableDatabase.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
@@ -64,7 +64,7 @@ class CallLogIterator(private val cursor: Cursor) : Iterator<AdHocCall?>, Closea
callId = callId,
recipientId = cursor.requireLong(CallTable.PEER),
state = AdHocCall.State.GENERIC,
startedCallTimestamp = cursor.requireLong(CallTable.TIMESTAMP)
callTimestamp = cursor.requireLong(CallTable.TIMESTAMP)
)
}

View File

@@ -8,13 +8,13 @@ package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.Base64.decode
import org.signal.core.util.Base64.decodeOrThrow
import org.signal.core.util.Hex
import org.signal.core.util.logging.Log
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireLongOrNull
import org.signal.core.util.requireString
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
@@ -25,7 +25,9 @@ import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.LearnedProfileChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Quote
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
@@ -34,6 +36,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
import org.thoughtcrime.securesms.backup.v2.proto.SessionSwitchoverChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.backup.v2.proto.Sticker
import org.thoughtcrime.securesms.backup.v2.proto.StickerMessage
import org.thoughtcrime.securesms.backup.v2.proto.Text
import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate
import org.thoughtcrime.securesms.database.AttachmentTable
@@ -41,8 +45,10 @@ import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.PaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
@@ -51,21 +57,28 @@ import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.payments.FailureReason
import org.thoughtcrime.securesms.payments.State
import org.thoughtcrime.securesms.payments.proto.PaymentMetaData
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.util.toByteArray
import java.io.Closeable
import java.io.IOException
import java.util.HashMap
import java.util.LinkedList
import java.util.Queue
import kotlin.jvm.optionals.getOrNull
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
/**
* An iterator for chat items with a clever performance twist: rather than do the extra queries one at a time (for reactions,
@@ -74,7 +87,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
*
* All of this complexity is hidden from the user -- they just get a normal iterator interface.
*/
class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int, private val archiveMedia: Boolean) : Iterator<ChatItem>, Closeable {
class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int, private val archiveMedia: Boolean) : Iterator<ChatItem?>, Closeable {
companion object {
private val TAG = Log.tag(ChatItemExportIterator::class.java)
@@ -88,11 +101,13 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
*/
private val buffer: Queue<ChatItem> = LinkedList()
private val revisionMap: HashMap<Long, ArrayList<ChatItem>> = HashMap()
override fun hasNext(): Boolean {
return buffer.isNotEmpty() || (cursor.count > 0 && !cursor.isLast && !cursor.isAfterLast)
}
override fun next(): ChatItem {
override fun next(): ChatItem? {
if (buffer.isNotEmpty()) {
return buffer.remove()
}
@@ -117,219 +132,106 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
val builder = record.toBasicChatItemBuilder(groupReceiptsById[id])
when {
record.remoteDeleted -> builder.remoteDeletedMessage = RemoteDeletedMessage()
MessageTypes.isJoinedType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.JOINED_SIGNAL))
MessageTypes.isIdentityUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_UPDATE))
MessageTypes.isIdentityVerified(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_VERIFIED))
MessageTypes.isIdentityDefault(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_DEFAULT))
record.remoteDeleted -> {
builder.remoteDeletedMessage = RemoteDeletedMessage()
}
MessageTypes.isJoinedType(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.JOINED_SIGNAL)
}
MessageTypes.isIdentityUpdate(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.IDENTITY_UPDATE)
}
MessageTypes.isIdentityVerified(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.IDENTITY_VERIFIED)
}
MessageTypes.isIdentityDefault(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.IDENTITY_DEFAULT)
}
MessageTypes.isChangeNumber(record.type) -> {
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHANGE_NUMBER))
builder.sms = false
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.CHANGE_NUMBER)
}
MessageTypes.isBoostRequest(record.type) -> {
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BOOST_REQUEST))
builder.sms = false
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.BOOST_REQUEST)
}
MessageTypes.isEndSessionType(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.END_SESSION)
}
MessageTypes.isChatSessionRefresh(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.CHAT_SESSION_REFRESH)
}
MessageTypes.isBadDecryptType(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.BAD_DECRYPT)
}
MessageTypes.isPaymentsActivated(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.PAYMENTS_ACTIVATED)
}
MessageTypes.isPaymentsRequestToActivate(record.type) -> {
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST)
}
MessageTypes.isEndSessionType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.END_SESSION))
MessageTypes.isChatSessionRefresh(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHAT_SESSION_REFRESH))
MessageTypes.isBadDecryptType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BAD_DECRYPT))
MessageTypes.isPaymentsActivated(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENTS_ACTIVATED))
MessageTypes.isPaymentsRequestToActivate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST))
MessageTypes.isExpirationTimerUpdate(record.type) -> {
builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate(record.expiresIn.toInt()))
builder.expiresInMs = 0
}
MessageTypes.isProfileChange(record.type) -> {
if (record.body == null) continue
builder.updateMessage = ChatUpdateMessage(
profileChange = try {
val decoded: ByteArray = Base64.decode(record.body!!)
val profileChangeDetails = ProfileChangeDetails.ADAPTER.decode(decoded)
if (profileChangeDetails.profileNameChange != null) {
ProfileChangeChatUpdate(previousName = profileChangeDetails.profileNameChange.previous, newName = profileChangeDetails.profileNameChange.newValue)
} else {
ProfileChangeChatUpdate()
}
} catch (e: IOException) {
Log.w(TAG, "Profile name change details could not be read", e)
ProfileChangeChatUpdate()
}
)
builder.sms = false
builder.updateMessage = record.toProfileChangeUpdate()
}
MessageTypes.isSessionSwitchoverType(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
sessionSwitchover = try {
val event = SessionSwitchoverEvent.ADAPTER.decode(decodeOrThrow(record.body!!))
SessionSwitchoverChatUpdate(event.e164.e164ToLong()!!)
} catch (e: Exception) {
SessionSwitchoverChatUpdate()
}
)
builder.updateMessage = record.toSessionSwitchoverUpdate()
}
MessageTypes.isThreadMergeType(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
threadMerge = try {
val event = ThreadMergeEvent.ADAPTER.decode(decodeOrThrow(record.body!!))
ThreadMergeChatUpdate(event.previousE164.e164ToLong()!!)
} catch (e: Exception) {
ThreadMergeChatUpdate()
}
)
builder.updateMessage = record.toThreadMergeUpdate()
}
MessageTypes.isGroupV2(record.type) && MessageTypes.isGroupUpdate(record.type) -> {
val groupChange = record.messageExtras?.gv2UpdateDescription?.groupChangeUpdate
if (groupChange != null) {
builder.updateMessage = ChatUpdateMessage(
groupChange = groupChange
)
} else if (record.body != null) {
try {
val decoded: ByteArray = decode(record.body)
val context = DecryptedGroupV2Context.ADAPTER.decode(decoded)
builder.updateMessage = ChatUpdateMessage(
groupChange = GroupsV2UpdateMessageConverter.translateDecryptedChange(selfIds = SignalStore.account().getServiceIds(), context)
)
} catch (e: IOException) {
continue
}
} else {
continue
}
builder.updateMessage = record.toGroupUpdate()
}
MessageTypes.isCallLog(record.type) -> {
builder.sms = false
val call = calls.getCallByMessageId(record.id)
if (call != null) {
if (call.type == CallTable.Type.GROUP_CALL) {
builder.updateMessage = ChatUpdateMessage(
groupCall = GroupCall(
callId = record.id,
state = when (call.event) {
CallTable.Event.MISSED -> GroupCall.State.MISSED
CallTable.Event.ONGOING -> GroupCall.State.GENERIC
CallTable.Event.ACCEPTED -> GroupCall.State.ACCEPTED
CallTable.Event.NOT_ACCEPTED -> GroupCall.State.GENERIC
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> GroupCall.State.MISSED_NOTIFICATION_PROFILE
CallTable.Event.DELETE -> continue
CallTable.Event.GENERIC_GROUP_CALL -> GroupCall.State.GENERIC
CallTable.Event.JOINED -> GroupCall.State.JOINED
CallTable.Event.RINGING -> GroupCall.State.RINGING
CallTable.Event.DECLINED -> GroupCall.State.DECLINED
CallTable.Event.OUTGOING_RING -> GroupCall.State.OUTGOING_RING
},
ringerRecipientId = call.ringerRecipient?.toLong(),
startedCallAci = if (call.ringerRecipient != null) SignalDatabase.recipients.getRecord(call.ringerRecipient).aci?.toByteString() else null,
startedCallTimestamp = call.timestamp
)
)
} else if (call.type != CallTable.Type.AD_HOC_CALL) {
builder.updateMessage = ChatUpdateMessage(
individualCall = IndividualCall(
callId = call.callId,
type = if (call.type == CallTable.Type.VIDEO_CALL) IndividualCall.Type.VIDEO_CALL else IndividualCall.Type.AUDIO_CALL,
direction = if (call.direction == CallTable.Direction.INCOMING) IndividualCall.Direction.INCOMING else IndividualCall.Direction.OUTGOING,
state = when (call.event) {
CallTable.Event.MISSED -> IndividualCall.State.MISSED
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> IndividualCall.State.MISSED_NOTIFICATION_PROFILE
CallTable.Event.ACCEPTED -> IndividualCall.State.ACCEPTED
CallTable.Event.NOT_ACCEPTED -> IndividualCall.State.NOT_ACCEPTED
else -> IndividualCall.State.UNKNOWN_STATE
},
startedCallTimestamp = call.timestamp
)
)
} else {
continue
}
builder.updateMessage = record.toCallUpdate()
}
MessageTypes.isPaymentsNotification(record.type) -> {
builder.paymentNotification = record.toPaymentNotificationUpdate()
}
MessageTypes.isGiftBadge(record.type) -> {
builder.giftBadge = record.toGiftBadgeUpdate()
}
else -> {
if (record.body == null && !attachmentsById.containsKey(record.id)) {
Log.w(TAG, "Record with ID ${record.id} missing a body and doesn't have attachments. Skipping.")
continue
}
val attachments = attachmentsById[record.id]
val sticker = attachments?.firstOrNull { dbAttachment ->
dbAttachment.isSticker
}
if (sticker?.stickerLocator != null) {
builder.stickerMessage = sticker.toStickerMessage(reactionsById[id])
} else {
when {
MessageTypes.isMissedAudioCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.AUDIO_CALL,
state = IndividualCall.State.MISSED,
direction = IndividualCall.Direction.INCOMING
)
)
}
MessageTypes.isMissedVideoCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.VIDEO_CALL,
state = IndividualCall.State.MISSED,
direction = IndividualCall.Direction.INCOMING
)
)
}
MessageTypes.isIncomingAudioCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.AUDIO_CALL,
state = IndividualCall.State.ACCEPTED,
direction = IndividualCall.Direction.INCOMING
)
)
}
MessageTypes.isIncomingVideoCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.VIDEO_CALL,
state = IndividualCall.State.ACCEPTED,
direction = IndividualCall.Direction.INCOMING
)
)
}
MessageTypes.isOutgoingAudioCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.AUDIO_CALL,
state = IndividualCall.State.ACCEPTED,
direction = IndividualCall.Direction.OUTGOING
)
)
}
MessageTypes.isOutgoingVideoCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.VIDEO_CALL,
state = IndividualCall.State.ACCEPTED,
direction = IndividualCall.Direction.OUTGOING
)
)
}
MessageTypes.isGroupCall(record.type) -> {
try {
val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body)
builder.updateMessage = ChatUpdateMessage(
groupCall = GroupCall(
state = GroupCall.State.GENERIC,
startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(),
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp
)
)
} catch (exception: java.lang.Exception) {
continue
}
}
}
builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id])
}
}
record.body == null && !attachmentsById.containsKey(record.id) -> {
Log.w(TAG, "Record missing a body and doesnt have attachments, skipping")
continue
}
else -> builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id])
}
buffer += builder.build()
if (record.latestRevisionId == null) {
val previousEdits = revisionMap.remove(record.id)
if (previousEdits != null) {
builder.revisions = previousEdits
}
buffer += builder.build()
} else {
var previousEdits = revisionMap[record.latestRevisionId]
if (previousEdits == null) {
previousEdits = ArrayList()
revisionMap[record.latestRevisionId] = previousEdits
}
previousEdits += builder.build()
}
}
return if (buffer.isNotEmpty()) {
buffer.remove()
} else {
throw NoSuchElementException()
null
}
}
@@ -337,14 +239,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
cursor.close()
}
private fun String.e164ToLong(): Long? {
val fixed = if (this.startsWith("+")) {
this.substring(1)
} else {
this
}
return fixed.toLongOrNull()
private fun simpleUpdate(type: SimpleChatUpdate.Type): ChatUpdateMessage {
return ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = type))
}
private fun BackupMessageRecord.toBasicChatItemBuilder(groupReceipts: List<GroupReceiptTable.GroupReceiptInfo>?): ChatItem.Builder {
@@ -357,7 +253,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
expireStartDate = if (record.expireStarted > 0) record.expireStarted else 0
expiresInMs = if (record.expiresIn > 0) record.expiresIn else 0
revisions = emptyList()
sms = !MessageTypes.isSecureType(record.type)
sms = record.type.isSmsType()
if (MessageTypes.isCallLog(record.type)) {
directionless = ChatItem.DirectionlessMessageDetails()
} else if (MessageTypes.isOutgoingMessageType(record.type)) {
@@ -375,6 +271,220 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
private fun BackupMessageRecord.toProfileChangeUpdate(): ChatUpdateMessage? {
val profileChangeDetails = if (this.messageExtras != null) {
this.messageExtras.profileChangeDetails
} else {
Base64.decodeOrNull(this.body)?.let { ProfileChangeDetails.ADAPTER.decode(it) }
}
return if (profileChangeDetails?.profileNameChange != null) {
ChatUpdateMessage(profileChange = ProfileChangeChatUpdate(previousName = profileChangeDetails.profileNameChange.previous, newName = profileChangeDetails.profileNameChange.newValue))
} else if (profileChangeDetails?.learnedProfileName != null) {
ChatUpdateMessage(learnedProfileChange = LearnedProfileChatUpdate(e164 = profileChangeDetails.learnedProfileName.e164?.e164ToLong(), username = profileChangeDetails.learnedProfileName.username))
} else {
null
}
}
private fun BackupMessageRecord.toSessionSwitchoverUpdate(): ChatUpdateMessage {
if (this.body == null) {
return ChatUpdateMessage(sessionSwitchover = SessionSwitchoverChatUpdate())
}
return ChatUpdateMessage(
sessionSwitchover = try {
val event = SessionSwitchoverEvent.ADAPTER.decode(Base64.decodeOrThrow(this.body))
SessionSwitchoverChatUpdate(event.e164.e164ToLong()!!)
} catch (e: IOException) {
SessionSwitchoverChatUpdate()
}
)
}
private fun BackupMessageRecord.toThreadMergeUpdate(): ChatUpdateMessage {
if (this.body == null) {
return ChatUpdateMessage(threadMerge = ThreadMergeChatUpdate())
}
return ChatUpdateMessage(
threadMerge = try {
val event = ThreadMergeEvent.ADAPTER.decode(Base64.decodeOrThrow(this.body))
ThreadMergeChatUpdate(event.previousE164.e164ToLong()!!)
} catch (e: IOException) {
ThreadMergeChatUpdate()
}
)
}
private fun BackupMessageRecord.toGroupUpdate(): ChatUpdateMessage? {
val groupChange = this.messageExtras?.gv2UpdateDescription?.groupChangeUpdate
return if (groupChange != null) {
ChatUpdateMessage(
groupChange = groupChange
)
} else if (this.body != null) {
try {
val decoded: ByteArray = Base64.decode(this.body)
val context = DecryptedGroupV2Context.ADAPTER.decode(decoded)
ChatUpdateMessage(
groupChange = GroupsV2UpdateMessageConverter.translateDecryptedChange(selfIds = SignalStore.account.getServiceIds(), context)
)
} catch (e: IOException) {
null
}
} else {
null
}
}
private fun BackupMessageRecord.toCallUpdate(): ChatUpdateMessage? {
val call = calls.getCallByMessageId(this.id)
return if (call != null) {
call.toCallUpdate()
} else {
when {
MessageTypes.isMissedAudioCall(this.type) -> {
ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.AUDIO_CALL,
state = IndividualCall.State.MISSED,
direction = IndividualCall.Direction.INCOMING
)
)
}
MessageTypes.isMissedVideoCall(this.type) -> {
ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.VIDEO_CALL,
state = IndividualCall.State.MISSED,
direction = IndividualCall.Direction.INCOMING
)
)
}
MessageTypes.isIncomingAudioCall(this.type) -> {
ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.AUDIO_CALL,
state = IndividualCall.State.ACCEPTED,
direction = IndividualCall.Direction.INCOMING
)
)
}
MessageTypes.isIncomingVideoCall(this.type) -> {
ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.VIDEO_CALL,
state = IndividualCall.State.ACCEPTED,
direction = IndividualCall.Direction.INCOMING
)
)
}
MessageTypes.isOutgoingAudioCall(this.type) -> {
ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.AUDIO_CALL,
state = IndividualCall.State.ACCEPTED,
direction = IndividualCall.Direction.OUTGOING
)
)
}
MessageTypes.isOutgoingVideoCall(this.type) -> {
ChatUpdateMessage(
individualCall = IndividualCall(
type = IndividualCall.Type.VIDEO_CALL,
state = IndividualCall.State.ACCEPTED,
direction = IndividualCall.Direction.OUTGOING
)
)
}
MessageTypes.isGroupCall(this.type) -> {
try {
val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(this.body)
ChatUpdateMessage(
groupCall = GroupCall(
state = GroupCall.State.GENERIC,
startedCallRecipientId = recipients.getByAci(ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid))).getOrNull()?.toLong(),
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp
)
)
} catch (exception: IOException) {
null
}
}
else -> {
null
}
}
}
}
private fun CallTable.Call.toCallUpdate(): ChatUpdateMessage? {
return if (this.type == CallTable.Type.GROUP_CALL) {
ChatUpdateMessage(
groupCall = GroupCall(
callId = this.messageId,
state = when (this.event) {
CallTable.Event.MISSED -> GroupCall.State.MISSED
CallTable.Event.ONGOING -> GroupCall.State.GENERIC
CallTable.Event.ACCEPTED -> GroupCall.State.ACCEPTED
CallTable.Event.NOT_ACCEPTED -> GroupCall.State.GENERIC
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> GroupCall.State.MISSED_NOTIFICATION_PROFILE
CallTable.Event.GENERIC_GROUP_CALL -> GroupCall.State.GENERIC
CallTable.Event.JOINED -> GroupCall.State.JOINED
CallTable.Event.RINGING -> GroupCall.State.RINGING
CallTable.Event.DECLINED -> GroupCall.State.DECLINED
CallTable.Event.OUTGOING_RING -> GroupCall.State.OUTGOING_RING
CallTable.Event.DELETE -> return null
},
ringerRecipientId = this.ringerRecipient?.toLong(),
startedCallRecipientId = this.ringerRecipient?.toLong(),
startedCallTimestamp = this.timestamp
)
)
} else if (this.type != CallTable.Type.AD_HOC_CALL) {
ChatUpdateMessage(
individualCall = IndividualCall(
callId = this.callId,
type = if (this.type == CallTable.Type.VIDEO_CALL) IndividualCall.Type.VIDEO_CALL else IndividualCall.Type.AUDIO_CALL,
direction = if (this.direction == CallTable.Direction.INCOMING) IndividualCall.Direction.INCOMING else IndividualCall.Direction.OUTGOING,
state = when (this.event) {
CallTable.Event.MISSED -> IndividualCall.State.MISSED
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> IndividualCall.State.MISSED_NOTIFICATION_PROFILE
CallTable.Event.ACCEPTED -> IndividualCall.State.ACCEPTED
CallTable.Event.NOT_ACCEPTED -> IndividualCall.State.NOT_ACCEPTED
else -> IndividualCall.State.UNKNOWN_STATE
},
startedCallTimestamp = this.timestamp
)
)
} else {
null
}
}
private fun BackupMessageRecord.toPaymentNotificationUpdate(): PaymentNotification {
val paymentUuid = UuidUtil.parseOrNull(this.body)
val payment = if (paymentUuid != null) {
SignalDatabase.payments.getPayment(paymentUuid)
} else {
null
}
return if (payment == null) {
PaymentNotification()
} else {
PaymentNotification(
amountMob = payment.amount.serializeAmountString(),
feeMob = payment.fee.serializeAmountString(),
note = payment.note,
transactionDetails = payment.getTransactionDetails()
)
}
}
private fun BackupMessageRecord.toStandardMessage(reactionRecords: List<ReactionRecord>?, mentions: List<Mention>?, attachments: List<DatabaseAttachment>?): StandardMessage {
val text = if (body == null) {
null
@@ -390,7 +500,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
quote = this.toQuote(quotedAttachments),
text = text,
attachments = messageAttachments.toBackupAttachments(),
// TODO Link previews!
// TODO [backup] Link previews!
linkPreview = emptyList(),
longText = null,
reactions = reactionRecords.toBackupReactions()
@@ -416,6 +526,39 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
private fun BackupMessageRecord.toGiftBadgeUpdate(): BackupGiftBadge {
val giftBadge = try {
GiftBadge.ADAPTER.decode(Base64.decode(this.body ?: ""))
} catch (e: IOException) {
Log.w(TAG, "Failed to decode GiftBadge!")
return BackupGiftBadge()
}
return BackupGiftBadge(
receiptCredentialPresentation = giftBadge.redemptionToken,
state = when (giftBadge.redemptionState) {
GiftBadge.RedemptionState.REDEEMED -> BackupGiftBadge.State.REDEEMED
GiftBadge.RedemptionState.FAILED -> BackupGiftBadge.State.FAILED
GiftBadge.RedemptionState.PENDING -> BackupGiftBadge.State.UNOPENED
GiftBadge.RedemptionState.STARTED -> BackupGiftBadge.State.OPENED
}
)
}
private fun DatabaseAttachment.toStickerMessage(reactions: List<ReactionRecord>?): StickerMessage {
val stickerLocator = this.stickerLocator!!
return StickerMessage(
sticker = Sticker(
packId = Hex.fromStringCondensed(stickerLocator.packId).toByteString(),
packKey = Hex.fromStringCondensed(stickerLocator.packKey).toByteString(),
stickerId = stickerLocator.stickerId,
emoji = stickerLocator.emoji,
data_ = this.toBackupAttachment().pointer
),
reactions = reactions.toBackupReactions()
)
}
private fun List<DatabaseAttachment>.toBackupQuoteAttachments(): List<Quote.QuotedAttachment> {
return this.map { attachment ->
Quote.QuotedAttachment(
@@ -444,7 +587,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
builder.backupLocator = FilePointer.BackupLocator(
mediaName = archiveMediaName ?: this.getMediaName().toString(),
cdnNumber = if (archiveMediaName != null) archiveCdn else Cdn.CDN_3.cdnNumber, // TODO (clark): Update when new proto with optional cdn is landed
key = decode(remoteKey).toByteString(),
key = Base64.decode(remoteKey).toByteString(),
size = this.size.toInt(),
digest = remoteDigest.toByteString()
)
@@ -456,7 +599,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
cdnKey = this.remoteLocation,
cdnNumber = this.cdn.cdnNumber,
uploadTimestamp = this.uploadTimestamp,
key = decode(remoteKey).toByteString(),
key = Base64.decode(remoteKey).toByteString(),
size = this.size.toInt(),
digest = remoteDigest.toByteString()
)
@@ -466,7 +609,16 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
return MessageAttachment(
pointer = builder.build(),
wasDownloaded = this.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || this.transferState == AttachmentTable.TRANSFER_NEEDS_RESTORE,
flag = if (voiceNote) MessageAttachment.Flag.VOICE_MESSAGE else if (videoGif) MessageAttachment.Flag.GIF else if (borderless) MessageAttachment.Flag.BORDERLESS else MessageAttachment.Flag.NONE
flag = if (voiceNote) {
MessageAttachment.Flag.VOICE_MESSAGE
} else if (videoGif) {
MessageAttachment.Flag.GIF
} else if (borderless) {
MessageAttachment.Flag.BORDERLESS
} else {
MessageAttachment.Flag.NONE
},
clientUuid = uuid?.let { UuidUtil.toByteString(uuid) }
)
}
@@ -476,6 +628,46 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
private fun PaymentTable.PaymentTransaction.getTransactionDetails(): PaymentNotification.TransactionDetails? {
if (failureReason != null || state == State.FAILED) {
return PaymentNotification.TransactionDetails(failedTransaction = PaymentNotification.TransactionDetails.FailedTransaction(reason = failureReason.toBackupFailureReason()))
}
return PaymentNotification.TransactionDetails(
transaction = PaymentNotification.TransactionDetails.Transaction(
status = this.state.toBackupState(),
timestamp = timestamp,
blockIndex = blockIndex,
blockTimestamp = blockTimestamp,
mobileCoinIdentification = paymentMetaData.mobileCoinTxoIdentification?.toBackup()
)
)
}
private fun PaymentMetaData.MobileCoinTxoIdentification.toBackup(): PaymentNotification.TransactionDetails.MobileCoinTxoIdentification {
return PaymentNotification.TransactionDetails.MobileCoinTxoIdentification(
publicKey = this.publicKey,
keyImages = this.keyImages
)
}
private fun State.toBackupState(): PaymentNotification.TransactionDetails.Transaction.Status {
return when (this) {
State.INITIAL -> PaymentNotification.TransactionDetails.Transaction.Status.INITIAL
State.SUBMITTED -> PaymentNotification.TransactionDetails.Transaction.Status.SUBMITTED
State.SUCCESSFUL -> PaymentNotification.TransactionDetails.Transaction.Status.SUCCESSFUL
State.FAILED -> throw IllegalArgumentException("state cannot be failed")
}
}
private fun FailureReason?.toBackupFailureReason(): PaymentNotification.TransactionDetails.FailedTransaction.FailureReason {
return when (this) {
FailureReason.UNKNOWN -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.GENERIC
FailureReason.INSUFFICIENT_FUNDS -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.INSUFFICIENT_FUNDS
FailureReason.NETWORK -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.NETWORK
else -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.GENERIC
}
}
private fun List<Mention>.toBackupBodyRanges(): List<BackupBodyRange> {
return this.map {
BackupBodyRange(
@@ -615,6 +807,28 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
private fun Long.isSmsType(): Boolean {
if (MessageTypes.isSecureType(this)) {
return false
}
if (MessageTypes.isCallLog(this)) {
return false
}
return MessageTypes.isOutgoingMessageType(this) || MessageTypes.isInboxType(this)
}
private fun String.e164ToLong(): Long? {
val fixed = if (this.startsWith("+")) {
this.substring(1)
} else {
this
}
return fixed.toLongOrNull()
}
private fun Cursor.toBackupMessageRecord(): BackupMessageRecord {
return BackupMessageRecord(
id = this.requireLong(MessageTable.ID),
@@ -637,8 +851,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
quoteMissing = this.requireBoolean(MessageTable.QUOTE_MISSING),
quoteBodyRanges = this.requireBlob(MessageTable.QUOTE_BODY_RANGES),
quoteType = this.requireInt(MessageTable.QUOTE_TYPE),
originalMessageId = this.requireLong(MessageTable.ORIGINAL_MESSAGE_ID),
latestRevisionId = this.requireLong(MessageTable.LATEST_REVISION_ID),
originalMessageId = this.requireLongOrNull(MessageTable.ORIGINAL_MESSAGE_ID),
latestRevisionId = this.requireLongOrNull(MessageTable.LATEST_REVISION_ID),
hasDeliveryReceipt = this.requireBoolean(MessageTable.HAS_DELIVERY_RECEIPT),
viewed = this.requireBoolean(MessageTable.VIEWED_COLUMN),
hasReadReceipt = this.requireBoolean(MessageTable.HAS_READ_RECEIPT),
@@ -672,8 +886,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
val quoteMissing: Boolean,
val quoteBodyRanges: ByteArray?,
val quoteType: Int,
val originalMessageId: Long,
val latestRevisionId: Long,
val originalMessageId: Long?,
val latestRevisionId: Long?,
val hasDeliveryReceipt: Boolean,
val hasReadReceipt: Boolean,
val viewed: Boolean,

View File

@@ -7,7 +7,9 @@ package org.thoughtcrime.securesms.backup.v2.database
import android.content.ContentValues
import androidx.core.content.contentValuesOf
import okio.ByteString
import org.signal.core.util.Base64
import org.signal.core.util.Hex
import org.signal.core.util.SqlUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
@@ -22,14 +24,17 @@ import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification
import org.thoughtcrime.securesms.backup.v2.proto.Quote
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.backup.v2.proto.Sticker
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
@@ -38,6 +43,7 @@ import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.ReactionTable
import org.thoughtcrime.securesms.database.SQLiteDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
import org.thoughtcrime.securesms.database.documents.NetworkFailure
@@ -45,22 +51,34 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.CryptoValue
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
import org.thoughtcrime.securesms.database.model.databaseprotos.PaymentTombstone
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.payments.CryptoValueUtil
import org.thoughtcrime.securesms.payments.Direction
import org.thoughtcrime.securesms.payments.State
import org.thoughtcrime.securesms.payments.proto.PaymentMetaData
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.api.payments.Money
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.internal.push.DataMessage
import java.math.BigInteger
import java.util.Optional
import java.util.UUID
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
/**
* An object that will ingest all fo the [ChatItem]s you want to write, buffer them until hitting a specified batch size, and then batch insert them
@@ -92,7 +110,6 @@ class ChatItemImportInserter(
MessageTable.EXPIRE_STARTED,
MessageTable.UNIDENTIFIED,
MessageTable.REMOTE_DELETED,
MessageTable.REMOTE_DELETED,
MessageTable.NETWORK_FAILURES,
MessageTable.QUOTE_ID,
MessageTable.QUOTE_AUTHOR,
@@ -104,7 +121,9 @@ class ChatItemImportInserter(
MessageTable.LINK_PREVIEWS,
MessageTable.MESSAGE_RANGES,
MessageTable.VIEW_ONCE,
MessageTable.MESSAGE_EXTRAS
MessageTable.MESSAGE_EXTRAS,
MessageTable.ORIGINAL_MESSAGE_ID,
MessageTable.LATEST_REVISION_ID
)
private val REACTION_COLUMNS = arrayOf(
@@ -156,8 +175,22 @@ class ChatItemImportInserter(
Log.w(TAG, "[insert] Could not find a backup recipientId for backup chatId ${chatItem.chatId}! Skipping.")
return
}
val messageInsert = chatItem.toMessageInsert(fromLocalRecipientId, chatLocalRecipientId, localThreadId)
if (chatItem.revisions.isNotEmpty()) {
val originalId = messageId
val latestRevisionId = originalId + chatItem.revisions.size
val sortedRevisions = chatItem.revisions.sortedBy { it.dateSent }.map { it.toMessageInsert(fromLocalRecipientId, chatLocalRecipientId, localThreadId) }
for (revision in sortedRevisions) {
revision.contentValues.put(MessageTable.ORIGINAL_MESSAGE_ID, originalId)
revision.contentValues.put(MessageTable.LATEST_REVISION_ID, latestRevisionId)
revision.contentValues.put(MessageTable.REVISION_NUMBER, (messageId - originalId))
buffer.messages += revision
messageId++
}
buffer.messages += chatItem.toMessageInsert(fromLocalRecipientId, chatLocalRecipientId, localThreadId)
messageInsert.contentValues.put(MessageTable.ORIGINAL_MESSAGE_ID, originalId)
}
buffer.messages += messageInsert
buffer.reactions += chatItem.toReactionContentValues(messageId)
buffer.groupReceipts += chatItem.toGroupReceiptContentValues(messageId, chatBackupRecipientId)
@@ -266,6 +299,22 @@ class ChatItemImportInserter(
}
}
}
if (this.paymentNotification != null) {
followUp = { messageRowId ->
val uuid = tryRestorePayment(this, chatRecipientId)
if (uuid != null) {
db.update(
MessageTable.TABLE_NAME,
contentValuesOf(
MessageTable.BODY to uuid.toString(),
MessageTable.TYPE to ((contentValues.getAsLong(MessageTable.TYPE) and MessageTypes.SPECIAL_TYPES_MASK.inv()) or MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION)
),
"${MessageTable.ID}=?",
SqlUtil.buildArgs(messageRowId)
)
}
}
}
if (this.standardMessage != null) {
val bodyRanges = this.standardMessage.text?.bodyRanges
if (!bodyRanges.isNullOrEmpty()) {
@@ -298,6 +347,15 @@ class ChatItemImportInserter(
}
}
}
if (this.stickerMessage != null) {
val sticker = this.stickerMessage.sticker
val attachment = sticker.toLocalAttachment()
if (attachment != null) {
followUp = { messageRowId ->
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(attachment), emptyList())
}
}
}
return MessageInsert(contentValues, followUp)
}
@@ -353,11 +411,43 @@ class ChatItemImportInserter(
this.standardMessage != null -> contentValues.addStandardMessage(this.standardMessage)
this.remoteDeletedMessage != null -> contentValues.put(MessageTable.REMOTE_DELETED, 1)
this.updateMessage != null -> contentValues.addUpdateMessage(this.updateMessage)
this.paymentNotification != null -> contentValues.addPaymentNotification(this, chatRecipientId)
this.giftBadge != null -> contentValues.addGiftBadge(this.giftBadge)
}
return contentValues
}
private fun tryRestorePayment(chatItem: ChatItem, chatRecipientId: RecipientId): UUID? {
val paymentNotification = chatItem.paymentNotification!!
val amount = paymentNotification.amountMob?.tryParseMoney() ?: return null
val fee = paymentNotification.feeMob?.tryParseMoney() ?: return null
if (paymentNotification.transactionDetails?.failedTransaction != null) {
return null
}
val transaction = paymentNotification.transactionDetails?.transaction
val mobileCoinIdentification = transaction?.mobileCoinIdentification?.toLocal() ?: return null
return SignalDatabase.payments.restoreFromBackup(
chatRecipientId,
transaction.timestamp ?: 0,
transaction.blockIndex ?: 0,
paymentNotification.note ?: "",
if (chatItem.outgoing != null) Direction.SENT else Direction.RECEIVED,
transaction.status.toLocalStatus(),
amount,
fee,
transaction.transaction?.toByteArray(),
transaction.receipt?.toByteArray(),
mobileCoinIdentification,
chatItem.incoming?.read ?: true
)
}
private fun ChatItem.toReactionContentValues(messageId: Long): List<ContentValues> {
val reactions: List<Reaction> = when {
this.standardMessage != null -> this.standardMessage.reactions
@@ -430,6 +520,10 @@ class ChatItemImportInserter(
type = type or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT
}
if (this.giftBadge != null) {
type = type or MessageTypes.SPECIAL_TYPE_GIFT_BADGE
}
return type
}
@@ -465,6 +559,7 @@ class ChatItemImportInserter(
SimpleChatUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE or typeWithoutBase
SimpleChatUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED or typeWithoutBase
SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST or typeWithoutBase
SimpleChatUpdate.Type.UNSUPPORTED_PROTOCOL_MESSAGE -> MessageTypes.UNSUPPORTED_MESSAGE_TYPE or typeWithoutBase
}
}
updateMessage.expirationTimerChange != null -> {
@@ -474,8 +569,14 @@ class ChatItemImportInserter(
updateMessage.profileChange != null -> {
typeFlags = MessageTypes.PROFILE_CHANGE_TYPE
val profileChangeDetails = ProfileChangeDetails(profileNameChange = ProfileChangeDetails.StringChange(previous = updateMessage.profileChange.previousName, newValue = updateMessage.profileChange.newName))
.encode()
put(MessageTable.BODY, Base64.encodeWithPadding(profileChangeDetails))
val messageExtras = MessageExtras(profileChangeDetails = profileChangeDetails).encode()
put(MessageTable.MESSAGE_EXTRAS, messageExtras)
}
updateMessage.learnedProfileChange != null -> {
typeFlags = MessageTypes.PROFILE_CHANGE_TYPE
val profileChangeDetails = ProfileChangeDetails(learnedProfileName = ProfileChangeDetails.LearnedProfileName(e164 = updateMessage.learnedProfileChange.e164?.toString(), username = updateMessage.learnedProfileChange.username))
val messageExtras = MessageExtras(profileChangeDetails = profileChangeDetails).encode()
put(MessageTable.MESSAGE_EXTRAS, messageExtras)
}
updateMessage.sessionSwitchover != null -> {
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
@@ -500,7 +601,17 @@ class ChatItemImportInserter(
this.put(MessageTable.TYPE, typeFlags)
}
updateMessage.groupCall != null -> {
this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.groupCall))
val startedCallRecipientId = if (updateMessage.groupCall.startedCallRecipientId != null) {
backupState.backupToLocalRecipientId[updateMessage.groupCall.startedCallRecipientId]
} else {
null
}
val startedCall = if (startedCallRecipientId != null) {
recipients.getRecord(startedCallRecipientId).aci
} else {
null
}
this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.groupCall, startedCall))
this.put(MessageTable.TYPE, MessageTypes.GROUP_CALL_TYPE)
}
updateMessage.groupChange != null -> {
@@ -518,6 +629,118 @@ class ChatItemImportInserter(
this.put(MessageTable.TYPE, typeFlags)
}
/**
* Add the payment notification to the chat item.
*
* Note we add a tombstone first, then post insertion update it to a proper notification
*/
private fun ContentValues.addPaymentNotification(chatItem: ChatItem, chatRecipientId: RecipientId) {
val paymentNotification = chatItem.paymentNotification!!
if (chatItem.paymentNotification.amountMob.isNullOrEmpty()) {
addPaymentTombstoneNoAmount()
return
}
val amount = paymentNotification.amountMob?.tryParseMoney() ?: return addPaymentTombstoneNoAmount()
val fee = paymentNotification.feeMob?.tryParseMoney() ?: return addPaymentTombstoneNoAmount()
if (chatItem.paymentNotification.transactionDetails?.failedTransaction != null) {
addFailedPaymentNotification(chatItem, amount, fee, chatRecipientId)
return
}
addPaymentTombstoneNoMetadata(chatItem.paymentNotification)
}
private fun PaymentNotification.TransactionDetails.MobileCoinTxoIdentification.toLocal(): PaymentMetaData {
return PaymentMetaData(
mobileCoinTxoIdentification = PaymentMetaData.MobileCoinTxoIdentification(
publicKey = this.publicKey,
keyImages = this.keyImages
)
)
}
private fun ContentValues.addFailedPaymentNotification(chatItem: ChatItem, amount: Money, fee: Money, chatRecipientId: RecipientId) {
val uuid = SignalDatabase.payments.restoreFromBackup(
chatRecipientId,
0,
0,
chatItem.paymentNotification?.note ?: "",
if (chatItem.outgoing != null) Direction.SENT else Direction.RECEIVED,
State.FAILED,
amount,
fee,
null,
null,
null,
chatItem.incoming?.read ?: true
)
if (uuid != null) {
put(MessageTable.BODY, uuid.toString())
put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION)
} else {
addPaymentTombstoneNoMetadata(chatItem.paymentNotification!!)
}
}
private fun ContentValues.addPaymentTombstoneNoAmount() {
put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or MessageTypes.SPECIAL_TYPE_PAYMENTS_TOMBSTONE)
}
private fun ContentValues.addPaymentTombstoneNoMetadata(paymentNotification: PaymentNotification) {
put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or MessageTypes.SPECIAL_TYPE_PAYMENTS_TOMBSTONE)
val amount = tryParseCryptoValue(paymentNotification.amountMob)
val fee = tryParseCryptoValue(paymentNotification.feeMob)
put(
MessageTable.MESSAGE_EXTRAS,
MessageExtras(
paymentTombstone = PaymentTombstone(
note = paymentNotification.note,
amount = amount,
fee = fee
)
).encode()
)
}
private fun ContentValues.addGiftBadge(giftBadge: BackupGiftBadge) {
val dbGiftBadge = GiftBadge(
redemptionToken = giftBadge.receiptCredentialPresentation,
redemptionState = when (giftBadge.state) {
BackupGiftBadge.State.UNOPENED -> GiftBadge.RedemptionState.PENDING
BackupGiftBadge.State.OPENED -> GiftBadge.RedemptionState.STARTED
BackupGiftBadge.State.REDEEMED -> GiftBadge.RedemptionState.REDEEMED
BackupGiftBadge.State.FAILED -> GiftBadge.RedemptionState.FAILED
}
)
put(MessageTable.BODY, Base64.encodeWithPadding(GiftBadge.ADAPTER.encode(dbGiftBadge)))
}
private fun String?.tryParseMoney(): Money? {
if (this.isNullOrEmpty()) {
return null
}
val amountCryptoValue = tryParseCryptoValue(this)
return if (amountCryptoValue != null) {
CryptoValueUtil.cryptoValueToMoney(amountCryptoValue)
} else {
null
}
}
private fun tryParseCryptoValue(bigIntegerString: String?): CryptoValue? {
if (bigIntegerString == null) {
return null
}
val amount = try {
BigInteger(bigIntegerString).toString()
} catch (e: NumberFormatException) {
return null
}
return CryptoValue(mobileCoinValue = CryptoValue.MobileCoinValue(picoMobileCoin = amount))
}
private fun ContentValues.addQuote(quote: Quote) {
this.put(MessageTable.QUOTE_ID, quote.targetSentTimestamp ?: MessageTable.QUOTE_TARGET_MISSING_ID)
this.put(MessageTable.QUOTE_AUTHOR, backupState.backupToLocalRecipientId[quote.authorId]!!.serialize())
@@ -528,6 +751,15 @@ class ChatItemImportInserter(
this.put(MessageTable.QUOTE_MISSING, (quote.targetSentTimestamp == null).toInt())
}
private fun PaymentNotification.TransactionDetails.Transaction.Status?.toLocalStatus(): State {
return when (this) {
PaymentNotification.TransactionDetails.Transaction.Status.INITIAL -> State.INITIAL
PaymentNotification.TransactionDetails.Transaction.Status.SUBMITTED -> State.SUBMITTED
PaymentNotification.TransactionDetails.Transaction.Status.SUCCESSFUL -> State.SUCCESSFUL
else -> State.INITIAL
}
}
private fun Quote.Type.toLocalQuoteType(): Int {
return when (this) {
Quote.Type.UNKNOWN -> QuoteModel.Type.NORMAL.code
@@ -607,80 +839,129 @@ class ChatItemImportInserter(
}
}
private fun MessageAttachment.toLocalAttachment(contentType: String? = pointer?.contentType, fileName: String? = pointer?.fileName): Attachment? {
if (pointer == null) return null
if (pointer.attachmentLocator != null) {
private fun FilePointer?.toLocalAttachment(voiceNote: Boolean, borderless: Boolean, gif: Boolean, wasDownloaded: Boolean, stickerLocator: StickerLocator? = null, contentType: String? = this?.contentType, fileName: String? = this?.fileName, uuid: ByteString? = null): Attachment? {
if (this == null) return null
if (attachmentLocator != null) {
val signalAttachmentPointer = SignalServiceAttachmentPointer(
pointer.attachmentLocator.cdnNumber,
SignalServiceAttachmentRemoteId.from(pointer.attachmentLocator.cdnKey),
attachmentLocator.cdnNumber,
SignalServiceAttachmentRemoteId.from(attachmentLocator.cdnKey),
contentType,
pointer.attachmentLocator.key.toByteArray(),
Optional.ofNullable(pointer.attachmentLocator.size),
attachmentLocator.key.toByteArray(),
Optional.ofNullable(attachmentLocator.size),
Optional.empty(),
pointer.width ?: 0,
pointer.height ?: 0,
Optional.ofNullable(pointer.attachmentLocator.digest.toByteArray()),
Optional.ofNullable(pointer.incrementalMac?.toByteArray()),
pointer.incrementalMacChunkSize ?: 0,
width ?: 0,
height ?: 0,
Optional.ofNullable(attachmentLocator.digest.toByteArray()),
Optional.ofNullable(incrementalMac?.toByteArray()),
incrementalMacChunkSize ?: 0,
Optional.ofNullable(fileName),
flag == MessageAttachment.Flag.VOICE_MESSAGE,
flag == MessageAttachment.Flag.BORDERLESS,
flag == MessageAttachment.Flag.GIF,
Optional.ofNullable(pointer.caption),
Optional.ofNullable(pointer.blurHash),
pointer.attachmentLocator.uploadTimestamp
voiceNote,
borderless,
gif,
Optional.ofNullable(caption),
Optional.ofNullable(blurHash),
attachmentLocator.uploadTimestamp,
UuidUtil.fromByteStringOrNull(uuid)
)
return PointerAttachment.forPointer(
pointer = Optional.of(signalAttachmentPointer),
stickerLocator = stickerLocator,
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING
).orNull()
} else if (pointer.invalidAttachmentLocator != null) {
} else if (invalidAttachmentLocator != null) {
return TombstoneAttachment(
contentType = contentType,
incrementalMac = pointer.incrementalMac?.toByteArray(),
incrementalMacChunkSize = pointer.incrementalMacChunkSize,
width = pointer.width,
height = pointer.height,
caption = pointer.caption,
blurHash = pointer.blurHash,
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
gif = flag == MessageAttachment.Flag.GIF,
quote = false
incrementalMac = incrementalMac?.toByteArray(),
incrementalMacChunkSize = incrementalMacChunkSize,
width = width,
height = height,
caption = caption,
blurHash = blurHash,
voiceNote = voiceNote,
borderless = borderless,
gif = gif,
quote = false,
uuid = UuidUtil.fromByteStringOrNull(uuid)
)
} else if (pointer.backupLocator != null) {
} else if (backupLocator != null) {
return ArchivedAttachment(
contentType = contentType,
size = pointer.backupLocator.size.toLong(),
cdn = pointer.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
key = pointer.backupLocator.key.toByteArray(),
cdnKey = pointer.backupLocator.transitCdnKey,
archiveCdn = pointer.backupLocator.cdnNumber,
archiveMediaName = pointer.backupLocator.mediaName,
archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(pointer.backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(pointer.backupLocator.mediaName)).encode(),
digest = pointer.backupLocator.digest.toByteArray(),
incrementalMac = pointer.incrementalMac?.toByteArray(),
incrementalMacChunkSize = pointer.incrementalMacChunkSize,
width = pointer.width,
height = pointer.height,
caption = pointer.caption,
blurHash = pointer.blurHash,
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
gif = flag == MessageAttachment.Flag.GIF,
quote = false
size = backupLocator.size.toLong(),
cdn = backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
key = backupLocator.key.toByteArray(),
cdnKey = backupLocator.transitCdnKey,
archiveCdn = backupLocator.cdnNumber,
archiveMediaName = backupLocator.mediaName,
archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(backupLocator.mediaName)).encode(),
digest = backupLocator.digest.toByteArray(),
incrementalMac = incrementalMac?.toByteArray(),
incrementalMacChunkSize = incrementalMacChunkSize,
width = width,
height = height,
caption = caption,
blurHash = blurHash,
voiceNote = voiceNote,
borderless = borderless,
gif = gif,
quote = false,
stickerLocator = stickerLocator,
uuid = UuidUtil.fromByteStringOrNull(uuid)
)
}
return null
}
private fun Sticker?.toLocalAttachment(): Attachment? {
if (this == null) return null
return data_.toLocalAttachment(
voiceNote = false,
gif = false,
borderless = false,
wasDownloaded = true,
stickerLocator = StickerLocator(
packId = Hex.toStringCondensed(packId.toByteArray()),
packKey = Hex.toStringCondensed(packKey.toByteArray()),
stickerId = stickerId,
emoji = emoji
)
)
}
private fun MessageAttachment.toLocalAttachment(): Attachment? {
return pointer?.toLocalAttachment(
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
gif = flag == MessageAttachment.Flag.GIF,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
wasDownloaded = wasDownloaded,
uuid = clientUuid
)
}
private fun MessageAttachment.toLocalAttachment(contentType: String?, fileName: String?): Attachment? {
return pointer?.toLocalAttachment(
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
gif = flag == MessageAttachment.Flag.GIF,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
wasDownloaded = wasDownloaded,
contentType = contentType,
fileName = fileName,
uuid = clientUuid
)
}
private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? {
return thumbnail?.toLocalAttachment(this.contentType, this.fileName)
?: if (this.contentType == null) null else PointerAttachment.forPointer(quotedAttachment = DataMessage.Quote.QuotedAttachment(contentType = this.contentType, fileName = this.fileName, thumbnail = null)).orNull()
}
private class MessageInsert(val contentValues: ContentValues, val followUp: ((Long) -> Unit)?)
private class MessageInsert(
val contentValues: ContentValues,
val followUp: ((Long) -> Unit)?,
val edits: List<MessageInsert>? = null
)
private class Buffer(
val messages: MutableList<MessageInsert> = mutableListOf(),

Some files were not shown because too many files have changed in this diff Show More