Compare commits

...

338 Commits

Author SHA1 Message Date
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
Nicholas Tinsley
eb114de5c8 Bump version to 7.8.1 2024-05-16 15:50:48 -04:00
Nicholas Tinsley
1bf9695cff Update translations and other static files. 2024-05-16 15:45:51 -04:00
Clark
241bf065e8 Fix missing thumbnail_file column in media query. 2024-05-16 13:09:58 -04:00
Clark Chen
e0f3b35805 Fix missing archive_thumbnail_cdn column. 2024-05-16 12:30:33 -04:00
Nicholas Tinsley
5741dfc00b Bump version to 7.8.0 2024-05-16 10:24:48 -04:00
Nicholas Tinsley
ec430da772 Update translations and other static files. 2024-05-16 10:20:08 -04:00
Rashad Sookram
5e6d9434de Update to RingRTC v2.42.0 2024-05-16 10:16:10 -04:00
Clark
b72d586748 Add initial thumbnail restore for message backup. 2024-05-16 10:16:10 -04:00
Ehren Kret
757c0fd2ea create video call mimetype for raw contacts links 2024-05-16 10:16:10 -04:00
Nicholas Tinsley
c4e4eaf110 Remove lower hand confirmation dialog. 2024-05-16 10:16:10 -04:00
Nicholas Tinsley
f83275e246 Add customize button to in-call reaction picker. 2024-05-16 10:16:10 -04:00
Greyson Parrelli
d0340d39db Reset backupV2 credentials on 403. 2024-05-16 10:16:10 -04:00
Greyson Parrelli
227a279131 Make sure note to self is included in backupsV2. 2024-05-16 10:16:10 -04:00
mtang-signal
0465fdea62 Update contacts permission UI. 2024-05-16 10:16:10 -04:00
Nicholas Tinsley
13bd4a9c74 Update regv2 result field name. 2024-05-16 10:16:10 -04:00
Greyson Parrelli
f570f1f2c4 Initial test implementation of SVR3. 2024-05-15 15:55:22 -04:00
Nicholas Tinsley
68ced18ea1 Fleshed out session management in registration v2. 2024-05-15 15:55:22 -04:00
Greyson Parrelli
b4a8f01980 Include message timestamp in local send timings. 2024-05-15 15:55:22 -04:00
mtang-signal
c3c743fbb8 Update camera permission UI in media. 2024-05-15 15:55:22 -04:00
Adam Mork
b14eddefc9 Add payment enclave measurements for v6.0.0 2024-05-15 15:55:21 -04:00
Nicholas Tinsley
46638a1948 Bump version to 7.7.2 2024-05-15 15:54:11 -04:00
Nicholas Tinsley
5cee85fcdc Update translations and other static files. 2024-05-15 15:44:52 -04:00
mtang-signal
f97d7e3dfd Fix permissions ask in gallery. 2024-05-15 12:18:50 -07:00
Alex Hart
6da0ecf827 Bump version to 7.7.1 2024-05-10 22:54:45 -03:00
Alex Hart
9803550bba Update baseline profile. 2024-05-10 22:51:55 -03:00
Alex Hart
15284da4c5 Update translations and other static files. 2024-05-10 22:48:55 -03:00
Alex Hart
351c3219e4 Replace RxStore with MutableStateFlow for better lifecycle control. 2024-05-10 22:44:05 -03:00
Alex Hart
ab95dbbc77 Bump version to 7.7.0 2024-05-08 16:43:30 -03:00
Alex Hart
cc6cba45c6 Update baseline profile. 2024-05-08 16:36:44 -03:00
Alex Hart
ce37660df2 Update translations and other static files. 2024-05-08 16:34:17 -03:00
Nicholas Tinsley
ca14ed9b2c Allow for captcha solving for reg v2. 2024-05-08 16:30:53 -03:00
Clark
ba4cdea75d Add cellular backup toggle for message backup. 2024-05-08 16:30:53 -03:00
Clark
83c34dd4cc Integrate swapping backup tiers from backup settings. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
b6db3802d3 Set raised hand list to be distinct by RecipientID. 2024-05-08 16:30:53 -03:00
Greyson Parrelli
a9a19d3ae0 Add job to upload thumbnails to archive. 2024-05-08 16:30:53 -03:00
Alex Hart
52fb873b1b Specify vibrate attributes to resolve vibrate from background. 2024-05-08 16:30:53 -03:00
moiseev-signal
9a0bb243cd Implement a libsignal-net shadowing web socket. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
78bbab37fb Show missing FCM dialog in registration V2. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
9af73b1409 Allow initialization of registration V2 without FCM. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
9c5bb4aa17 Initial error handling for registration v2. 2024-05-08 16:30:53 -03:00
Clark
49ba83dda8 Integrate message backup frequency. 2024-05-08 16:30:53 -03:00
Clark
de3b0d4ca2 Integrate the backup size into backup settings. 2024-05-08 16:30:53 -03:00
fm-sys
b2efc42357 Add back ability to long press title bar to go to system contact.
Resolves #13372
2024-05-08 16:30:53 -03:00
Cody Henthorne
a71faf674d Cleanup group management code. 2024-05-08 16:30:53 -03:00
moiseev-signal
34faa9003f Upgrade to libsignal 0.46.0. 2024-05-08 16:30:53 -03:00
Clark
bc527a2bc1 Basic settings functionality for message backup. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
0a3f96935a Support device transfers in restore flow v2. 2024-05-08 16:30:53 -03:00
Alex Konradi
35232a3928 Unwrap ExecutionException from Future observable 2024-05-08 16:30:53 -03:00
Alex Hart
70d74e0bb1 Allow users who have disabled Contacts permission to hide system contacts. 2024-05-08 16:30:53 -03:00
Alex Hart
36c91a95e2 Check spannable intersect when moving between stories. 2024-05-08 16:30:53 -03:00
Greyson Parrelli
4600e38a2a Add partial index to improve unread count perf. 2024-05-08 16:30:52 -03:00
Alex Hart
55abd88a03 Implement better handling for call peeking when opening the calls tab. 2024-05-08 16:30:52 -03:00
mtang-signal
cd880b0879 Expand double tap area. 2024-05-08 16:30:52 -03:00
mtang-signal
bbae6d876f Avoid translating support email strings. 2024-05-08 16:30:52 -03:00
dalamsya50
48a0c5a5a9 Fix git error when running on GitHub Actions.
Fixes #13495
Resolves #13547
2024-05-08 16:30:52 -03:00
BenjaminMuslic
c261df41b0 Added automatic capitalization to profile name fields.
Resolves #13544
2024-05-08 16:30:52 -03:00
Greyson Parrelli
cc98eced27 Short-circuit query if list is empty. 2024-05-08 16:30:52 -03:00
moiseev-signal
452d5960e4 Add test and extra cleanup around usage of incremental mac. 2024-05-08 16:30:52 -03:00
mtang-signal
c95b180728 Update gallery permission UI 2024-05-08 16:30:52 -03:00
Greyson Parrelli
3c380d35fd Attempt to reduce impact of thread updates. 2024-05-08 16:30:52 -03:00
Nicholas Tinsley
41935120e5 DeviceTransferFragment Kotlin rewrite. 2024-05-01 16:45:36 -04:00
Alex Hart
03d8f72c41 Fix group collisions error. 2024-05-01 16:45:36 -04:00
Greyson Parrelli
ab9ecff4d4 Improve timing of query methods. 2024-05-01 16:45:36 -04:00
Alex Hart
e351a0b235 Correct flags for story replies. 2024-05-01 16:45:36 -04:00
Nicholas Tinsley
4a08de370a Fix issue with Mp4Writer with massive time scales. 2024-05-01 16:45:36 -04:00
Cody Henthorne
6d657b449c Convert and update Manage Storage Settings. 2024-05-01 16:45:36 -04:00
Clark
adef572abb Store group snapshot attributes in GroupAttributeBlobs. 2024-05-01 16:45:36 -04:00
Greyson Parrelli
d6f2039bd1 Update attachment cipher tests to use longer inputs. 2024-05-01 16:45:35 -04:00
Clark
1223c3c768 Add support for new backup calls proto and call links. 2024-05-01 16:45:35 -04:00
Greyson Parrelli
333fa22c96 Bump version to 7.6.2 2024-05-01 16:42:29 -04:00
Greyson Parrelli
76c04d8d6d Update translations and other static files. 2024-05-01 16:42:03 -04:00
Greyson Parrelli
c3070f2913 Revert "Expand double tap touch area."
This reverts commit 8c81e47737.
2024-05-01 16:33:33 -04:00
Nicholas Tinsley
234b3967ed Fix button crash in v1 PIN restore fragment. 2024-05-01 10:42:23 -04:00
Greyson Parrelli
89d420cda8 Bump version to 7.6.1 2024-04-30 16:42:44 -04:00
Greyson Parrelli
ced4ece5b8 Update translations and other static files. 2024-04-30 16:41:02 -04:00
mtang-signal
8c81e47737 Expand double tap touch area. 2024-04-30 16:29:33 -04:00
Cody Henthorne
5d15eef61d Improve translations with pluralized string resources. 2024-04-30 16:04:14 -04:00
Greyson Parrelli
8f3e62245f Fix some issues where views were accessed after being destroyed. 2024-04-30 15:22:57 -04:00
Greyson Parrelli
e4ab795c62 Fix stream reading error. 2024-04-30 15:22:57 -04:00
mtang-signal
e4d6f9240f Fix double tap layout warning. 2024-04-30 14:44:00 -04:00
Greyson Parrelli
cfaf40e605 Fix KeyValueDataSet tests. 2024-04-30 11:05:39 -07:00
Greyson Parrelli
bdcf2431e7 Bump version to 7.6.0 2024-04-29 22:04:56 -04:00
Greyson Parrelli
7241283be2 Update baseline profile. 2024-04-29 22:04:56 -04:00
Greyson Parrelli
dde2a8b63a Update translations and other static files. 2024-04-29 22:04:56 -04:00
Greyson Parrelli
f7763a5b82 Be more lenient around long-int conversion in SignalStore. 2024-04-29 22:04:47 -04:00
Greyson Parrelli
c6f4a01001 Hopeful fix for crash in SimpleProgressDialog. 2024-04-29 22:04:31 -04:00
Greyson Parrelli
95a6835988 Improve handling of backup initialization. 2024-04-29 19:26:06 -04:00
moiseev-signal
f9a8f447d2 Support proxy in connections managed by libsignal. 2024-04-29 19:26:06 -04:00
Nicholas Tinsley
d20f588802 Inline the group call reactions feature flag. 2024-04-29 19:25:59 -04:00
Nicholas Tinsley
f23476a4e9 Initial support for restoring backups and skipping SMS in registration v2. 2024-04-29 19:25:59 -04:00
mtang-signal
fd4864b3b1 Update microphone permission UI for calls. 2024-04-29 19:25:59 -04:00
mtang-signal
c5c0c432c4 Update microphone permission UI for voice messages. 2024-04-29 19:25:59 -04:00
Jim Gustafson
69c40a6835 Update to RingRTC v2.41.0 2024-04-29 19:25:59 -04:00
moiseev-signal
7ef7aa65e6 Upgrade to libsignal 0.45.1. 2024-04-29 19:25:59 -04:00
Greyson Parrelli
97c08f0d52 Add additional validations to incremental attachment streams. 2024-04-29 19:25:59 -04:00
mtang-signal
18e6c57e75 Update location permission UI. 2024-04-29 19:25:59 -04:00
mtang-signal
ffc1463cda Add double tap editing feature. 2024-04-29 19:25:59 -04:00
Clark
84e654efb2 Set archive transfer state when archive data is set. 2024-04-29 19:25:59 -04:00
Clark
d983265e08 Persist group state in backup. 2024-04-29 19:25:59 -04:00
Alex Hart
e60b32202e Improved missed call state handling. 2024-04-29 19:25:59 -04:00
moiseev-signal
95fbd7a31c Implement unauthenticated chat web socket connection via libsignal-net. 2024-04-29 19:25:59 -04:00
Nicholas Tinsley
00a91e32fc Multiple skin tones for reaction bursts. 2024-04-29 19:25:59 -04:00
Alex Hart
fa32b7a883 Fix coloring on outgoing calls. 2024-04-24 15:10:12 -03:00
Alex Hart
63e6f955ed Prevent getCallLinks from returning links without root keys. 2024-04-24 14:17:58 -03:00
Alex Hart
7dcb8a425a Handle joined sync message for call links. 2024-04-24 13:31:35 -03:00
Cody Henthorne
f35ce068f9 Change profile fetch REST fallback based on authentication error. 2024-04-24 11:41:31 -04:00
Nicholas Tinsley
881d231a93 Improve group call reactions UI when presented without raise hand.
This also dismisses the custom reaction picker when switching to PiP mode.
2024-04-24 10:13:56 -04:00
Alex Hart
293634c758 Send call link update sync message upon call link creation. 2024-04-24 10:48:00 -03:00
Greyson Parrelli
4134df3f35 Use archive-specific endpoint for attachment backfill. 2024-04-23 16:29:03 -04:00
Clark
f78a019c70 Use seconds instead of millis for redemption time. 2024-04-23 15:56:38 -04:00
Cody Henthorne
d561a1385c Fix extremely long emoji search crash. 2024-04-23 12:29:03 -04:00
moiseev-signal
9b5387e221 Upgrade to libsignal 0.45.0 2024-04-23 12:29:03 -04:00
Cody Henthorne
25b1a814fe Remove legacy keyword search flag from emoji search infra. 2024-04-23 12:29:03 -04:00
Clark
b043b6e458 Schedule message backups when enabled. 2024-04-23 12:29:03 -04:00
Clark
8a972d93e9 Actually use backup jitter in local backups. 2024-04-23 12:29:03 -04:00
Cody Henthorne
8fe66a14c5 Fix multi-window camera crash. 2024-04-23 12:29:03 -04:00
Clark
f82bd64c10 Copy inbound attachments to archive service. 2024-04-23 12:29:03 -04:00
Nicholas Tinsley
4bcab49539 Correct UnopinionatedResponseCodeHandler constant name. 2024-04-23 12:29:03 -04:00
mtang-signal
0f4618ab11 Remove link preview images from shared media. 2024-04-23 12:29:03 -04:00
Cody Henthorne
475ca50fab Fix missing local participant state changes in group calls bug. 2024-04-23 12:29:03 -04:00
Greyson Parrelli
a64a02fa0c Fix issue where structured contact name syncing was delayed. 2024-04-23 12:29:02 -04:00
Clark
f3669a5865 Fix message extra column not being restored properly. 2024-04-23 12:29:02 -04:00
Greyson Parrelli
34dbd11db0 Update file format for backupV2. 2024-04-23 12:29:02 -04:00
Nicholas Tinsley
2e7279c72f Only display "Processing" text on outgoing media. 2024-04-23 12:29:02 -04:00
Nicholas Tinsley
6ad72f00af Fix phone number formatter in Registration V2. 2024-04-23 12:29:02 -04:00
Alex Hart
b771a21518 Add screen for managing backup type. 2024-04-23 12:29:02 -04:00
Greyson Parrelli
04fb459acd Remove unused backup outputstream class. 2024-04-23 12:29:02 -04:00
Greyson Parrelli
690a68f0d0 Remove libweb submodule entirely. 2024-04-23 12:29:02 -04:00
Greyson Parrelli
f34ae8d118 Add padding to the gzipped backup output. 2024-04-23 12:29:02 -04:00
Cody Henthorne
da43ff1e95 Bump version to 7.5.2 2024-04-23 11:42:24 -04:00
Cody Henthorne
f053ebbd51 Update baseline profile. 2024-04-23 11:36:15 -04:00
Cody Henthorne
87606af29c Update translations and other static files. 2024-04-23 11:30:56 -04:00
Cody Henthorne
c811bdcffa Fix benchmark test messages. 2024-04-23 11:26:36 -04:00
Cody Henthorne
0536628da3 Stagger app wake ups due to analyze database alarm. 2024-04-23 10:44:09 -04:00
Nicholas Tinsley
1fa53cfcb8 Prevent crash on attachment delete while voice note system tone is playing. 2024-04-23 10:22:01 -04:00
Cody Henthorne
a9ea3854d2 Bump version to 7.5.1 2024-04-22 17:06:18 -04:00
Cody Henthorne
dc35261e00 Update translations and other static files. 2024-04-22 16:56:39 -04:00
Cody Henthorne
716bc1f5e7 Cleanup dangling domain reference. 2024-04-22 16:52:02 -04:00
Cody Henthorne
db27204084 Validate pni signature message. 2024-04-22 16:33:03 -04:00
Cody Henthorne
42aeceffe2 Revert full usage of ActiveCallManager. 2024-04-22 16:32:27 -04:00
Greyson Parrelli
03845eabaf Bump version to 7.5.0 2024-04-18 16:44:32 -04:00
Greyson Parrelli
62af9dad50 Update translations and other static files. 2024-04-18 16:43:51 -04:00
Cody Henthorne
ee58d47926 Cycle rx message sending flag. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
d74260b536 Improve network reliability. 2024-04-18 16:24:13 -04:00
Alex Hart
15d8a698c5 Add new name collision state management. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
62cf3feeaa Restore a Local Backup v2 2024-04-18 16:24:13 -04:00
Alex Hart
947ab7d48b Implement skeleton for backup sheets. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
a82b9ee25f Add a job to backfill attachment uploads to the archive service. 2024-04-18 16:24:13 -04:00
mtang-signal
1e4d96b7c4 Add camera permission check to group stories. 2024-04-18 16:24:13 -04:00
Alex Hart
735a8e680c Add backupSubscription field to configuration object. 2024-04-18 16:24:13 -04:00
Alex Hart
d9e9fe1d6a Move backups selection code to its own package. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
4bcd1df4f8 Expand account consistency checks. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
9762899272 Remove old thread remappings. 2024-04-18 16:24:13 -04:00
Alex Hart
ce1b73970c Implement BackupStatus widget. 2024-04-18 16:24:13 -04:00
Alex Hart
58282e589b Implement backups settings fragment. 2024-04-18 16:24:13 -04:00
mtang-signal
75bd113545 Fix missing send button for voice notes. 2024-04-18 16:24:13 -04:00
Cody Henthorne
7a6bd0e1f2 Revert "Remove vestigial call camera toggle button."
This reverts commit 7a9c01e6e5.
2024-04-18 16:24:13 -04:00
Greyson Parrelli
f673c4eb83 Remove sql language annotation (for now).
It's broken in newer versions of Android Studio. It doesn't seem to
allow partial-sql anymore, only fully-formed statements. Same with
roomsql.
2024-04-18 16:24:13 -04:00
Jim Gustafson
cbb04e8f0c Update to RingRTC v2.40.0 2024-04-18 16:24:13 -04:00
mtang-signal
cd03da54d5 Fix note to self message detail text. 2024-04-18 16:24:13 -04:00
Clark
5f31f5966c Update backup locator proto. 2024-04-18 16:24:13 -04:00
Clark
d8bbfe2678 Add archived media sync job. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
7a2d408ca2 Stop voice memo playback if the current item is deleted.
Fixes #13502.
2024-04-18 16:24:13 -04:00
Nicholas Tinsley
5e4dfcc65f Add translator notes for some strings. 2024-04-18 16:24:13 -04:00
Clark
7811e51b41 Add CDN number as parameter for read credential call. 2024-04-18 16:24:13 -04:00
Alex Konradi
9703a868e5 Request new ZKC-based auth credential. 2024-04-18 16:24:13 -04:00
Alex Hart
1b7784b01f Update call strings to align with new designs. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
a83abaca1d Order story viewer names alphabetically. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
29b3f09d8a Catch possible ISE at end of re-registration. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
d36b2a23f5 Hide irrelevant rows in self about sheet. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
8f1722c718 Update placeholder label for view once media. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
5416c3b8aa Improve play button display logic on video editor fragment. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
89eeae36c4 Fix signed int overflow in disappearing timer UI message. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
eec2685e67 Registration refactor initial scaffolding. 2024-04-18 16:24:13 -04:00
Clark
318b59a6b2 Do not fallback to REST for resumable upload spec on ratelimit. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
a2e0468cd9 Remove "lower hand" confirmation dialog. 2024-04-18 16:24:13 -04:00
Clark
689eacd618 Add initial support for backup and restore of message and media to staging.
Co-authored-by: Cody Henthorne <cody@signal.org>
2024-04-18 16:24:13 -04:00
tedgravlin
8617a074ad Update CLA link in PR template. 2024-04-18 16:24:12 -04:00
Greyson Parrelli
046b8da880 Add missing static IPs.
Fixes #13513
2024-04-18 16:24:12 -04:00
Clark Chen
34a36ddfea Bump version to 7.4.2 2024-04-15 16:32:09 -04:00
Clark Chen
9330448198 Update translations and other static files. 2024-04-15 16:24:09 -04:00
Clark Chen
b3336b4d84 Revert "Use existing libsignal proguard rules."
This reverts commit 2ce6ea9a2a.
2024-04-15 10:17:36 -04:00
Alex Hart
9553c94097 Bump version to 7.4.1 2024-04-12 16:38:43 -03:00
Alex Hart
c1845ae1c4 Update baseline profile. 2024-04-12 16:33:11 -03:00
Alex Hart
b6cc3852b0 Update translations and other static files. 2024-04-12 16:28:07 -03:00
Cody Henthorne
eefc86f27e Fix dangling call notification and remove active call manager flag. 2024-04-12 09:38:06 -04:00
Nicholas Tinsley
09404157aa Add processor information to debug log. 2024-04-11 16:09:33 -04:00
Alex Hart
abfd9f8f41 Add proper capitalization settings in nickname activity. 2024-04-11 10:35:47 -03:00
Bishal
e04381fd75 Add fix for missing play button when the audio is not sent in offline mode. 2024-04-11 10:31:12 -03:00
Alex Hart
30cc3ff9fc Bump version to 7.4.0 2024-04-10 16:31:53 -03:00
Alex Hart
6f5f299035 Update baseline profile. 2024-04-10 16:31:47 -03:00
Alex Hart
02eed02cb8 Update translations and other static files. 2024-04-10 16:29:24 -03:00
Greyson Parrelli
c1d29b5c39 Set internalUser=true for nightly builds. 2024-04-10 14:54:35 -04:00
Greyson Parrelli
db4442939d Remove Environment.IS_PNP 2024-04-10 14:52:59 -04:00
tedgravlin
6ece776382 Fix navbar color in multiple instances. 2024-04-10 14:29:58 -03:00
Alex Hart
0eda714755 Send recipients when sending group story sync. 2024-04-10 14:21:34 -03:00
Greyson Parrelli
831d099503 Inline the nicknames feature flag. 2024-04-10 13:18:01 -04:00
Alex Hart
fa23e4ca70 Convert members collection to set to avoid duplicate entries. 2024-04-10 13:45:46 -03:00
Greyson Parrelli
982f602178 Regularly analyze database tables to improve index usage. 2024-04-09 16:55:25 -04:00
Greyson Parrelli
713298109a Specify indexes for mention table queries. 2024-04-09 16:18:21 -04:00
Greyson Parrelli
8793981804 Add a log section for the database schema. 2024-04-09 16:18:21 -04:00
Greyson Parrelli
9bd4e9524c Convert MentionTable to kotlin. 2024-04-09 16:18:21 -04:00
Cody Henthorne
791dc2724f Attempt to fix bad notification for call service shutdown. 2024-04-09 16:18:21 -04:00
Cody Henthorne
ba3473c61a Fix scroll to message when bubble is under toolbar. 2024-04-09 16:18:21 -04:00
moiseev-signal
3ea194255d Add getUsername default method to CredentialsProvider 2024-04-09 16:18:21 -04:00
Cody Henthorne
ea081e981f Treat unregistered user during send as general failure. 2024-04-09 16:18:21 -04:00
Alex Konradi
2ce6ea9a2a Use existing libsignal proguard rules. 2024-04-09 16:18:20 -04:00
Alex Konradi
295c9310e9 Map libsignal CDSI errors to existing exceptions. 2024-04-09 16:18:20 -04:00
Greyson Parrelli
7447ed2eac Add the ability to jump to a specific date in search. 2024-04-09 16:18:20 -04:00
Cody Henthorne
d5bf16b91a Fix incorrect thread body adjustments containing media, mentions, and styling. 2024-04-09 16:18:06 -04:00
Cody Henthorne
76665c1f0d Prevent excessive video toggling in group calls due to server instability. 2024-04-09 16:18:06 -04:00
Cody Henthorne
dd28523b05 Transition full screen call UX to terminal state when call handled by linked device. 2024-04-09 16:18:06 -04:00
Cody Henthorne
16588c401e Reduce verbosity of WebRtcViewModel event logging during calls. 2024-04-09 16:18:06 -04:00
Greyson Parrelli
dbf8a7ca87 Rotate libsignal-net flag. 2024-04-09 16:18:06 -04:00
moiseev-signal
e92c76434e Upgrade to libsignal-client 0.44.0 2024-04-09 16:18:06 -04:00
Greyson Parrelli
7adb581271 Bump version to 7.3.1 2024-04-09 16:17:21 -04:00
Greyson Parrelli
869476a41b Update translations and other static files. 2024-04-09 16:16:47 -04:00
Greyson Parrelli
8daf1bca20 Improve handling of unknown groups. 2024-04-09 15:56:15 -04:00
Greyson Parrelli
d044b3c931 Remove most lazy properties from Recipient. 2024-04-09 15:02:36 -04:00
Cody Henthorne
0fcb19e1cc Fix group recipient resolve race that can cause unknown group recipients in live cache. 2024-04-09 14:59:47 -04:00
Nicholas Tinsley
2a6977da75 Nickname screen copy update. 2024-04-04 09:45:26 -04:00
Nicholas Tinsley
26bd435bf6 Update nickname delete dialog copy. 2024-04-03 16:48:26 -04:00
1251 changed files with 72110 additions and 23489 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

@@ -2,7 +2,7 @@
### First time contributor checklist
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
- [ ] I have read [how to contribute](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md) to this project
- [ ] I have signed the [Contributor License Agreement](https://whispersystems.org/cla/)
- [ ] I have signed the [Contributor License Agreement](https://signal.org/cla/)
### Contributor checklist
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "libwebp"]
path = libwebp
url = https://github.com/webmproject/libwebp.git

View File

@@ -21,8 +21,8 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1406
val canonicalVersionName = "7.3.0"
val canonicalVersionCode = 1424
val canonicalVersionName = "7.9.2"
val postFixSize = 100
val abiPostFix: Map<String, Int> = mapOf(
@@ -79,7 +79,7 @@ wire {
}
ktlint {
version.set("0.49.1")
version.set("1.2.1")
}
android {
@@ -152,7 +152,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.4"
kotlinCompilerExtensionVersion = "1.5.4"
}
defaultConfig {
@@ -178,7 +178,6 @@ android {
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3.signal.org\"")
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\"")
buildConfigField("String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"")
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\"")
buildConfigField("String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"")
buildConfigField("String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\"")
@@ -377,7 +376,6 @@ android {
buildConfigField("String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\"")
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3-staging.signal.org\"")
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
@@ -553,6 +551,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)
@@ -714,7 +713,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

@@ -24,21 +24,23 @@ 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
@@ -233,7 +235,7 @@ class BackupTest {
@Test
fun accountData() {
val context = ApplicationDependencies.getApplication()
val context = AppDependencies.application
backupTest(validateKeyValue = true) {
val self = Recipient.self()
@@ -251,8 +253,7 @@ class BackupTest {
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"))
InAppPaymentsRepository.setSubscriber(InAppPaymentSubscriberRecord(SubscriberId.generate(), "USD", InAppPaymentSubscriberRecord.Type.DONATION, false, InAppPaymentData.PaymentMethodType.UNKNOWN))
SignalStore.donationsValues().setDisplayBadgesOnProfile(false)
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE

View File

@@ -5,9 +5,7 @@
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 okio.ByteString.Companion.toByteString
import org.junit.Assert
@@ -16,13 +14,14 @@ 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
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.Call
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
@@ -32,6 +31,7 @@ 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.Group
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Quote
@@ -133,6 +133,9 @@ class ImportExportTest {
private val standardFrames = arrayOf(defaultBackupInfo, standardAccountData, selfRecipient, releaseNotes)
}
private val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext
@JvmField
@Rule
var testName = TestName()
@@ -194,8 +197,7 @@ class ImportExportTest {
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = true,
hideStory = false,
storySendMode = Group.StorySendMode.ENABLED,
name = "Cool Group $i"
storySendMode = Group.StorySendMode.ENABLED
)
)
)
@@ -215,7 +217,7 @@ class ImportExportTest {
@Test
fun largeNumberOfMessagesAndChats() {
val NUM_INDIVIDUAL_RECIPIENTS = 1000
val numIndividualRecipients = 1000
val numIndividualMessages = 500
val numGroupMessagesPerPerson = 200
@@ -224,7 +226,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(
@@ -261,8 +263,7 @@ class ImportExportTest {
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = random.trueWithProbability(0.9f),
hideStory = random.trueWithProbability(0.1f),
storySendMode = if (random.trueWithProbability(0.9f)) Group.StorySendMode.ENABLED else Group.StorySendMode.DISABLED,
name = "Cool Group $i"
storySendMode = if (random.trueWithProbability(0.9f)) Group.StorySendMode.ENABLED else Group.StorySendMode.DISABLED
)
)
)
@@ -369,12 +370,12 @@ class ImportExportTest {
}
}
}
val import = exportFrames(
exportFrames(
*standardFrames,
*recipients.toArray(),
*chatItems.toArray()
)
outputFile(import)
}
@Test
@@ -431,7 +432,12 @@ class ImportExportTest {
whitelisted = true,
hideStory = true,
storySendMode = Group.StorySendMode.ENABLED,
name = "Cool test group"
snapshot = Group.GroupSnapshot(
title = Group.GroupAttributeBlob(title = "Group Cool"),
description = Group.GroupAttributeBlob(descriptionText = "Description"),
version = 10,
disappearingMessagesTimer = Group.GroupAttributeBlob(disappearingMessagesDuration = 1500000)
)
)
),
Recipient(
@@ -441,7 +447,12 @@ class ImportExportTest {
whitelisted = false,
hideStory = false,
storySendMode = Group.StorySendMode.DEFAULT,
name = "Cool test group"
snapshot = Group.GroupSnapshot(
title = Group.GroupAttributeBlob(title = "Group Cool"),
description = Group.GroupAttributeBlob(descriptionText = "Description"),
version = 10,
disappearingMessagesTimer = Group.GroupAttributeBlob(disappearingMessagesDuration = 1500000)
)
)
)
)
@@ -555,12 +566,12 @@ class ImportExportTest {
)
)
import(importData)
val exported = export()
val exported = BackupRepository.export()
val expected = exportFrames(
*standardFrames,
alexa
)
outputFile(importData, expected)
compare(expected, exported)
}
@@ -592,8 +603,7 @@ class ImportExportTest {
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = true,
hideStory = true,
storySendMode = Group.StorySendMode.DEFAULT,
name = "Cool test group"
storySendMode = Group.StorySendMode.DEFAULT
)
),
Chat(
@@ -611,69 +621,70 @@ class ImportExportTest {
}
@Test
fun calls() {
val individualCalls = ArrayList<Call>()
val groupCalls = ArrayList<Call>()
val states = arrayOf(Call.State.MISSED, Call.State.COMPLETED, Call.State.DECLINED_BY_USER, Call.State.DECLINED_BY_NOTIFICATION_PROFILE)
val types = arrayOf(Call.Type.VIDEO_CALL, Call.Type.AD_HOC_CALL, Call.Type.AUDIO_CALL)
var id = 1L
var timestamp = 12345L
fun individualCalls() {
val individualCalls = ArrayList<ChatItem>()
val states = arrayOf(IndividualCall.State.ACCEPTED, IndividualCall.State.NOT_ACCEPTED, IndividualCall.State.MISSED, IndividualCall.State.MISSED_NOTIFICATION_PROFILE)
val oldStates = arrayOf(IndividualCall.State.ACCEPTED, IndividualCall.State.MISSED)
val types = arrayOf(IndividualCall.Type.VIDEO_CALL, IndividualCall.Type.AUDIO_CALL)
val directions = arrayOf(IndividualCall.Direction.OUTGOING, IndividualCall.Direction.INCOMING)
var sentTime = 0L
var callId = 1L
val startedAci = TestRecipientUtils.nextAci().toByteString()
for (state in states) {
for (type in types) {
individualCalls.add(
Call(
callId = id++,
conversationRecipientId = 3,
type = type,
state = state,
timestamp = timestamp++,
ringerRecipientId = 3,
outgoing = true
for (direction in directions) {
// With call id
individualCalls.add(
ChatItem(
chatId = 1,
authorId = selfRecipient.id,
dateSent = sentTime++,
sms = false,
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
individualCall = IndividualCall(
callId = callId++,
type = type,
state = state,
direction = direction
)
)
)
)
)
individualCalls.add(
Call(
callId = id++,
conversationRecipientId = 3,
type = type,
state = state,
timestamp = timestamp++,
ringerRecipientId = selfRecipient.id,
outgoing = false
)
)
}
}
}
for (state in oldStates) {
for (type in types) {
for (direction in directions) {
if (state == IndividualCall.State.MISSED && direction == IndividualCall.Direction.OUTGOING) continue
// Without call id
individualCalls.add(
ChatItem(
chatId = 1,
authorId = selfRecipient.id,
dateSent = sentTime++,
sms = false,
directionless = ChatItem.DirectionlessMessageDetails(),
updateMessage = ChatUpdateMessage(
individualCall = IndividualCall(
callId = null,
type = type,
state = state,
direction = direction
)
)
)
)
}
}
groupCalls.add(
Call(
callId = id++,
conversationRecipientId = 4,
type = Call.Type.GROUP_CALL,
state = state,
timestamp = timestamp++,
ringerRecipientId = 3,
outgoing = true
)
)
groupCalls.add(
Call(
callId = id++,
conversationRecipientId = 4,
type = Call.Type.GROUP_CALL,
state = state,
timestamp = timestamp++,
ringerRecipientId = selfRecipient.id,
outgoing = false
)
)
}
importExport(
*standardFrames,
Recipient(
id = 3,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
aci = startedAci,
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
@@ -694,12 +705,21 @@ class ImportExportTest {
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = true,
hideStory = true,
storySendMode = Group.StorySendMode.DEFAULT,
name = "Cool test group"
storySendMode = Group.StorySendMode.DEFAULT
)
),
*individualCalls.toArray(),
*groupCalls.toArray()
Chat(
id = 1,
recipientId = 3,
archived = true,
pinnedOrder = 1,
expirationTimerMs = 1.days.inWholeMilliseconds,
muteUntilMs = System.currentTimeMillis(),
markedUnread = true,
dontNotifyForMentionsIfMuted = true,
wallpaper = null
),
*individualCalls.toArray()
)
}
@@ -936,7 +956,7 @@ class ImportExportTest {
chatId = 1,
authorId = alice.id,
dateSent = 101,
expireStartDate = null,
expireStartDate = 0,
expiresInMs = TimeUnit.DAYS.toMillis(1),
sms = false,
incoming = ChatItem.IncomingMessageDetails(
@@ -975,14 +995,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)
}
@@ -1008,17 +1027,47 @@ class ImportExportTest {
attachmentLocator = FilePointer.AttachmentLocator(
cdnKey = "coolCdnKey",
cdnNumber = 2,
uploadTimestamp = System.currentTimeMillis()
uploadTimestamp = System.currentTimeMillis(),
key = (1..32).map { it.toByte() }.toByteArray().toByteString(),
size = 12345,
digest = (1..32).map { it.toByte() }.toByteArray().toByteString()
),
key = (1..32).map { it.toByte() }.toByteArray().toByteString(),
contentType = "image/png",
size = 12345,
fileName = "very_cool_picture.png",
width = 100,
height = 200,
caption = "Love this cool picture!",
incrementalMacChunkSize = 0
)
),
wasDownloaded = true
),
MessageAttachment(
pointer = FilePointer(
invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator(),
contentType = "image/png",
width = 100,
height = 200,
caption = "Love this cool picture! Too bad u cant download it",
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
)
)
)
@@ -1338,7 +1387,7 @@ class ImportExportTest {
is Recipient -> writer.write(Frame(recipient = obj))
is Chat -> writer.write(Frame(chat = obj))
is ChatItem -> writer.write(Frame(chatItem = obj))
is Call -> writer.write(Frame(call = obj))
is AdHocCall -> writer.write(Frame(adHocCall = obj))
is StickerPack -> writer.write(Frame(stickerPack = obj))
else -> Assert.fail("invalid object $obj")
}
@@ -1347,27 +1396,10 @@ 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()
@@ -1377,10 +1409,12 @@ class ImportExportTest {
}
/**
* 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()
@@ -1399,18 +1433,19 @@ class ImportExportTest {
is Recipient -> writer.write(Frame(recipient = obj))
is Chat -> writer.write(Frame(chat = obj))
is ChatItem -> writer.write(Frame(chatItem = obj))
is Call -> writer.write(Frame(call = obj))
is AdHocCall -> writer.write(Frame(adHocCall = obj))
is StickerPack -> writer.write(Frame(stickerPack = obj))
else -> Assert.fail("invalid object $obj")
}
}
}
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) {
@@ -1430,8 +1465,8 @@ class ImportExportTest {
val chatsExported = ArrayList<Chat>()
val chatItemsImported = ArrayList<ChatItem>()
val chatItemsExported = ArrayList<ChatItem>()
val callsImported = ArrayList<Call>()
val callsExported = ArrayList<Call>()
val callsImported = ArrayList<AdHocCall>()
val callsExported = ArrayList<AdHocCall>()
val stickersImported = ArrayList<StickerPack>()
val stickersExported = ArrayList<StickerPack>()
@@ -1441,7 +1476,7 @@ class ImportExportTest {
f.recipient != null -> recipientsImported.add(f.recipient!!)
f.chat != null -> chatsImported.add(f.chat!!)
f.chatItem != null -> chatItemsImported.add(f.chatItem!!)
f.call != null -> callsImported.add(f.call!!)
f.adHocCall != null -> callsImported.add(f.adHocCall!!)
f.stickerPack != null -> stickersImported.add(f.stickerPack!!)
}
}
@@ -1452,7 +1487,7 @@ class ImportExportTest {
f.recipient != null -> recipientsExported.add(f.recipient!!)
f.chat != null -> chatsExported.add(f.chat!!)
f.chatItem != null -> chatItemsExported.add(f.chatItem!!)
f.call != null -> callsExported.add(f.call!!)
f.adHocCall != null -> callsExported.add(f.adHocCall!!)
f.stickerPack != null -> stickersExported.add(f.stickerPack!!)
}
}
@@ -1464,11 +1499,11 @@ class ImportExportTest {
prettyAssertEquals(stickersImported, stickersExported) { it.packId }
}
private fun <T> prettyAssertEquals(import: List<T>, export: List<T>) {
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")
}
}
}
@@ -1477,7 +1512,7 @@ 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()
for (i in import) {
@@ -1513,9 +1548,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)
@@ -1530,11 +1564,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

@@ -3,6 +3,9 @@ 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 io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Before
@@ -12,9 +15,10 @@ 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.AppDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.SvrRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
@@ -34,10 +38,14 @@ 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.kbs.MasterKey
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.svr.SecureValueRecovery
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.MismatchedDevices
import org.whispersystems.signalservice.internal.push.PreKeyState
import java.security.SecureRandom
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@@ -62,10 +70,13 @@ class ChangeNumberViewModelTest {
viewModel.setNewCountry(1)
viewModel.setNewNationalNumber("5555550102")
}
mockkObject(SvrRepository)
}
@After
fun tearDown() {
unmockkObject(SvrRepository)
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@@ -249,6 +260,8 @@ class ChangeNumberViewModelTest {
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
every { SvrRepository.restoreMasterKeyPreRegistration(any(), any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password"))
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
@@ -356,6 +369,8 @@ class ChangeNumberViewModelTest {
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
every { SvrRepository.restoreMasterKeyPreRegistration(any(), any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password"))
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
@@ -371,7 +386,7 @@ class ChangeNumberViewModelTest {
}
private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) {
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniProtocolStore = AppDependencies.protocolStore.pni()
val pniMetadataStore = SignalStore.account().pniPreKeys
Recipient.self().requireE164() assertIs "+15555550102"

View File

@@ -7,6 +7,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
import org.thoughtcrime.securesms.database.MessageType
@@ -15,7 +16,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
@@ -137,7 +137,7 @@ class ConversationItemPreviewer {
private fun attachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
Cdn.CDN_3.cdnNumber,
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
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
@@ -328,5 +329,8 @@ class V2ConversationItemShapeTest {
override fun onReportSpamLearnMoreClicked() = Unit
override fun onMessageRequestAcceptOptionsClicked() = Unit
override fun onItemDoubleClick(item: MultiselectPart) = Unit
override fun onPaymentTombstoneClicked() = Unit
}
}

View File

@@ -14,7 +14,9 @@ import org.junit.runner.RunWith
import org.signal.core.util.Base64
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
@@ -24,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
@@ -194,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.
@@ -219,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
@@ -233,6 +240,7 @@ class AttachmentTableTest_deduping {
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
}
@@ -252,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.
@@ -281,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.
@@ -296,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.
@@ -326,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.
@@ -342,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
@@ -456,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
@@ -469,6 +483,7 @@ class AttachmentTableTest_deduping {
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
}
@@ -647,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) {
@@ -742,7 +766,16 @@ class AttachmentTableTest_deduping {
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
assertEquals(lhsAttachment.cdnNumber, rhsAttachment.cdnNumber)
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) {
@@ -751,7 +784,7 @@ class AttachmentTableTest_deduping {
assertNull(databaseAttachment.remoteLocation)
assertNull(databaseAttachment.remoteDigest)
assertNull(databaseAttachment.remoteKey)
assertEquals(0, databaseAttachment.cdnNumber)
assertEquals(0, databaseAttachment.cdn.cdnNumber)
}
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
@@ -776,7 +809,7 @@ class AttachmentTableTest_deduping {
AttachmentTable.TRANSFER_PROGRESS_DONE,
databaseAttachment.size, // size
null,
3, // cdnNumber
Cdn.CDN_3, // cdnNumber
location,
key,
digest,

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

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -75,21 +74,6 @@ class GroupTableTest {
assertEquals(2, groups.size)
}
@Test
fun givenGroups_whenIQueryGroupsByMembership_thenIExpectBothGroups() {
insertPushGroup()
insertMmsGroup(members = listOf(harness.others[1]))
val groups = groupTable.queryGroupsByMembership(
setOf(harness.self.id, harness.others[1]),
includeInactive = false,
excludeV1 = false,
excludeMms = false
)
assertEquals(2, groups.cursor?.count)
}
@Test
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
insertPushGroup()
@@ -181,68 +165,6 @@ class GroupTableTest {
assertFalse(actual)
}
@Test
fun givenAGroup_whenIUpdateMembers_thenIExpectUpdatedMembers() {
val v2Group = insertPushGroup()
groupTable.updateMembers(v2Group, listOf(harness.self.id, harness.others[1]))
val groupRecord = groupTable.getGroup(v2Group)
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.get().members.toSet())
}
@Test
fun givenAnMmsGroup_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val members: List<RecipientId> = listOf(harness.self.id, harness.others[0])
val other = insertMmsGroup(members + listOf(harness.others[1]))
val mmsGroup = insertMmsGroup(members)
val actual = groupTable.getOrCreateMmsGroupForMembers(members.toSet())
assertNotEquals(other, actual)
assertEquals(mmsGroup, actual)
}
@Test
fun givenMultipleMmsGroups_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1])
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2])
val group1: GroupId = insertMmsGroup(group1Members)
val group2: GroupId = insertMmsGroup(group2Members)
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.toSet())
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.toSet())
assertEquals(group1, group1Result)
assertEquals(group2, group2Result)
assertNotEquals(group1Result, group2Result)
}
@Test
fun givenMultipleMmsGroupsWithDifferentMemberOrders_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1], harness.others[2]).shuffled()
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2], harness.others[3]).shuffled()
val group1: GroupId = insertMmsGroup(group1Members)
val group2: GroupId = insertMmsGroup(group2Members)
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.shuffled().toSet())
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.shuffled().toSet())
assertEquals(group1, group1Result)
assertEquals(group2, group2Result)
assertNotEquals(group1Result, group2Result)
}
@Test
fun givenMmsGroupWithOneMember_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val groupMembers: List<RecipientId> = listOf(harness.self.id)
val group: GroupId = insertMmsGroup(groupMembers)
val groupResult: GroupId = groupTable.getOrCreateMmsGroupForMembers(groupMembers.toSet())
assertEquals(group, groupResult)
}
@Test
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
val g1 = insertPushGroup(listOf())

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

@@ -0,0 +1,239 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIsSize
@RunWith(AndroidJUnit4::class)
class NameCollisionTablesTest {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
private lateinit var charlie: RecipientId
@Before
fun setUp() {
alice = setUpRecipient(harness.others[0])
bob = setUpRecipient(harness.others[1])
charlie = setUpRecipient(harness.others[2])
}
@Test
fun givenAUserWithAThreadIdButNoConflicts_whenIGetCollisionsForThreadRecipient_thenIExpectNoCollisions() {
val threadRecipientId = alice
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(threadRecipientId))
val actual = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(threadRecipientId)
actual assertIsSize 0
}
@Test
fun givenTwoUsers_whenOneChangesTheirProfileNameToMatchTheOther_thenIExpectANameCollision() {
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
actualAlice assertIsSize 2
actualBob assertIsSize 2
}
@Test
fun givenTwoUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectNoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
actualAlice assertIsSize 0
actualBob assertIsSize 0
}
@Test
fun givenThreeUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectTwoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(charlie, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
val actualCharlie = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(charlie)
actualAlice assertIsSize 0
actualBob assertIsSize 2
actualCharlie assertIsSize 2
}
@Test
fun givenTwoUsersWithADismissedNameCollision_whenOneChangesToADifferentNameAndBack_thenIExpectANameCollision() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
actualAlice assertIsSize 2
}
@Test
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
actualCollisions assertIsSize 0
}
@Test
fun givenADismissedNameCollisionForAliceThatIUpdate_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
actualCollisions assertIsSize 0
}
@Test
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForBob_thenIExpectANameCollisionWithTwoEntries() {
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
actualCollisions assertIsSize 2
}
@Test
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
collisions assertIsSize 2
}
@Test
fun givenAGroupWithAliceAndBobWithDismissedCollision_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(info.recipientId)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
collisions assertIsSize 0
}
@Test
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAliceWithMismatch_thenIExpectNoGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Alice", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Alice Android", "Bob Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
collisions assertIsSize 0
}
private fun setUpRecipient(recipientId: RecipientId): RecipientId {
SignalDatabase.recipients.setProfileSharing(recipientId, false)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
MmsHelper.insert(
threadId = threadId,
message = IncomingMessage(
type = MessageType.NORMAL,
from = recipientId,
groupId = null,
body = "hi",
sentTimeMillis = 100L,
receivedTimeMillis = 200L,
serverTimeMillis = 100L,
isUnidentified = true
)
)
return recipientId
}
private fun setProfileName(recipientId: RecipientId, name: ProfileName) {
SignalDatabase.recipients.setProfileName(recipientId, name)
SignalDatabase.nameCollisions.handleIndividualNameCollision(recipientId)
}
private fun createGroup(): GroupTestingUtils.TestGroupInfo {
return GroupTestingUtils.insertGroup(
revision = 0,
DecryptedMember(
aciBytes = harness.self.requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
),
DecryptedMember(
aciBytes = Recipient.resolved(alice).requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
),
DecryptedMember(
aciBytes = Recipient.resolved(bob).requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
)
)
}
}

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

@@ -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 {
@@ -101,6 +106,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,168 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.CapturingSlot
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import okio.ByteString.Companion.toByteString
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.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData
import org.thoughtcrime.securesms.messages.MessageHelper
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsSize
import org.thoughtcrime.securesms.util.MessageTableTestUtils
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SendMessageResult
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.Content
import java.util.Optional
@RunWith(AndroidJUnit4::class)
class MultiDeviceDeleteSendSyncJobTest {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var messageHelper: MessageHelper
private lateinit var success: SendMessageResult
private lateinit var failure: SendMessageResult
private lateinit var content: CapturingSlot<Content>
@Before
fun setUp() {
messageHelper = MessageHelper(harness)
mockkStatic(TextSecurePreferences::class)
every { TextSecurePreferences.isMultiDevice(any()) } answers {
true
}
success = SendMessageResult.success(SignalServiceAddress(Recipient.self().requireServiceId()), listOf(2), true, false, 0, Optional.empty())
failure = SendMessageResult.networkFailure(SignalServiceAddress(Recipient.self().requireServiceId()))
content = slot<Content>()
}
@After
fun tearDown() {
messageHelper.tearDown()
unmockkStatic(TextSecurePreferences::class)
}
@Test
fun messageDeletes() {
// GIVEN
val messages = mutableListOf<MessageHelper.MessageData>()
messages += messageHelper.incomingText()
messages += messageHelper.incomingText()
messages += messageHelper.outgoingText()
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
val records: Set<MessageRecord> = MessageTableTestUtils.getMessages(threadId).toSet()
// WHEN
every { AppDependencies.signalServiceMessageSender.sendSyncMessage(capture(content), any(), any()) } returns success
val job = MultiDeviceDeleteSendSyncJob.createMessageDeletes(records)
val result = job.run()
// THEN
result.isSuccess assertIs true
assertDeleteSync(messageHelper.alice, messages)
}
@Test
fun groupMessageDeletes() {
// GIVEN
val messages = mutableListOf<MessageHelper.MessageData>()
messages += messageHelper.incomingText(destination = messageHelper.group.recipientId)
messages += messageHelper.incomingText(destination = messageHelper.group.recipientId)
messages += messageHelper.outgoingText(conversationId = messageHelper.group.recipientId)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
val records: Set<MessageRecord> = MessageTableTestUtils.getMessages(threadId).toSet()
// WHEN
every { AppDependencies.signalServiceMessageSender.sendSyncMessage(capture(content), any(), any()) } returns success
val job = MultiDeviceDeleteSendSyncJob.createMessageDeletes(records)
val result = job.run()
// THEN
result.isSuccess assertIs true
assertDeleteSync(messageHelper.group.recipientId, messages)
}
@Test
fun retryOfDeletes() {
// GIVEN
val alice = messageHelper.alice.toLong()
// WHEN
every { AppDependencies.signalServiceMessageSender.sendSyncMessage(capture(content), any(), any()) } returns failure
val job = MultiDeviceDeleteSendSyncJob(
messages = listOf(DeleteSyncJobData.AddressableMessage(alice, 1, alice)),
threads = listOf(DeleteSyncJobData.ThreadDelete(alice, listOf(DeleteSyncJobData.AddressableMessage(alice, 1, alice)))),
localOnlyThreads = listOf(DeleteSyncJobData.ThreadDelete(alice))
)
val result = job.run()
val data = DeleteSyncJobData.ADAPTER.decode(job.serialize())
// THEN
result.isRetry assertIs true
data.messageDeletes.assertIsSize(1)
data.threadDeletes.assertIsSize(1)
data.localOnlyThreadDeletes.assertIsSize(1)
}
private fun assertDeleteSync(conversation: RecipientId, inputMessages: List<MessageHelper.MessageData>) {
val messagesMap = inputMessages.associateBy { it.timestamp }
val content = this.content.captured
content.syncMessage?.padding.assertIsNotNull()
content.syncMessage?.deleteForMe.assertIsNotNull()
val deleteForMe = content.syncMessage!!.deleteForMe!!
deleteForMe.messageDeletes.assertIsSize(1)
deleteForMe.conversationDeletes.assertIsSize(0)
deleteForMe.localOnlyConversationDeletes.assertIsSize(0)
val messageDeletes = deleteForMe.messageDeletes[0]
val conversationRecipient = Recipient.resolved(conversation)
if (conversationRecipient.isGroup) {
messageDeletes.conversation!!.threadGroupId assertIs conversationRecipient.requireGroupId().decodedId.toByteString()
} else {
messageDeletes.conversation!!.threadAci assertIs conversationRecipient.requireAci().toString()
}
messageDeletes
.messages
.forEach { delete ->
val messageData = messagesMap[delete.sentTimestamp]
delete.sentTimestamp assertIs messageData!!.timestamp
delete.authorAci assertIs Recipient.resolved(messageData.author).requireAci().toString()
}
}
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import org.thoughtcrime.securesms.database.SignalDatabase
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.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 java.util.UUID
/**
* 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
).apply { updateMessage?.invoke(this) }
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 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
}
/**
* 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,508 @@
/*
* 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 io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
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.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.assertIsNotNull
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.IdentityUtil
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class SyncMessageProcessorTest_synchronizeDeleteForMe {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var messageHelper: MessageHelper
@Before
fun setUp() {
messageHelper = MessageHelper(harness)
mockkStatic(FeatureFlags::class)
every { FeatureFlags.deleteSyncEnabled() } returns true
}
@After
fun tearDown() {
messageHelper.tearDown()
unmockkStatic(FeatureFlags::class)
}
@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, 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 localOnlyRemainingAfterConversationDeleteWithFullDelete() {
// 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 = 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 }, true),
DeleteForMeSync(conversationId = messageHelper.bob, allMessages[messageHelper.bob]!!.takeLast(5).map { it.recipientId to it.timetamp }, 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)
IdentityUtil.markIdentityUpdate(harness.context, alice.id)
// 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.calls.insertAcceptedGroupCall(4, messageHelper.group.recipientId, CallTable.Direction.INCOMING, System.currentTimeMillis())
SignalDatabase.calls.insertDeclinedGroupCall(5, messageHelper.group.recipientId, System.currentTimeMillis())
// Detected changes
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.messages.insertLearnedProfileNameChangeMessage(alice, null, "username.42")
SignalDatabase.messages.insertNumberChangeMessages(alice.id)
SignalDatabase.messages.insertSmsExportMessage(alice.id, SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!)
SignalDatabase.messages.insertSessionSwitchoverEvent(alice.id, aliceThreadId, SessionSwitchoverEvent())
// Sent failed
SignalDatabase.messages.markAsSending(messageHelper.outgoingText().messageId)
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
messageHelper.outgoingText().let {
SignalDatabase.messages.markAsSending(it.messageId)
SignalDatabase.messages.markAsRateLimited(it.messageId)
}
// Group change
messageHelper.outgoingGroupChange()
// 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.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
}
}

View File

@@ -0,0 +1,54 @@
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
@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.donationsValues().setSubscriberCurrency("USD", InAppPaymentSubscriberRecord.Type.DONATION)
SignalStore.donationsValues().setSubscriber("USD", subscriberId)
SignalStore.donationsValues().setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
SignalStore.donationsValues().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.currencyCode assertIs "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

@@ -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
@@ -37,7 +37,7 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
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

@@ -139,7 +139,9 @@ 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

View File

@@ -61,13 +61,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 +150,85 @@ 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(threadAci = conversation.requireAci().toString())
},
messages = conversationDeletes.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorAci = 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 { (conversationId, conversationDeletes, isFullDelete) ->
val conversation = Recipient.resolved(conversationId)
SyncMessage.DeleteForMe.ConversationDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadAci = conversation.requireAci().toString())
},
mostRecentMessages = conversationDeletes.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorAci = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
},
isFullDelete = 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(threadAci = conversation.requireAci().toString())
}
)
}
)
)
).build()
}
/**
* Create a random media message that may be:
* - A text body
@@ -278,7 +357,7 @@ object MessageContentFuzzer {
caption = string(allowNullString = true)
blurHash = string()
uploadTimestamp = random.nextLong()
cdnNumber = 1
cdnNumber = 2
build()
}
@@ -290,4 +369,12 @@ object MessageContentFuzzer {
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
return envelopeTimestamp + 10
}
data class DeleteForMeSync(
val conversationId: RecipientId,
val messages: List<Pair<RecipientId, Long>>,
val isFullDelete: Boolean = true
) {
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 {

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
@@ -145,7 +145,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
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

@@ -2,6 +2,7 @@ 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`
@@ -56,6 +57,10 @@ 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")

View File

@@ -2,7 +2,7 @@ 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.whispersystems.signalservice.api.SignalServiceAccountManager
@@ -16,7 +16,7 @@ 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,

View File

@@ -1,5 +1,6 @@
package org.signal.benchmark.setup
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageType
@@ -9,7 +10,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
@@ -144,7 +144,7 @@ object TestMessages {
}
private fun imageAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
Cdn.S3.cdnNumber,
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
@@ -167,7 +167,7 @@ object TestMessages {
private fun voiceAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
Cdn.S3.cdnNumber,
SignalServiceAttachmentRemoteId.from(""),
"audio/aac",
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()
@@ -104,7 +104,7 @@ object TestUsers {
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()
}
@@ -300,6 +300,14 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onItemDoubleClick(item: MultiselectPart) {
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

@@ -749,7 +749,7 @@
android:exported="false"/>
<activity
android:name=".backup.v2.ui.MessageBackupsFlowActivity"
android:name=".backup.v2.ui.subscription.MessageBackupsFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar"
@@ -783,6 +783,12 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity
android:name=".components.settings.app.changenumber.v2.ChangeNumberLockV2Activity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity
android:name=".components.settings.conversation.ConversationSettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
@@ -837,6 +843,20 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".registration.v2.ui.RegistrationV2Activity"
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=".restore.RestoreActivity"
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=".revealable.ViewOnceMessageActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.FullScreenMedia"
@@ -933,12 +953,18 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" />
</intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.videocall" />
</intent-filter>
</activity>
<activity android:name=".mediasend.AvatarSelectionActivity"
@@ -962,7 +988,7 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".backup.v2.ui.MessageBackupsTestRestoreActivity"
<activity android:name=".backup.v2.ui.subscription.MessageBackupsTestRestoreActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:exported="false"/>
@@ -1168,6 +1194,10 @@
android:name=".service.AttachmentProgressService"
android:exported="false"/>
<service
android:name=".service.BackupProgressService"
android:exported="false"/>
<service
android:name=".gcm.FcmFetchBackgroundService"
android:exported="false"/>
@@ -1288,6 +1318,12 @@
</intent-filter>
</receiver>
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
@@ -1352,7 +1388,11 @@
</intent-filter>
</receiver>
<service android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService" android:exported="false" />
<service
android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService"
android:exported="false"
android:foregroundServiceType="camera|microphone" />
<receiver android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallServiceReceiver" android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.DENY"/>

File diff suppressed because it is too large Load Diff

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

@@ -40,13 +40,14 @@ import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
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;
@@ -55,11 +56,12 @@ import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
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;
@@ -69,7 +71,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;
@@ -83,12 +84,14 @@ import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
import org.thoughtcrime.securesms.service.AnalyzeDatabaseAlarmListener;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.MessageBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
import org.thoughtcrime.securesms.service.webrtc.ActiveCallManager;
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
@@ -165,7 +168,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", () -> {
@@ -193,10 +196,10 @@ 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)
@@ -207,16 +210,17 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.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)
.addPostRender(() -> ActiveCallManager.clearNotifications(this))
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -229,11 +233,11 @@ 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();
AppDependencies.getJobManager().add(new InAppPaymentAuthCheckJob());
FcmFetchManager.onForeground(this);
startAnrDetector();
@@ -242,7 +246,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
RetrieveProfileJob.enqueueRoutineFetchIfNecessary();
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getShakeToReport().enable();
AppDependencies.getShakeToReport().enable();
checkBuildExpiration();
MemoryTracker.start();
@@ -264,10 +268,10 @@ 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();
}
@@ -350,16 +354,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() {
@@ -382,34 +386,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();
}
}
@@ -417,8 +423,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
RotateSignedPreKeyListener.schedule(this);
DirectoryRefreshListener.schedule(this);
LocalBackupListener.schedule(this);
MessageBackupListener.schedule(this);
RotateSenderCertificateListener.schedule(this);
RoutineMessageFetchReceiver.startOrUpdateAlarm(this);
AnalyzeDatabaseAlarmListener.schedule(this);
if (BuildConfig.MANAGES_APP_UPDATES) {
ApkUpdateRefreshListener.schedule(this);
@@ -442,7 +450,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) {
@@ -454,19 +462,19 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void ensureProfileUploaded() {
if (SignalStore.account().isRegistered() && !SignalStore.registrationValues().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

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

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms;
import android.net.Uri;
import android.view.GestureDetector;
import android.view.View;
import androidx.annotation.NonNull;
@@ -58,6 +59,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void setEventListener(@Nullable EventListener listener);
default void setGestureDetector(@Nullable GestureDetector gestureDetector) {
// Intentionally Blank.
}
default void setParentScrolling(boolean isParentScrolling) {
// Intentionally Blank.
}
@@ -120,11 +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

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms
import android.content.Context
import android.view.View
import android.widget.TextView
import com.google.android.material.button.MaterialButton
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
@@ -24,6 +25,8 @@ class ContactSelectionListAdapter(
init {
registerFactory(NewGroupModel::class.java, LayoutFactory({ NewGroupViewHolder(it, onClickCallbacks::onNewGroupClicked) }, R.layout.contact_selection_new_group_item))
registerFactory(InviteToSignalModel::class.java, LayoutFactory({ InviteToSignalViewHolder(it, onClickCallbacks::onInviteToSignalClicked) }, R.layout.contact_selection_invite_action_item))
registerFactory(FindContactsModel::class.java, LayoutFactory({ FindContactsViewHolder(it, onClickCallbacks::onFindContactsClicked) }, R.layout.contact_selection_find_contacts_item))
registerFactory(FindContactsBannerModel::class.java, LayoutFactory({ FindContactsBannerViewHolder(it, onClickCallbacks::onDismissFindContactsBannerClicked, onClickCallbacks::onFindContactsClicked) }, R.layout.contact_selection_find_contacts_banner_item))
registerFactory(RefreshContactsModel::class.java, LayoutFactory({ RefreshContactsViewHolder(it, onClickCallbacks::onRefreshContactsClicked) }, R.layout.contact_selection_refresh_action_item))
registerFactory(MoreHeaderModel::class.java, LayoutFactory({ MoreHeaderViewHolder(it) }, R.layout.contact_search_section_header))
registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.contact_selection_empty_state))
@@ -46,6 +49,16 @@ class ContactSelectionListAdapter(
override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true
}
class FindContactsModel : MappingModel<FindContactsModel> {
override fun areItemsTheSame(newItem: FindContactsModel): Boolean = true
override fun areContentsTheSame(newItem: FindContactsModel): Boolean = true
}
class FindContactsBannerModel : MappingModel<FindContactsBannerModel> {
override fun areItemsTheSame(newItem: FindContactsBannerModel): Boolean = true
override fun areContentsTheSame(newItem: FindContactsBannerModel): Boolean = true
}
class FindByUsernameModel : MappingModel<FindByUsernameModel> {
override fun areItemsTheSame(newItem: FindByUsernameModel): Boolean = true
override fun areContentsTheSame(newItem: FindByUsernameModel): Boolean = true
@@ -86,6 +99,23 @@ class ContactSelectionListAdapter(
override fun bind(model: RefreshContactsModel) = Unit
}
private class FindContactsViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<FindContactsModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: FindContactsModel) = Unit
}
private class FindContactsBannerViewHolder(itemView: View, onDismissListener: () -> Unit, onClickListener: () -> Unit) : MappingViewHolder<FindContactsBannerModel>(itemView) {
init {
itemView.findViewById<MaterialButton>(R.id.no_thanks_button).setOnClickListener { onDismissListener() }
itemView.findViewById<MaterialButton>(R.id.allow_contacts_button).setOnClickListener { onClickListener() }
}
override fun bind(model: FindContactsBannerModel) = Unit
}
private class MoreHeaderViewHolder(itemView: View) : MappingViewHolder<MoreHeaderModel>(itemView) {
private val headerTextView: TextView = itemView.findViewById(R.id.section_header)
@@ -129,6 +159,8 @@ class ContactSelectionListAdapter(
INVITE_TO_SIGNAL("invite-to-signal"),
MORE_HEADING("more-heading"),
REFRESH_CONTACTS("refresh-contacts"),
FIND_CONTACTS("find-contacts"),
FIND_CONTACTS_BANNER("find-contacts-banner"),
FIND_BY_USERNAME("find-by-username"),
FIND_BY_PHONE_NUMBER("find-by-phone-number");
@@ -152,6 +184,8 @@ class ContactSelectionListAdapter(
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
ArbitraryRow.FIND_CONTACTS -> FindContactsModel()
ArbitraryRow.FIND_CONTACTS_BANNER -> FindContactsBannerModel()
ArbitraryRow.FIND_BY_PHONE_NUMBER -> FindByPhoneNumberModel()
ArbitraryRow.FIND_BY_USERNAME -> FindByUsernameModel()
}
@@ -162,6 +196,8 @@ class ContactSelectionListAdapter(
fun onNewGroupClicked()
fun onInviteToSignalClicked()
fun onRefreshContactsClicked()
fun onFindContactsClicked()
fun onDismissFindContactsBannerClicked()
fun onFindByPhoneNumberClicked()
fun onFindByUsernameClicked()
}

View File

@@ -70,13 +70,13 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameAciFetchResult;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -125,10 +125,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
private TextView emptyText;
private OnContactSelectedListener onContactSelectedListener;
private SwipeRefreshLayout swipeRefresh;
private View showContactsLayout;
private Button showContactsButton;
private TextView showContactsDescription;
private ProgressWheel showContactsProgress;
private String cursorFilter;
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
@@ -223,43 +219,25 @@ public final class ContactSelectionListFragment extends LoggingFragment {
public void onStart() {
super.onStart();
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
.ifNecessary()
.onAllGranted(() -> {
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
handleContactPermissionGranted();
} else {
contactSearchMediator.refresh();
}
})
.onAnyDenied(() -> {
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
if (safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false))) {
contactSearchMediator.refresh();
} else {
initializeNoContactsPermission();
}
})
.execute();
if (hasContactsPermissions(requireContext()) && !TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
handleContactPermissionGranted();
} else {
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
contactSearchMediator.refresh();
}
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
emptyText = view.findViewById(android.R.id.empty);
recyclerView = view.findViewById(R.id.recycler_view);
swipeRefresh = view.findViewById(R.id.swipe_refresh);
fastScroller = view.findViewById(R.id.fast_scroller);
showContactsLayout = view.findViewById(R.id.show_contacts_container);
showContactsButton = view.findViewById(R.id.show_contacts_button);
showContactsDescription = view.findViewById(R.id.show_contacts_description);
showContactsProgress = view.findViewById(R.id.progress);
chipRecycler = view.findViewById(R.id.chipRecycler);
constraintLayout = view.findViewById(R.id.container);
headerActionView = view.findViewById(R.id.header_action);
emptyText = view.findViewById(android.R.id.empty);
recyclerView = view.findViewById(R.id.recycler_view);
swipeRefresh = view.findViewById(R.id.swipe_refresh);
fastScroller = view.findViewById(R.id.fast_scroller);
chipRecycler = view.findViewById(R.id.chipRecycler);
constraintLayout = view.findViewById(R.id.container);
headerActionView = view.findViewById(R.id.header_action);
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
@@ -269,6 +247,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
return true;
}
@Override
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
recyclerView.setAlpha(1f);
}
});
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
@@ -372,6 +355,19 @@ public final class ContactSelectionListFragment extends LoggingFragment {
fixedContacts,
displayOptions,
new ContactSelectionListAdapter.OnContactSelectionClick() {
@Override
public void onDismissFindContactsBannerClicked() {
SignalStore.uiHints().markDismissedContactsPermissionBanner();
if (onRefreshListener != null) {
onRefreshListener.onRefresh();
}
}
@Override
public void onFindContactsClicked() {
requestContactPermissions();
}
@Override
public void onRefreshContactsClicked() {
if (onRefreshListener != null) {
@@ -498,6 +494,27 @@ public final class ContactSelectionListFragment extends LoggingFragment {
return isMulti;
}
private void requestContactPermissions() {
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
.ifNecessary()
.onAllGranted(() -> {
recyclerView.setAlpha(0.5f);
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
handleContactPermissionGranted();
} else {
contactSearchMediator.refresh();
if (onRefreshListener != null) {
swipeRefresh.setRefreshing(true);
onRefreshListener.onRefresh();
}
}
})
.onAnyDenied(() -> contactSearchMediator.refresh())
.withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts), null, R.string.ContactSelectionListFragment_allow_access_contacts, R.string.ContactSelectionListFragment_to_find_people, getParentFragmentManager())
.execute();
}
private void initializeCursor() {
recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
recyclerView.setAdapter(contactSearchMediator.getAdapter());
@@ -521,28 +538,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
return hasQueryFilter() || shouldDisplayRecents();
}
private void initializeNoContactsPermission() {
swipeRefresh.setVisibility(View.GONE);
showContactsLayout.setVisibility(View.VISIBLE);
showContactsProgress.setVisibility(View.INVISIBLE);
showContactsDescription.setText(R.string.contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them);
showContactsButton.setVisibility(View.VISIBLE);
showContactsButton.setOnClickListener(v -> {
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts))
.onSomeGranted(permissions -> {
if (permissions.contains(Manifest.permission.WRITE_CONTACTS)) {
handleContactPermissionGranted();
}
})
.execute();
});
}
public void setQueryFilter(String filter) {
if (Objects.equals(filter, this.cursorFilter)) {
return;
@@ -583,7 +578,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
}
swipeRefresh.setVisibility(View.VISIBLE);
showContactsLayout.setVisibility(View.GONE);
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
boolean useFastScroller = count > 20;
@@ -614,12 +608,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
new AsyncTask<Void, Void, Boolean>() {
@Override
protected void onPreExecute() {
swipeRefresh.setVisibility(View.GONE);
showContactsLayout.setVisibility(View.VISIBLE);
showContactsButton.setVisibility(View.INVISIBLE);
showContactsDescription.setText(R.string.ConversationListFragment_loading);
showContactsProgress.setVisibility(View.VISIBLE);
showContactsProgress.spin();
if (onRefreshListener != null) {
setRefreshing(true);
onRefreshListener.onRefresh();
}
}
@Override
@@ -636,14 +628,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
@Override
protected void onPostExecute(Boolean result) {
if (result) {
showContactsLayout.setVisibility(View.GONE);
swipeRefresh.setVisibility(View.VISIBLE);
reset();
} else {
Context context = getContext();
if (context != null) {
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
initializeNoContactsPermission();
}
}
}
@@ -890,6 +879,13 @@ public final class ContactSelectionListFragment extends LoggingFragment {
return ContactSearchConfiguration.build(builder -> {
builder.setQuery(contactSearchState.getQuery());
if (newConversationCallback != null &&
!hasContactsPermissions(requireContext()) &&
!SignalStore.uiHints().getDismissedContactsPermissionBanner() &&
!hasQuery) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_CONTACTS_BANNER.getCode());
}
if (newConversationCallback != null && !hasQuery) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
}
@@ -946,7 +942,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
builder.username(newRowMode);
}
if ((newCallCallback != null || newConversationCallback != null) && !hasQuery) {
if ((newCallCallback != null || newConversationCallback != null)) {
addMoreSection(builder);
builder.withEmptyState(emptyBuilder -> {
emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
@@ -959,9 +955,17 @@ public final class ContactSelectionListFragment extends LoggingFragment {
});
}
private boolean hasContactsPermissions(@NonNull Context context) {
return Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS);
}
private void addMoreSection(@NonNull ContactSearchConfiguration.Builder builder) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.MORE_HEADING.getCode());
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode());
if (hasContactsPermissions(requireContext())) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode());
} else if (SignalStore.uiHints().getDismissedContactsPermissionBanner()) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_CONTACTS.getCode());
}
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode());
}

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;
@@ -131,14 +131,15 @@ public class DeviceActivity extends PassphraseRequiredActivity
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_scan_qr_code_allow_camera), R.drawable.symbol_camera_24)
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getSupportFragmentManager())
.onAllGranted(() -> {
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, deviceAddFragment)
.addToBackStack(null)
.commitAllowingStateLoss();
})
.onAnyDenied(() -> Toast.makeText(this, R.string.DeviceActivity_unable_to_scan_a_qr_code_without_the_camera_permission, Toast.LENGTH_LONG).show())
.onAnyDenied(() -> Toast.makeText(this, R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show())
.execute();
}
@@ -190,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

@@ -334,13 +334,7 @@ public class NewConversationActivity extends ContactSelectionActivity
R.drawable.ic_minus_circle_20, // TODO [alex] -- correct asset
getString(R.string.NewConversationActivity__remove),
R.color.signal_colorOnSurface,
() -> {
if (recipient.isSystemContact()) {
displayIsInSystemContactsDialog(recipient);
} else {
displayRemovalDialog(recipient);
}
}
() -> displayRemovalDialog(recipient)
);
}

View File

@@ -16,8 +16,9 @@ import org.signal.core.util.logging.Log;
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.components.settings.app.changenumber.v2.ChangeNumberLockV2Activity;
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.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
@@ -28,8 +29,11 @@ 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.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.TextSecurePreferences;
import java.util.Locale;
@@ -51,6 +55,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 SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -59,7 +64,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);
@@ -88,7 +93,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();
}
@@ -125,8 +130,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private void routeApplicationState(boolean locked) {
Intent intent = getIntentForState(getApplicationState(locked));
final int applicationState = getApplicationState(locked);
Intent intent = getIntentForState(applicationState);
if (intent != null) {
Log.d(TAG, "routeApplicationState(), intent: " + intent.getComponent());
startActivity(intent);
finish();
}
@@ -146,6 +153,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();
default: return null;
}
}
@@ -159,6 +167,8 @@ 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()) {
@@ -171,7 +181,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_TRANSFER_ONGOING;
} else if (SignalStore.misc().isOldDeviceTransferLocked()) {
return STATE_TRANSFER_LOCKED;
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class && getClass() != ChangeNumberLockV2Activity.class) {
return STATE_CHANGE_NUMBER_LOCK;
} else {
return STATE_NORMAL;
@@ -196,7 +206,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;
}
@@ -208,7 +218,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private Intent getPushRegistrationIntent() {
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
if (FeatureFlags.registrationV2()) {
return RegistrationV2Activity.newIntentForNewRegistration(this, getIntent());
} else {
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
}
}
private Intent getEnterSignalPinIntent() {
@@ -227,6 +241,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return getRoutedIntent(CreateSvrPinActivity.class, intent);
}
private Intent getRestoreIntent() {
Intent intent = RestoreActivity.getIntentForRestore(this);
return getRoutedIntent(intent, getIntent());
}
private Intent getCreateProfileNameIntent() {
Intent intent = CreateProfileActivity.getIntentForUserProfile(this);
return getRoutedIntent(intent, getIntent());
@@ -246,7 +265,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private Intent getChangeNumberLockIntent() {
return ChangeNumberLockActivity.createIntent(this);
if (FeatureFlags.registrationV2()) {
return ChangeNumberLockV2Activity.createIntent(this);
} else {
return ChangeNumberLockActivity.createIntent(this);
}
}
private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) {

View File

@@ -29,8 +29,10 @@ 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;
@@ -61,6 +63,7 @@ import org.thoughtcrime.securesms.components.webrtc.CallLinkProfileKeySender;
import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.CallReactionScrubber;
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
@@ -78,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;
@@ -114,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 {
@@ -162,7 +166,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private ControlsAndInfoController controlsAndInfo;
private boolean enterPipOnResume;
private long lastProcessedIntentTimestamp;
private WebRtcViewModel previousEvent = null;
private boolean isAskingForPermission;
private Disposable ephemeralStateDisposable = Disposable.empty();
@Override
@@ -222,6 +227,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
processIntent(getIntent());
registerSystemPipChangeListeners();
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
windowInfoTrackerCallbackAdapter = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
@@ -232,16 +239,29 @@ 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() {
addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfo -> {
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
CallReactionScrubber.dismissCustomEmojiBottomSheet(getSupportFragmentManager());
});
}
@Override
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
@@ -287,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();
@@ -307,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();
}
}
}
@@ -356,8 +376,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
return false;
}
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
return true;
}
if (Build.VERSION.SDK_INT >= 31) {
@@ -422,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();
@@ -434,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();
@@ -502,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 -> {
@@ -633,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));
@@ -719,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) {
@@ -817,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();
callScreen.enableRingGroup(canRing);
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
AppDependencies.getSignalCallManager().setRingGroup(canRing);
}
private void updateSpeakerHint(boolean showSpeakerHint) {
@@ -847,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());
}
@@ -861,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();
@@ -885,7 +886,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event);
Log.i(TAG, "Got message from service: " + event.describeDifference(previousEvent));
previousEvent = event;
viewModel.setRecipient(event.getRecipient());
callScreen.setRecipient(event.getRecipient());
@@ -980,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();
@@ -1002,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();
}
@@ -1026,6 +1107,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
}
@Override
public void onAudioPermissionsRequested(Runnable onGranted) {
askAudioPermissions(onGranted);
}
@Override
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
maybeDisplaySpeakerphonePopup(audioOutput);
@@ -1051,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
@@ -1061,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
@@ -1114,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);
}
}
@@ -1147,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

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,6 +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 restore complete.")
}
private fun cumulativeHashCode(): Int {
@@ -61,6 +65,6 @@ class SignalBackupAgent : BackupAgent() {
}
companion object {
private const val TAG = "SignalBackupAgent"
private val TAG = Log.tag(SignalBackupAgent::class)
}
}

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
@@ -41,7 +41,7 @@ object ApkUpdateInstaller {
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
}
@@ -61,7 +61,7 @@ object ApkUpdateInstaller {
}
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
}
@@ -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

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

@@ -0,0 +1,98 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
class ArchivedAttachment : Attachment {
@JvmField
val archiveCdn: Int
@JvmField
val archiveMediaName: String
@JvmField
val archiveMediaId: String
@JvmField
val archiveThumbnailMediaId: String
constructor(
contentType: String?,
size: Long,
cdn: Int,
key: ByteArray,
cdnKey: String?,
archiveCdn: Int?,
archiveMediaName: String,
archiveMediaId: String,
archiveThumbnailMediaId: String,
digest: ByteArray,
incrementalMac: ByteArray?,
incrementalMacChunkSize: Int?,
width: Int?,
height: Int?,
caption: String?,
blurHash: String?,
voiceNote: Boolean,
borderless: Boolean,
gif: Boolean,
quote: Boolean
) : super(
contentType = contentType ?: "",
quote = quote,
transferState = AttachmentTable.TRANSFER_NEEDS_RESTORE,
size = size,
fileName = null,
cdn = Cdn.fromCdnNumber(cdn),
remoteLocation = cdnKey,
remoteKey = Base64.encodeWithoutPadding(key),
remoteDigest = digest,
incrementalDigest = incrementalMac,
fastPreflightId = null,
voiceNote = voiceNote,
borderless = borderless,
videoGif = gif,
width = width ?: 0,
height = height ?: 0,
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
uploadTimestamp = 0,
caption = caption,
stickerLocator = null,
blurHash = BlurHash.parseOrNull(blurHash),
audioHash = null,
transformProperties = null
) {
this.archiveCdn = archiveCdn ?: Cdn.CDN_3.cdnNumber
this.archiveMediaName = archiveMediaName
this.archiveMediaId = archiveMediaId
this.archiveThumbnailMediaId = archiveThumbnailMediaId
}
constructor(parcel: Parcel) : super(parcel) {
archiveCdn = parcel.readInt()
archiveMediaName = parcel.readString()!!
archiveMediaId = parcel.readString()!!
archiveThumbnailMediaId = parcel.readString()!!
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeInt(archiveCdn)
dest.writeString(archiveMediaName)
dest.writeString(archiveMediaId)
dest.writeString(archiveThumbnailMediaId)
}
override val uri: Uri? = null
override val publicUri: Uri? = null
override val thumbnailUri: Uri? = null
}

View File

@@ -29,7 +29,7 @@ abstract class Attachment(
@JvmField
val fileName: String?,
@JvmField
val cdnNumber: Int,
val cdn: Cdn,
@JvmField
val remoteLocation: String?,
@JvmField
@@ -70,13 +70,16 @@ abstract class Attachment(
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()!!,
transferState = parcel.readInt(),
size = parcel.readLong(),
fileName = parcel.readString(),
cdnNumber = parcel.readInt(),
cdn = Cdn.deserialize(parcel.readInt()),
remoteLocation = parcel.readString(),
remoteKey = parcel.readString(),
remoteDigest = ParcelUtil.readByteArray(parcel),
@@ -103,7 +106,7 @@ abstract class Attachment(
dest.writeInt(transferState)
dest.writeLong(size)
dest.writeString(fileName)
dest.writeInt(cdnNumber)
dest.writeInt(cdn.serialize())
dest.writeString(remoteLocation)
dest.writeString(remoteKey)
ParcelUtil.writeByteArray(dest, remoteDigest)
@@ -129,7 +132,7 @@ abstract class Attachment(
}
val isInProgress: Boolean
get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE && transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED
val isPermanentlyFailed: Boolean
get() = transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE

View File

@@ -17,7 +17,8 @@ object AttachmentCreator : Parcelable.Creator<Attachment> {
DATABASE(DatabaseAttachment::class.java, "database"),
POINTER(PointerAttachment::class.java, "pointer"),
TOMBSTONE(TombstoneAttachment::class.java, "tombstone"),
URI(UriAttachment::class.java, "uri")
URI(UriAttachment::class.java, "uri"),
ARCHIVED(ArchivedAttachment::class.java, "archived")
}
@JvmStatic
@@ -34,6 +35,7 @@ object AttachmentCreator : Parcelable.Creator<Attachment> {
Subclass.POINTER -> PointerAttachment(source)
Subclass.TOMBSTONE -> TombstoneAttachment(source)
Subclass.URI -> UriAttachment(source)
Subclass.ARCHIVED -> ArchivedAttachment(source)
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.attachments
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import org.signal.core.util.logging.Log
import org.signal.protos.resumableuploads.ResumableUpload
import org.thoughtcrime.securesms.blurhash.BlurHashEncoder
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.MediaUtil
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
import java.io.IOException
import java.util.Objects
/**
* A place collect common attachment upload operations to allow for code reuse.
*/
object AttachmentUploadUtil {
private val TAG = Log.tag(AttachmentUploadUtil::class.java)
/**
* Builds a [SignalServiceAttachmentStream] from the provided data, which can then be provided to various upload methods.
*/
@Throws(IOException::class)
fun buildSignalServiceAttachmentStream(
context: Context,
attachment: Attachment,
uploadSpec: ResumableUpload,
cancellationSignal: (() -> Boolean)? = null,
progressListener: ProgressListener? = null
): SignalServiceAttachmentStream {
val inputStream = PartAuthority.getAttachmentStream(context, attachment.uri!!)
val builder = SignalServiceAttachment.newStreamBuilder()
.withStream(inputStream)
.withContentType(attachment.contentType)
.withLength(attachment.size)
.withFileName(attachment.fileName)
.withVoiceNote(attachment.voiceNote)
.withBorderless(attachment.borderless)
.withGif(attachment.videoGif)
.withFaststart(attachment.transformProperties?.mp4FastStart ?: false)
.withWidth(attachment.width)
.withHeight(attachment.height)
.withUploadTimestamp(System.currentTimeMillis())
.withCaption(attachment.caption)
.withResumableUploadSpec(ResumableUploadSpec.from(uploadSpec))
.withCancelationSignal(cancellationSignal)
.withListener(progressListener)
if (MediaUtil.isImageType(attachment.contentType)) {
builder.withBlurHash(getImageBlurHash(context, attachment))
} else if (MediaUtil.isVideoType(attachment.contentType)) {
builder.withBlurHash(getVideoBlurHash(context, attachment))
}
return builder.build()
}
@Throws(IOException::class)
private fun getImageBlurHash(context: Context, attachment: Attachment): String? {
if (attachment.blurHash != null) {
return attachment.blurHash!!.hash
}
if (attachment.uri == null) {
return null
}
return PartAuthority.getAttachmentStream(context, attachment.uri!!).use { inputStream ->
BlurHashEncoder.encode(inputStream)
}
}
@Throws(IOException::class)
private fun getVideoBlurHash(context: Context, attachment: Attachment): String? {
if (attachment.blurHash != null) {
return attachment.blurHash.hash
}
if (Build.VERSION.SDK_INT < 23) {
Log.w(TAG, "Video thumbnails not supported...")
return null
}
return MediaUtil.getVideoThumbnail(context, Objects.requireNonNull(attachment.uri), 1000)?.let { bitmap ->
val thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false)
bitmap.recycle()
Log.i(TAG, "Generated video thumbnail...")
val hash = BlurHashEncoder.encode(thumb)
thumb.recycle()
hash
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.attachments
import org.signal.core.util.IntSerializer
/**
* Attachments/media can come from and go to multiple CDN locations depending on when and where
* they were uploaded. This class represents the CDNs where attachments/media can live.
*/
enum class Cdn(private val value: Int) {
S3(-1),
CDN_0(0),
CDN_2(2),
CDN_3(3);
val cdnNumber: Int
get() {
return when (this) {
S3 -> -1
CDN_0 -> 0
CDN_2 -> 2
CDN_3 -> 3
}
}
fun serialize(): Int {
return Serializer.serialize(this)
}
companion object Serializer : IntSerializer<Cdn> {
override fun serialize(data: Cdn): Int {
return data.value
}
override fun deserialize(data: Int): Cdn {
return values().first { it.value == data }
}
fun fromCdnNumber(cdnNumber: Int): Cdn {
return when (cdnNumber) {
-1 -> S3
0 -> CDN_0
2 -> CDN_2
3 -> CDN_3
else -> throw UnsupportedOperationException()
}
}
}
}

View File

@@ -5,10 +5,10 @@ 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
class DatabaseAttachment : Attachment {
@@ -25,6 +25,22 @@ class DatabaseAttachment : Attachment {
@JvmField
val dataHash: String?
@JvmField
val archiveCdn: Int
@JvmField
val archiveThumbnailCdn: Int
@JvmField
val archiveMediaName: String?
@JvmField
val archiveMediaId: String?
@JvmField
val thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState
private val hasArchiveThumbnail: Boolean
private val hasThumbnail: Boolean
val displayOrder: Int
@@ -33,11 +49,12 @@ class DatabaseAttachment : Attachment {
mmsId: Long,
hasData: Boolean,
hasThumbnail: Boolean,
hasArchiveThumbnail: Boolean,
contentType: String?,
transferProgress: Int,
size: Long,
fileName: String?,
cdnNumber: Int,
cdn: Cdn,
location: String?,
key: String?,
digest: ByteArray?,
@@ -57,13 +74,18 @@ class DatabaseAttachment : Attachment {
transformProperties: TransformProperties?,
displayOrder: Int,
uploadTimestamp: Long,
dataHash: String?
dataHash: String?,
archiveCdn: Int,
archiveThumbnailCdn: Int,
archiveMediaName: String?,
archiveMediaId: String?,
thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState
) : super(
contentType = contentType!!,
transferState = transferProgress,
size = size,
fileName = fileName,
cdnNumber = cdnNumber,
cdn = cdn,
remoteLocation = location,
remoteKey = key,
remoteDigest = digest,
@@ -87,7 +109,13 @@ class DatabaseAttachment : Attachment {
this.hasData = hasData
this.dataHash = dataHash
this.hasThumbnail = hasThumbnail
this.hasArchiveThumbnail = hasArchiveThumbnail
this.displayOrder = displayOrder
this.archiveCdn = archiveCdn
this.archiveThumbnailCdn = archiveThumbnailCdn
this.archiveMediaName = archiveMediaName
this.archiveMediaId = archiveMediaId
this.thumbnailRestoreState = thumbnailRestoreState
}
constructor(parcel: Parcel) : super(parcel) {
@@ -97,6 +125,12 @@ class DatabaseAttachment : Attachment {
hasThumbnail = ParcelUtil.readBoolean(parcel)
mmsId = parcel.readLong()
displayOrder = parcel.readInt()
archiveCdn = parcel.readInt()
archiveThumbnailCdn = parcel.readInt()
archiveMediaName = parcel.readString()
archiveMediaId = parcel.readString()
hasArchiveThumbnail = ParcelUtil.readBoolean(parcel)
thumbnailRestoreState = AttachmentTable.ThumbnailRestoreState.deserialize(parcel.readInt())
}
override fun writeToParcel(dest: Parcel, flags: Int) {
@@ -107,10 +141,16 @@ class DatabaseAttachment : Attachment {
ParcelUtil.writeBoolean(dest, hasThumbnail)
dest.writeLong(mmsId)
dest.writeInt(displayOrder)
dest.writeInt(archiveCdn)
dest.writeInt(archiveThumbnailCdn)
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
@@ -123,9 +163,16 @@ class DatabaseAttachment : Attachment {
null
}
override val thumbnailUri: Uri?
get() = if (hasArchiveThumbnail) {
PartAuthority.getAttachmentThumbnailUri(attachmentId)
} else {
null
}
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

@@ -9,7 +9,6 @@ import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.whispersystems.signalservice.api.InvalidMessageStructureException
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil
import org.whispersystems.signalservice.internal.push.DataMessage
import java.util.Optional
@@ -21,7 +20,7 @@ class PointerAttachment : Attachment {
transferState: Int,
size: Long,
fileName: String?,
cdnNumber: Int,
cdn: Cdn,
location: String,
key: String?,
digest: ByteArray?,
@@ -42,7 +41,7 @@ class PointerAttachment : Attachment {
transferState = transferState,
size = size,
fileName = fileName,
cdnNumber = cdnNumber,
cdn = cdn,
remoteLocation = location,
remoteKey = key,
remoteDigest = digest,
@@ -67,6 +66,7 @@ class PointerAttachment : Attachment {
override val uri: Uri? = null
override val publicUri: Uri? = null
override val thumbnailUri: Uri? = null
companion object {
@JvmStatic
@@ -83,7 +83,7 @@ class PointerAttachment : Attachment {
@JvmStatic
@JvmOverloads
fun forPointer(pointer: Optional<SignalServiceAttachment>, stickerLocator: StickerLocator? = null, fastPreflightId: String? = null): Optional<Attachment> {
fun forPointer(pointer: Optional<SignalServiceAttachment>, stickerLocator: StickerLocator? = null, fastPreflightId: String? = null, transferState: Int = AttachmentTable.TRANSFER_PROGRESS_PENDING): Optional<Attachment> {
if (!pointer.isPresent || !pointer.get().isPointer) {
return Optional.empty()
}
@@ -97,10 +97,10 @@ class PointerAttachment : Attachment {
return Optional.of(
PointerAttachment(
contentType = pointer.get().contentType,
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
transferState = transferState,
size = pointer.get().asPointer().size.orElse(0).toLong(),
fileName = pointer.get().asPointer().fileName.orElse(null),
cdnNumber = pointer.get().asPointer().cdnNumber,
cdn = Cdn.fromCdnNumber(pointer.get().asPointer().cdnNumber),
location = pointer.get().asPointer().remoteId.toString(),
key = encodedKey,
digest = pointer.get().asPointer().digest.orElse(null),
@@ -120,35 +120,6 @@ class PointerAttachment : Attachment {
)
}
fun forPointer(pointer: SignalServiceDataMessage.Quote.QuotedAttachment): Optional<Attachment> {
val thumbnail = pointer.thumbnail
return Optional.of(
PointerAttachment(
contentType = pointer.contentType,
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(),
fileName = pointer.fileName,
cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0,
location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0",
key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null,
digest = thumbnail?.asPointer()?.digest?.orElse(null),
incrementalDigest = thumbnail?.asPointer()?.incrementalDigest?.orElse(null),
incrementalMacChunkSize = thumbnail?.asPointer()?.incrementalMacChunkSize ?: 0,
fastPreflightId = null,
voiceNote = false,
borderless = false,
videoGif = false,
width = thumbnail?.asPointer()?.width ?: 0,
height = thumbnail?.asPointer()?.height ?: 0,
uploadTimestamp = thumbnail?.asPointer()?.uploadTimestamp ?: 0,
caption = thumbnail?.asPointer()?.caption?.orElse(null),
stickerLocator = null,
blurHash = null
)
)
}
fun forPointer(quotedAttachment: DataMessage.Quote.QuotedAttachment): Optional<Attachment> {
val thumbnail: SignalServiceAttachment? = try {
if (quotedAttachment.thumbnail != null) {
@@ -166,7 +137,7 @@ class PointerAttachment : Attachment {
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(),
fileName = quotedAttachment.fileName,
cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0,
cdn = Cdn.fromCdnNumber(thumbnail?.asPointer()?.cdnNumber ?: 0),
location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0",
key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null,
digest = thumbnail?.asPointer()?.digest?.orElse(null),

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
/**
@@ -17,7 +18,7 @@ class TombstoneAttachment : Attachment {
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
size = 0,
fileName = null,
cdnNumber = 0,
cdn = Cdn.CDN_0,
remoteLocation = null,
remoteKey = null,
remoteDigest = null,
@@ -37,8 +38,47 @@ class TombstoneAttachment : Attachment {
transformProperties = null
)
constructor(
contentType: String?,
incrementalMac: ByteArray?,
incrementalMacChunkSize: Int?,
width: Int?,
height: Int?,
caption: String?,
blurHash: String?,
voiceNote: Boolean = false,
borderless: Boolean = false,
gif: Boolean = false,
quote: Boolean
) : super(
contentType = contentType ?: "",
quote = quote,
transferState = AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE,
size = 0,
fileName = null,
cdn = Cdn.CDN_0,
remoteLocation = null,
remoteKey = null,
remoteDigest = null,
incrementalDigest = incrementalMac,
fastPreflightId = null,
voiceNote = voiceNote,
borderless = borderless,
videoGif = gif,
width = width ?: 0,
height = height ?: 0,
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
uploadTimestamp = 0,
caption = caption,
stickerLocator = null,
blurHash = BlurHash.parseOrNull(blurHash),
audioHash = null,
transformProperties = null
)
constructor(parcel: Parcel) : super(parcel)
override val uri: Uri? = null
override val publicUri: Uri? = null
override val thumbnailUri: Uri? = null
}

View File

@@ -69,7 +69,7 @@ class UriAttachment : Attachment {
transferState = transferState,
size = size,
fileName = fileName,
cdnNumber = 0,
cdn = Cdn.CDN_0,
remoteLocation = null,
remoteKey = null,
remoteDigest = null,
@@ -98,6 +98,7 @@ class UriAttachment : Attachment {
override val uri: Uri
override val publicUri: Uri? = null
override val thumbnailUri: Uri? = null
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)

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

@@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
import org.thoughtcrime.securesms.groups.ParcelableGroupId
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -222,34 +222,28 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
@Suppress("DEPRECATION")
private fun openCameraCapture() {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.onAllGranted {
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
}
.onAnyDenied {
Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT)
.show()
}
.execute()
if (CameraXUtil.isSupported()) {
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
} else {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.onAllGranted {
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
}
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_allow_camera), R.drawable.symbol_camera_24)
.withPermanentDenialDialog(getString(R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos, getParentFragmentManager())
.onAnyDenied { Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT).show() }
.execute()
}
}
@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

@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.OneTimePreKeyTable;
import org.thoughtcrime.securesms.database.PendingRetryReceiptTable;
import org.thoughtcrime.securesms.database.ReactionTable;
import org.thoughtcrime.securesms.database.RemappedRecordTables;
import org.thoughtcrime.securesms.database.SearchTable;
import org.thoughtcrime.securesms.database.SenderKeyTable;
import org.thoughtcrime.securesms.database.SenderKeySharedTable;
@@ -44,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;
@@ -92,7 +93,9 @@ public class FullBackupExporter extends FullBackupBase {
SenderKeyTable.TABLE_NAME,
SenderKeySharedTable.TABLE_NAME,
PendingRetryReceiptTable.TABLE_NAME,
AvatarPickerDatabase.TABLE_NAME
AvatarPickerDatabase.TABLE_NAME,
RemappedRecordTables.Recipients.TABLE_NAME,
RemappedRecordTables.Threads.TABLE_NAME
);
public static BackupEvent export(@NonNull Context context,
@@ -232,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)) {
@@ -533,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,7 +31,7 @@ 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;
@@ -98,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();
@@ -287,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

@@ -0,0 +1,36 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup
import org.signal.core.util.LongSerializer
enum class RestoreState(val id: Int, val inProgress: Boolean) {
FAILED(-1, false),
NONE(0, false),
PENDING(1, true),
RESTORING_DB(2, true),
RESTORING_MEDIA(3, true);
companion object {
val serializer: LongSerializer<RestoreState> = Serializer()
}
class Serializer : LongSerializer<RestoreState> {
override fun serialize(data: RestoreState): Long {
return data.id.toLong()
}
override fun deserialize(data: Long): RestoreState {
return when (data.toInt()) {
FAILED.id -> FAILED
PENDING.id -> PENDING
RESTORING_DB.id -> RESTORING_DB
RESTORING_MEDIA.id -> RESTORING_MEDIA
else -> NONE
}
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.signal.core.util.LongSerializer
/**
* Describes how often a users messages are backed up.
*/
enum class BackupFrequency(val id: Int) {
DAILY(0),
WEEKLY(1),
MONTHLY(2),
MANUAL(-1);
companion object Serializer : LongSerializer<BackupFrequency> {
override fun serialize(data: BackupFrequency): Long {
return data.id.toLong()
}
override fun deserialize(data: Long): BackupFrequency {
return when (data.toInt()) {
MANUAL.id -> MANUAL
DAILY.id -> DAILY
WEEKLY.id -> WEEKLY
MONTHLY.id -> MONTHLY
else -> MANUAL
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 Signal Messenger, LLC
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2
import org.signal.core.util.Base64
import org.signal.core.util.EventTimer
import org.signal.core.util.LongSerializer
import org.signal.core.util.logging.Log
import org.signal.core.util.withinTransaction
import org.signal.libsignal.messagebackup.MessageBackup
@@ -14,11 +15,14 @@ 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.profiles.ProfileKey
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.database.ChatItemImportInserter
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
import org.thoughtcrime.securesms.backup.v2.processor.CallLogBackupProcessor
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
@@ -29,25 +33,31 @@ 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.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.StatusCodeErrorAction
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
import org.whispersystems.signalservice.api.archive.ArchiveMediaRequest
import org.whispersystems.signalservice.api.archive.ArchiveMediaResponse
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
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.http.ResumableUploadSpec
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import kotlin.time.Duration.Companion.milliseconds
object BackupRepository {
@@ -55,10 +65,21 @@ object BackupRepository {
private val TAG = Log.tag(BackupRepository::class.java)
private const val VERSION = 1L
fun export(plaintext: Boolean = false): ByteArray {
val eventTimer = EventTimer()
private val resetInitializedStateErrorAction: StatusCodeErrorAction = { error ->
when (error.code) {
401 -> {
Log.i(TAG, "Resetting initialized state due to 401.")
SignalStore.backup().backupsInitialized = false
}
403 -> {
Log.i(TAG, "Bad auth credential. Clearing stored credentials.")
SignalStore.backup().clearAllCredentials()
}
}
}
val outputStream = ByteArrayOutputStream()
fun export(outputStream: OutputStream, append: (ByteArray) -> Unit, plaintext: Boolean = false) {
val eventTimer = EventTimer()
val writer: BackupExportWriter = if (plaintext) {
PlainTextBackupWriter(outputStream)
} else {
@@ -66,11 +87,11 @@ object BackupRepository {
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
aci = SignalStore.account().aci!!,
outputStream = outputStream,
append = { mac -> outputStream.write(mac) }
append = append
)
}
val exportState = ExportState(System.currentTimeMillis())
val exportState = ExportState(backupTime = System.currentTimeMillis(), allowMediaBackup = true)
writer.use {
writer.write(
@@ -97,7 +118,7 @@ object BackupRepository {
eventTimer.emit("thread")
}
CallLogBackupProcessor.export { frame ->
AdHocCallBackupProcessor.export { frame ->
writer.write(frame)
eventTimer.emit("call")
}
@@ -110,7 +131,11 @@ object BackupRepository {
}
Log.d(TAG, "export() ${eventTimer.stop().summary}")
}
fun export(plaintext: Boolean = false): ByteArray {
val outputStream = ByteArrayOutputStream()
export(outputStream = outputStream, append = { mac -> outputStream.write(mac) }, plaintext = plaintext)
return outputStream.toByteArray()
}
@@ -124,11 +149,13 @@ object BackupRepository {
fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false) {
val eventTimer = EventTimer()
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val frameReader = if (plaintext) {
PlainTextBackupReader(inputStreamFactory())
} else {
EncryptedBackupReader(
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
key = backupKey,
aci = selfData.aci,
streamLength = length,
dataStream = inputStreamFactory
@@ -160,7 +187,7 @@ object BackupRepository {
SignalDatabase.recipients.setProfileSharing(selfId, true)
eventTimer.emit("setup")
val backupState = BackupState()
val backupState = BackupState(backupKey)
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
for (frame in frameReader) {
@@ -180,8 +207,8 @@ object BackupRepository {
eventTimer.emit("chat")
}
frame.call != null -> {
CallLogBackupProcessor.import(frame.call, backupState)
frame.adHocCall != null -> {
AdHocCallBackupProcessor.import(frame.adHocCall, backupState)
eventTimer.emit("call")
}
@@ -208,28 +235,42 @@ 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))
}
}
Log.d(TAG, "import() ${eventTimer.stop().summary}")
}
fun listRemoteMediaObjects(limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.getArchiveMediaItemsPage(backupKey, credential, limit, cursor)
}
}
fun getRemoteBackupUsedSpace(): NetworkResult<Long?> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.getBackupInfo(backupKey, credential)
.map { it.usedSpace }
}
}
/**
* Returns an object with details about the remote backup state.
*/
fun getRemoteBackupState(): NetworkResult<BackupMetadata> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.setPublicKey(backupKey, credential)
.also { Log.i(TAG, "PublicKeyResult: $it") }
.map { credential }
}
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.getBackupInfo(backupKey, credential)
.map { it to credential }
@@ -253,17 +294,10 @@ object BackupRepository {
* @return True if successful, otherwise false.
*/
fun uploadBackupFile(backupStream: InputStream, backupStreamLength: Long): Boolean {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.setPublicKey(backupKey, credential)
.also { Log.i(TAG, "PublicKeyResult: $it") }
.map { credential }
}
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.getMessageBackupUploadForm(backupKey, credential)
.also { Log.i(TAG, "UploadFormResult: $it") }
@@ -281,67 +315,148 @@ object BackupRepository {
.also { Log.i(TAG, "OverallResult: $it") } is NetworkResult.Success
}
fun downloadBackupFile(destination: File, listener: ProgressListener? = null): Boolean {
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 } }
.map { pair ->
val (cdnCredentials, info) = pair
val messageReceiver = AppDependencies.signalServiceMessageReceiver
messageReceiver.retrieveBackup(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}", destination, listener)
} is NetworkResult.Success
}
/**
* Returns an object with details about the remote backup state.
*/
fun debugGetArchivedMediaState(): NetworkResult<List<ArchiveGetMediaItemsResponse.StoredMediaObject>> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.debugGetUploadedMediaItemMetadata(backupKey, credential)
}
}
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
/**
* Retrieves an upload spec that can be used to upload attachment media.
*/
fun getMediaUploadSpec(secretKey: ByteArray? = null): NetworkResult<ResumableUploadSpec> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
item = attachment.toArchiveMediaRequest(backupKey)
)
api.getMediaUploadForm(backupKey, credential)
}
.then { form ->
api.getResumableUploadSpec(form, secretKey)
}
.also { Log.i(TAG, "backupMediaResult: $it") }
}
fun archiveMedia(attachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResponse> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
fun archiveThumbnail(thumbnailAttachment: Attachment, parentAttachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val request = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey)
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
items = attachments.map { it.toArchiveMediaRequest(backupKey) }
item = request
)
}
.also { Log.i(TAG, "backupMediaResult: $it") }
}
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<Unit> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
val mediaName = attachment.getMediaName()
val request = attachment.toArchiveMediaRequest(mediaName, backupKey)
api
.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
item = request
)
.map { Triple(mediaName, request.mediaId, it) }
}
.map { (mediaName, mediaId, response) ->
val thumbnailId = backupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode()
SignalDatabase.attachments.setArchiveData(attachmentId = attachment.attachmentId, archiveCdn = response.cdn, archiveMediaName = mediaName.name, archiveMediaId = mediaId, archiveThumbnailMediaId = thumbnailId)
}
.also { Log.i(TAG, "archiveMediaResult: $it") }
}
fun archiveMedia(databaseAttachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResult> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
val requests = mutableListOf<ArchiveMediaRequest>()
val mediaIdToAttachmentId = mutableMapOf<String, AttachmentId>()
val attachmentIdToMediaName = mutableMapOf<AttachmentId, String>()
databaseAttachments.forEach {
val mediaName = it.getMediaName()
val request = it.toArchiveMediaRequest(mediaName, backupKey)
requests += request
mediaIdToAttachmentId[request.mediaId] = it.attachmentId
attachmentIdToMediaName[it.attachmentId] = mediaName.name
}
api
.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
items = requests
)
.map { BatchArchiveMediaResult(it, mediaIdToAttachmentId, attachmentIdToMediaName) }
}
.map { result ->
result
.successfulResponses
.forEach {
val attachmentId = result.mediaIdToAttachmentId(it.mediaId)
val mediaName = result.attachmentIdToMediaName(attachmentId)
val thumbnailId = backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(mediaName = mediaName)).encode()
SignalDatabase.attachments.setArchiveData(attachmentId = attachmentId, archiveCdn = it.cdn!!, archiveMediaName = mediaName, archiveMediaId = it.mediaId, thumbnailId)
}
result
}
.also { Log.i(TAG, "archiveMediaResult: $it") }
}
fun deleteArchivedMedia(attachments: List<DatabaseAttachment>): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val mediaToDelete = attachments.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = 3, // TODO [cody] store and reuse backup cdn returned from copy/move call
mediaId = backupKey.deriveMediaId(Base64.decode(it.dataHash!!)).toString()
)
val mediaToDelete = attachments
.filter { it.archiveMediaId != null }
.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = it.archiveCdn,
mediaId = it.archiveMediaId!!
)
}
if (mediaToDelete.isEmpty()) {
Log.i(TAG, "No media to delete, quick success")
return NetworkResult.Success(Unit)
}
return getAuthCredential()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.deleteArchivedMedia(
backupKey = backupKey,
@@ -349,7 +464,155 @@ object BackupRepository {
mediaToDelete = mediaToDelete
)
}
.also { Log.i(TAG, "deleteBackupMediaResult: $it") }
.map {
SignalDatabase.attachments.clearArchiveData(attachments.map { it.attachmentId })
}
.also { Log.i(TAG, "deleteArchivedMediaResult: $it") }
}
fun deleteAbandonedMediaObjects(mediaObjects: Collection<ArchivedMediaObject>): NetworkResult<Unit> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val mediaToDelete = mediaObjects
.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = it.cdn,
mediaId = it.mediaId
)
}
if (mediaToDelete.isEmpty()) {
Log.i(TAG, "No media to delete, quick success")
return NetworkResult.Success(Unit)
}
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.deleteArchivedMedia(
backupKey = backupKey,
serviceCredential = credential,
mediaToDelete = mediaToDelete
)
}
.also { Log.i(TAG, "deleteAbandonedMediaObjectsResult: $it") }
}
fun debugDeleteAllArchivedMedia(): NetworkResult<Unit> {
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return debugGetArchivedMediaState()
.then { archivedMedia ->
val mediaToDelete = archivedMedia
.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = it.cdn,
mediaId = it.mediaId
)
}
if (mediaToDelete.isEmpty()) {
Log.i(TAG, "No media to delete, quick success")
NetworkResult.Success(Unit)
} else {
getAuthCredential()
.then { credential ->
api.deleteArchivedMedia(
backupKey = backupKey,
serviceCredential = credential,
mediaToDelete = mediaToDelete
)
}
}
}
.map {
SignalDatabase.attachments.clearAllArchiveData()
}
.also { Log.i(TAG, "debugDeleteAllArchivedMediaResult: $it") }
}
/**
* Retrieve credentials for reading from the backup cdn.
*/
fun getCdnReadCredentials(cdnNumber: Int): NetworkResult<GetArchiveCdnCredentialsResponse> {
val cached = SignalStore.backup().cdnReadCredentials
if (cached != null) {
return NetworkResult.Success(cached)
}
val api = AppDependencies.signalServiceAccountManager.archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey)
.then { credential ->
api.getCdnReadCredentials(
cdnNumber = cdnNumber,
backupKey = backupKey,
serviceCredential = credential
)
}
.also {
if (it is NetworkResult.Success) {
SignalStore.backup().cdnReadCredentials = it.result
}
}
.also { Log.i(TAG, "getCdnReadCredentialsResult: $it") }
}
/**
* 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
if (cachedBackupDirectory != null && cachedBackupMediaDirectory != null) {
return NetworkResult.Success(
BackupDirectories(
backupDir = cachedBackupDirectory,
mediaDir = cachedBackupMediaDirectory
)
)
}
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
BackupDirectories(it.backupDir!!, it.mediaDir!!)
}
}
.also {
if (it is NetworkResult.Success) {
SignalStore.backup().cachedBackupDirectory = it.result.backupDir
SignalStore.backup().cachedBackupMediaDirectory = it.result.mediaDir
}
}
}
/**
* 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 = AppDependencies.signalServiceAccountManager.archiveApi
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 }
.runOnStatusCodeError(resetInitializedStateErrorAction)
}
}
/**
@@ -366,7 +629,7 @@ 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 ->
return AppDependencies.signalServiceAccountManager.archiveApi.getServiceCredentials(currentTime).map { result ->
SignalStore.backup().addCredentials(result.credentials.toList())
SignalStore.backup().clearCredentialsOlderThan(currentTime)
SignalStore.backup().credentialsByDay.getForCurrentTime(currentTime.milliseconds)!!
@@ -380,15 +643,24 @@ object BackupRepository {
val profileKey: ProfileKey
)
private fun DatabaseAttachment.toArchiveMediaRequest(backupKey: BackupKey): ArchiveMediaRequest {
val mediaSecrets = backupKey.deriveMediaSecrets(Base64.decode(dataHash!!))
fun DatabaseAttachment.getMediaName(): MediaName {
return MediaName.fromDigest(remoteDigest!!)
}
fun DatabaseAttachment.getThumbnailMediaName(): MediaName {
return MediaName.fromDigestForThumbnail(remoteDigest!!)
}
private fun Attachment.toArchiveMediaRequest(mediaName: MediaName, backupKey: BackupKey): ArchiveMediaRequest {
val mediaSecrets = backupKey.deriveMediaSecrets(mediaName)
return ArchiveMediaRequest(
sourceAttachment = ArchiveMediaRequest.SourceAttachment(
cdn = cdnNumber,
cdn = cdn.cdnNumber,
key = remoteLocation!!
),
objectLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(size)).toInt(),
mediaId = mediaSecrets.id.toString(),
mediaId = mediaSecrets.id.encode(),
hmacKey = Base64.encodeWithPadding(mediaSecrets.macKey),
encryptionKey = Base64.encodeWithPadding(mediaSecrets.cipherKey),
iv = Base64.encodeWithPadding(mediaSecrets.iv)
@@ -396,20 +668,38 @@ object BackupRepository {
}
}
class ExportState(val backupTime: Long) {
data class ArchivedMediaObject(val mediaId: String, val cdn: Int)
data class BackupDirectories(val backupDir: String, val mediaDir: String)
class ExportState(val backupTime: Long, val allowMediaBackup: Boolean) {
val recipientIds = HashSet<Long>()
val threadIds = HashSet<Long>()
}
class BackupState {
class BackupState(val backupKey: BackupKey) {
val backupToLocalRecipientId = HashMap<Long, RecipientId>()
val chatIdToLocalThreadId = HashMap<Long, Long>()
val chatIdToLocalRecipientId = HashMap<Long, RecipientId>()
val chatIdToBackupRecipientId = HashMap<Long, Long>()
val callIdToType = HashMap<Long, Long>()
}
class BackupMetadata(
val usedSpace: Long,
val mediaCount: Long
)
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()
}
override fun deserialize(data: Long): MessageBackupTier {
return values().firstOrNull { it.value == data.toInt() } ?: FREE
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.attachments.AttachmentId
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.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.RestoreAttachmentThumbnailJob
/**
* Responsible for managing logic around restore prioritization
*/
object BackupRestoreManager {
private val reprioritizedAttachments: HashSet<AttachmentId> = HashSet()
/**
* Raise priority of all attachments for the included message records.
*
* This is so we can make certain attachments get downloaded more quickly
*/
fun prioritizeAttachmentsIfNeeded(messageRecords: List<MessageRecord>) {
SignalExecutors.BOUNDED.execute {
synchronized(this) {
val restoringAttachments = messageRecords
.mapNotNull { (it as? MmsMessageRecord?)?.slideDeck?.slides }
.flatten()
.mapNotNull { it.asAttachment() as? DatabaseAttachment }
.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.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

@@ -0,0 +1,14 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
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
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse
/**
* Result of attempting to batch copy multiple attachments at once with helpers for
* processing the collection of mini-responses.
*/
data class BatchArchiveMediaResult(
private val response: BatchArchiveMediaResponse,
private val mediaIdToAttachmentId: Map<String, AttachmentId>,
private val attachmentIdToMediaName: Map<AttachmentId, String>
) {
val successfulResponses: Sequence<BatchArchiveMediaResponse.BatchArchiveMediaItemResponse>
get() = response
.responses
.asSequence()
.filter { it.status == 200 }
val sourceNotFoundResponses: Sequence<BatchArchiveMediaResponse.BatchArchiveMediaItemResponse>
get() = response
.responses
.asSequence()
.filter { it.status == 410 }
fun mediaIdToAttachmentId(mediaId: String): AttachmentId {
return mediaIdToAttachmentId[mediaId]!!
}
fun attachmentIdToMediaName(attachmentId: AttachmentId): String {
return attachmentIdToMediaName[attachmentId]!!
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.signal.core.util.select
import org.signal.ringrtc.CallLinkRootKey
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.backup.v2.proto.CallLink
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import java.io.Closeable
import java.time.Instant
fun CallLinkTable.getCallLinksForBackup(): BackupCallLinkIterator {
val cursor = readableDatabase
.select()
.from(CallLinkTable.TABLE_NAME)
.run()
return BackupCallLinkIterator(cursor)
}
fun CallLinkTable.restoreFromBackup(callLink: CallLink): RecipientId {
return SignalDatabase.callLinks.insertCallLink(
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = CallLinkRoomId.fromCallLinkRootKey(CallLinkRootKey(callLink.rootKey.toByteArray())),
credentials = CallLinkCredentials(callLink.rootKey.toByteArray(), callLink.adminKey?.toByteArray()),
state = SignalCallLinkState(
name = callLink.name,
restrictions = callLink.restrictions.toLocal(),
expiration = Instant.ofEpochMilli(callLink.expirationMs)
)
)
)
}
/**
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class BackupCallLinkIterator(private val cursor: Cursor) : Iterator<BackupRecipient>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): BackupRecipient {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val callLink = CallLinkTable.CallLinkDeserializer.deserialize(cursor)
return BackupRecipient(
id = callLink.recipientId.toLong(),
callLink = CallLink(
rootKey = callLink.credentials!!.linkKeyBytes.toByteString(),
adminKey = callLink.credentials.adminPassBytes?.toByteString(),
name = callLink.state.name,
expirationMs = callLink.state.expiration.toEpochMilli(),
restrictions = callLink.state.restrictions.toBackup()
)
)
}
override fun close() {
cursor.close()
}
}
private fun CallLinkState.Restrictions.toBackup(): CallLink.Restrictions {
return when (this) {
CallLinkState.Restrictions.ADMIN_APPROVAL -> CallLink.Restrictions.ADMIN_APPROVAL
CallLinkState.Restrictions.NONE -> CallLink.Restrictions.NONE
CallLinkState.Restrictions.UNKNOWN -> CallLink.Restrictions.UNKNOWN
}
}
private fun CallLink.Restrictions.toLocal(): CallLinkState.Restrictions {
return when (this) {
CallLink.Restrictions.ADMIN_APPROVAL -> CallLinkState.Restrictions.ADMIN_APPROVAL
CallLink.Restrictions.NONE -> CallLinkState.Restrictions.NONE
CallLink.Restrictions.UNKNOWN -> CallLinkState.Restrictions.UNKNOWN
}
}

View File

@@ -8,57 +8,37 @@ package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import androidx.core.content.contentValuesOf
import org.signal.core.util.isNull
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.select
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.proto.Call
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.RecipientTable
import java.io.Closeable
typealias BackupCall = org.thoughtcrime.securesms.backup.v2.proto.Call
fun CallTable.getCallsForBackup(): CallLogIterator {
fun CallTable.getAdhocCallsForBackup(): CallLogIterator {
return CallLogIterator(
readableDatabase
.select()
.from(CallTable.TABLE_NAME)
.where("${CallTable.EVENT} != ${CallTable.Event.serialize(CallTable.Event.DELETE)}")
.where("${CallTable.TYPE}=?", CallTable.Type.AD_HOC_CALL)
.run()
)
}
fun CallTable.restoreCallLogFromBackup(call: BackupCall, backupState: BackupState) {
val type = when (call.type) {
Call.Type.VIDEO_CALL -> CallTable.Type.VIDEO_CALL
Call.Type.AUDIO_CALL -> CallTable.Type.AUDIO_CALL
Call.Type.AD_HOC_CALL -> CallTable.Type.AD_HOC_CALL
Call.Type.GROUP_CALL -> CallTable.Type.GROUP_CALL
Call.Type.UNKNOWN_TYPE -> return
}
fun CallTable.restoreCallLogFromBackup(call: AdHocCall, backupState: BackupState) {
val event = when (call.state) {
Call.State.MISSED -> CallTable.Event.MISSED
Call.State.COMPLETED -> CallTable.Event.ACCEPTED
Call.State.DECLINED_BY_USER -> CallTable.Event.DECLINED
Call.State.DECLINED_BY_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE
Call.State.UNKNOWN_EVENT -> return
AdHocCall.State.GENERIC -> CallTable.Event.GENERIC_GROUP_CALL
AdHocCall.State.UNKNOWN_STATE -> CallTable.Event.GENERIC_GROUP_CALL
}
val direction = if (call.outgoing) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING
backupState.callIdToType[call.callId] = CallTable.Call.getMessageType(type, direction, event)
val values = contentValuesOf(
CallTable.CALL_ID to call.callId,
CallTable.PEER to backupState.backupToLocalRecipientId[call.conversationRecipientId]!!.serialize(),
CallTable.TYPE to CallTable.Type.serialize(type),
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
CallTable.PEER to backupState.backupToLocalRecipientId[call.recipientId]!!.serialize(),
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.timestamp,
CallTable.RINGER to if (call.ringerRecipientId != null) backupState.backupToLocalRecipientId[call.ringerRecipientId]?.toLong() else null
CallTable.TIMESTAMP to call.startedCallTimestamp
)
writableDatabase.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
@@ -68,49 +48,23 @@ fun CallTable.restoreCallLogFromBackup(call: BackupCall, backupState: BackupStat
* Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s.
* Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources.
*/
class CallLogIterator(private val cursor: Cursor) : Iterator<BackupCall?>, Closeable {
class CallLogIterator(private val cursor: Cursor) : Iterator<AdHocCall?>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
override fun next(): BackupCall? {
override fun next(): AdHocCall? {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
val callId = cursor.requireLong(CallTable.CALL_ID)
val type = CallTable.Type.deserialize(cursor.requireInt(CallTable.TYPE))
val direction = CallTable.Direction.deserialize(cursor.requireInt(CallTable.DIRECTION))
val event = CallTable.Event.deserialize(cursor.requireInt(CallTable.EVENT))
return BackupCall(
return AdHocCall(
callId = callId,
conversationRecipientId = cursor.requireLong(CallTable.PEER),
type = when (type) {
CallTable.Type.AUDIO_CALL -> Call.Type.AUDIO_CALL
CallTable.Type.VIDEO_CALL -> Call.Type.VIDEO_CALL
CallTable.Type.AD_HOC_CALL -> Call.Type.AD_HOC_CALL
CallTable.Type.GROUP_CALL -> Call.Type.GROUP_CALL
},
outgoing = when (direction) {
CallTable.Direction.OUTGOING -> true
else -> false
},
timestamp = cursor.requireLong(CallTable.TIMESTAMP),
ringerRecipientId = if (cursor.isNull(CallTable.RINGER)) null else cursor.requireLong(CallTable.RINGER),
state = when (event) {
CallTable.Event.ONGOING -> Call.State.COMPLETED
CallTable.Event.OUTGOING_RING -> Call.State.COMPLETED
CallTable.Event.ACCEPTED -> Call.State.COMPLETED
CallTable.Event.DECLINED -> Call.State.DECLINED_BY_USER
CallTable.Event.GENERIC_GROUP_CALL -> Call.State.COMPLETED
CallTable.Event.JOINED -> Call.State.COMPLETED
CallTable.Event.MISSED -> Call.State.MISSED
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> Call.State.DECLINED_BY_NOTIFICATION_PROFILE
CallTable.Event.DELETE -> Call.State.COMPLETED
CallTable.Event.RINGING -> Call.State.MISSED
CallTable.Event.NOT_ACCEPTED -> Call.State.MISSED
}
recipientId = cursor.requireLong(CallTable.PEER),
state = AdHocCall.State.GENERIC,
startedCallTimestamp = cursor.requireLong(CallTable.TIMESTAMP)
)
}

View File

@@ -6,7 +6,6 @@
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import com.annimon.stream.Stream
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.Base64.decode
@@ -16,16 +15,20 @@ 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
import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaName
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
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
@@ -36,11 +39,15 @@ import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.backup.v2.proto.Text
import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate
import org.thoughtcrime.securesms.database.AttachmentTable
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
@@ -55,15 +62,19 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchove
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 java.util.UUID
import kotlin.jvm.optionals.getOrNull
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
/**
@@ -73,7 +84,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) : 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)
@@ -87,6 +98,8 @@ 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)
}
@@ -136,23 +149,22 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
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 = null
builder.expiresInMs = 0
}
MessageTypes.isProfileChange(record.type) -> {
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()
}
)
val profileChangeDetails = if (record.messageExtras != null) {
record.messageExtras.profileChangeDetails
} else {
Base64.decodeOrNull(record.body)?.let { ProfileChangeDetails.ADAPTER.decode(it) }
}
builder.updateMessage = 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 {
continue
}
builder.sms = false
}
MessageTypes.isSessionSwitchoverType(record.type) -> {
@@ -196,45 +208,115 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
MessageTypes.isCallLog(record.type) -> {
builder.sms = false
val call = calls.getCallByMessageId(record.id)
if (call != null) {
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callId = call.callId))
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(),
startedCallRecipientId = call.ringerRecipient?.toLong(),
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
}
} else {
when {
MessageTypes.isMissedAudioCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL)))
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(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL)))
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(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL)))
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(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL)))
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(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL)))
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(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL)))
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)
val joinedMembers = Stream.of(groupCallUpdateDetails.inCallUuids)
.map { uuid: String? -> UuidUtil.parseOrNull(uuid) }
.withoutNulls()
.map { obj: UUID? -> ACI.from(obj!!).toByteString() }
.toList()
builder.updateMessage = ChatUpdateMessage(
callingMessage = CallChatUpdate(
groupCall = GroupCallChatUpdate(
startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(),
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
inCallAcis = joinedMembers
)
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: java.lang.Exception) {
@@ -244,14 +326,44 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
}
MessageTypes.isPaymentsNotification(record.type) -> {
val paymentUuid = UuidUtil.parseOrNull(record.body)
val payment = if (paymentUuid != null) {
SignalDatabase.payments.getPayment(paymentUuid)
} else {
null
}
if (payment == null) {
builder.paymentNotification = PaymentNotification()
} else {
builder.paymentNotification = PaymentNotification(
amountMob = payment.amount.serializeAmountString(),
feeMob = payment.fee.serializeAmountString(),
note = payment.note,
transactionDetails = payment.getTransactionDetails()
)
}
}
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()) {
@@ -282,12 +394,13 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
chatId = record.threadId
authorId = record.fromRecipientId
dateSent = record.dateSent
expireStartDate = if (record.expireStarted > 0) record.expireStarted else null
expiresInMs = if (record.expiresIn > 0) record.expiresIn else null
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)
if (MessageTypes.isOutgoingMessageType(record.type)) {
if (MessageTypes.isCallLog(record.type)) {
directionless = ChatItem.DirectionlessMessageDetails()
} else if (MessageTypes.isOutgoingMessageType(record.type)) {
outgoing = ChatItem.OutgoingMessageDetails(
sendStatus = record.toBackupSendStatus(groupReceipts)
)
@@ -354,24 +467,54 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
private fun DatabaseAttachment.toBackupAttachment(): MessageAttachment {
val builder = FilePointer.Builder()
builder.contentType = contentType
builder.incrementalMac = incrementalDigest?.toByteString()
builder.incrementalMacChunkSize = incrementalMacChunkSize
builder.fileName = fileName
builder.width = width
builder.height = height
builder.caption = caption
builder.blurHash = blurHash?.hash
if (remoteKey.isNullOrBlank() || remoteDigest == null || size == 0L) {
builder.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
} else {
if (archiveMedia) {
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(),
size = this.size.toInt(),
digest = remoteDigest.toByteString()
)
} else {
if (remoteLocation.isNullOrBlank()) {
builder.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
} else {
builder.attachmentLocator = FilePointer.AttachmentLocator(
cdnKey = this.remoteLocation,
cdnNumber = this.cdn.cdnNumber,
uploadTimestamp = this.uploadTimestamp,
key = decode(remoteKey).toByteString(),
size = this.size.toInt(),
digest = remoteDigest.toByteString()
)
}
}
}
return MessageAttachment(
pointer = FilePointer(
attachmentLocator = FilePointer.AttachmentLocator(
cdnKey = this.remoteLocation ?: "",
cdnNumber = this.cdnNumber,
uploadTimestamp = this.uploadTimestamp
),
key = if (remoteKey != null) decode(remoteKey).toByteString() else null,
contentType = this.contentType,
size = this.size.toInt(),
incrementalMac = this.incrementalDigest?.toByteString(),
incrementalMacChunkSize = this.incrementalMacChunkSize,
fileName = this.fileName,
width = this.width,
height = this.height,
caption = this.caption,
blurHash = this.blurHash?.hash
)
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
}
)
}
@@ -381,6 +524,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(
@@ -542,8 +725,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),
@@ -577,8 +760,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

@@ -13,19 +13,25 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.core.util.requireLong
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
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.IndividualCallChatUpdate
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.database.AttachmentTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.MessageTable
@@ -33,27 +39,39 @@ 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
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.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.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.messages.SignalServiceDataMessage
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
/**
* 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
@@ -85,7 +103,6 @@ class ChatItemImportInserter(
MessageTable.EXPIRE_STARTED,
MessageTable.UNIDENTIFIED,
MessageTable.REMOTE_DELETED,
MessageTable.REMOTE_DELETED,
MessageTable.NETWORK_FAILURES,
MessageTable.QUOTE_ID,
MessageTable.QUOTE_AUTHOR,
@@ -96,7 +113,10 @@ class ChatItemImportInserter(
MessageTable.SHARED_CONTACTS,
MessageTable.LINK_PREVIEWS,
MessageTable.MESSAGE_RANGES,
MessageTable.VIEW_ONCE
MessageTable.VIEW_ONCE,
MessageTable.MESSAGE_EXTRAS,
MessageTable.ORIGINAL_MESSAGE_ID,
MessageTable.LATEST_REVISION_ID
)
private val REACTION_COLUMNS = arrayOf(
@@ -148,8 +168,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)
@@ -208,11 +242,69 @@ class ChatItemImportInserter(
var followUp: ((Long) -> Unit)? = null
if (this.updateMessage != null) {
if (this.updateMessage.callingMessage != null && this.updateMessage.callingMessage.callId != null) {
if (this.updateMessage.individualCall != null && this.updateMessage.individualCall.callId != null) {
followUp = { messageRowId ->
val callContentValues = ContentValues()
callContentValues.put(CallTable.MESSAGE_ID, messageRowId)
db.update(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, callContentValues, "${CallTable.CALL_ID} = ?", SqlUtil.buildArgs(this.updateMessage.callingMessage.callId))
val values = contentValuesOf(
CallTable.CALL_ID to updateMessage.individualCall.callId,
CallTable.MESSAGE_ID to messageRowId,
CallTable.PEER to chatRecipientId.serialize(),
CallTable.TYPE to CallTable.Type.serialize(if (updateMessage.individualCall.type == IndividualCall.Type.VIDEO_CALL) CallTable.Type.VIDEO_CALL else CallTable.Type.AUDIO_CALL),
CallTable.DIRECTION to CallTable.Direction.serialize(if (updateMessage.individualCall.direction == IndividualCall.Direction.OUTGOING) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING),
CallTable.EVENT to CallTable.Event.serialize(
when (updateMessage.individualCall.state) {
IndividualCall.State.MISSED -> CallTable.Event.MISSED
IndividualCall.State.MISSED_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE
IndividualCall.State.ACCEPTED -> CallTable.Event.ACCEPTED
IndividualCall.State.NOT_ACCEPTED -> CallTable.Event.NOT_ACCEPTED
else -> CallTable.Event.MISSED
}
),
CallTable.TIMESTAMP to updateMessage.individualCall.startedCallTimestamp,
CallTable.READ to CallTable.ReadState.serialize(CallTable.ReadState.UNREAD)
)
db.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
}
} else if (this.updateMessage.groupCall != null && this.updateMessage.groupCall.callId != null) {
followUp = { messageRowId ->
val values = contentValuesOf(
CallTable.CALL_ID to updateMessage.groupCall.callId,
CallTable.MESSAGE_ID to messageRowId,
CallTable.PEER to chatRecipientId.serialize(),
CallTable.TYPE to CallTable.Type.serialize(CallTable.Type.GROUP_CALL),
CallTable.DIRECTION to CallTable.Direction.serialize(if (backupState.backupToLocalRecipientId[updateMessage.groupCall.ringerRecipientId] == selfId) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING),
CallTable.EVENT to CallTable.Event.serialize(
when (updateMessage.groupCall.state) {
GroupCall.State.ACCEPTED -> CallTable.Event.ACCEPTED
GroupCall.State.MISSED -> CallTable.Event.MISSED
GroupCall.State.MISSED_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE
GroupCall.State.GENERIC -> CallTable.Event.GENERIC_GROUP_CALL
GroupCall.State.JOINED -> CallTable.Event.JOINED
GroupCall.State.RINGING -> CallTable.Event.RINGING
GroupCall.State.OUTGOING_RING -> CallTable.Event.OUTGOING_RING
GroupCall.State.DECLINED -> CallTable.Event.DECLINED
else -> CallTable.Event.GENERIC_GROUP_CALL
}
),
CallTable.TIMESTAMP to updateMessage.groupCall.startedCallTimestamp,
CallTable.READ to CallTable.ReadState.serialize(CallTable.ReadState.UNREAD)
)
db.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
}
}
}
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)
)
}
}
}
@@ -303,11 +395,42 @@ 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)
}
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
@@ -424,8 +547,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, Base64.encodeWithPadding(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, Base64.encodeWithPadding(messageExtras))
}
updateMessage.sessionSwitchover != null -> {
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
@@ -437,28 +566,32 @@ class ChatItemImportInserter(
val threadMergeDetails = ThreadMergeEvent(previousE164 = updateMessage.threadMerge.previousE164.toString()).encode()
put(MessageTable.BODY, Base64.encodeWithPadding(threadMergeDetails))
}
updateMessage.callingMessage != null -> {
when {
updateMessage.callingMessage.callId != null -> {
typeFlags = backupState.callIdToType[updateMessage.callingMessage.callId]!!
}
updateMessage.callingMessage.callMessage != null -> {
typeFlags = when (updateMessage.callingMessage.callMessage.type) {
IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL -> MessageTypes.INCOMING_AUDIO_CALL_TYPE
IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL -> MessageTypes.INCOMING_VIDEO_CALL_TYPE
IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE
IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE
IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL -> MessageTypes.MISSED_AUDIO_CALL_TYPE
IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL -> MessageTypes.MISSED_VIDEO_CALL_TYPE
IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE
IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE
IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags
}
updateMessage.individualCall != null -> {
if (updateMessage.individualCall.state == IndividualCall.State.MISSED || updateMessage.individualCall.state == IndividualCall.State.MISSED_NOTIFICATION_PROFILE) {
typeFlags = if (updateMessage.individualCall.type == IndividualCall.Type.AUDIO_CALL) MessageTypes.MISSED_AUDIO_CALL_TYPE else MessageTypes.MISSED_VIDEO_CALL_TYPE
} else {
typeFlags = if (updateMessage.individualCall.direction == IndividualCall.Direction.OUTGOING) {
if (updateMessage.individualCall.type == IndividualCall.Type.AUDIO_CALL) MessageTypes.OUTGOING_AUDIO_CALL_TYPE else MessageTypes.OUTGOING_VIDEO_CALL_TYPE
} else {
if (updateMessage.individualCall.type == IndividualCall.Type.AUDIO_CALL) MessageTypes.INCOMING_AUDIO_CALL_TYPE else MessageTypes.INCOMING_VIDEO_CALL_TYPE
}
}
// Calls don't use the incoming/outgoing flags, so we overwrite the flags here
this.put(MessageTable.TYPE, typeFlags)
}
updateMessage.groupCall != null -> {
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 -> {
put(MessageTable.BODY, "")
put(
@@ -474,6 +607,104 @@ 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 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())
@@ -484,6 +715,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
@@ -570,12 +810,12 @@ class ChatItemImportInserter(
pointer.attachmentLocator.cdnNumber,
SignalServiceAttachmentRemoteId.from(pointer.attachmentLocator.cdnKey),
contentType,
pointer.key?.toByteArray(),
Optional.ofNullable(pointer.size),
pointer.attachmentLocator.key.toByteArray(),
Optional.ofNullable(pointer.attachmentLocator.size),
Optional.empty(),
pointer.width ?: 0,
pointer.height ?: 0,
Optional.empty(),
Optional.ofNullable(pointer.attachmentLocator.digest.toByteArray()),
Optional.ofNullable(pointer.incrementalMac?.toByteArray()),
pointer.incrementalMacChunkSize ?: 0,
Optional.ofNullable(fileName),
@@ -586,17 +826,61 @@ class ChatItemImportInserter(
Optional.ofNullable(pointer.blurHash),
pointer.attachmentLocator.uploadTimestamp
)
return PointerAttachment.forPointer(Optional.of(signalAttachmentPointer)).orNull()
return PointerAttachment.forPointer(
pointer = Optional.of(signalAttachmentPointer),
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING
).orNull()
} else if (pointer.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
)
} else if (pointer.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
)
}
return null
}
private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? {
return thumbnail?.toLocalAttachment(this.contentType, this.fileName)
?: if (this.contentType == null) null else PointerAttachment.forPointer(SignalServiceDataMessage.Quote.QuotedAttachment(contentType = this.contentType!!, fileName = this.fileName, thumbnail = null)).orNull()
?: 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(),

View File

@@ -16,7 +16,7 @@ import java.util.concurrent.TimeUnit
private val TAG = Log.tag(MessageTable::class.java)
private const val BASE_TYPE = "base_type"
fun MessageTable.getMessagesForBackup(backupTime: Long): ChatItemExportIterator {
fun MessageTable.getMessagesForBackup(backupTime: Long, archiveMedia: Boolean): ChatItemExportIterator {
val cursor = readableDatabase
.select(
MessageTable.ID,
@@ -64,7 +64,7 @@ fun MessageTable.getMessagesForBackup(backupTime: Long): ChatItemExportIterator
.orderBy("${MessageTable.DATE_RECEIVED} ASC")
.run()
return ChatItemExportIterator(cursor, 100)
return ChatItemExportIterator(cursor, 100, archiveMedia)
}
fun MessageTable.createChatItemInserter(backupState: BackupState): ChatItemImportInserter {

View File

@@ -13,6 +13,7 @@ import org.signal.core.util.SqlUtil
import org.signal.core.util.deleteAll
import org.signal.core.util.logging.Log
import org.signal.core.util.nullIfBlank
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
@@ -23,8 +24,16 @@ import org.signal.core.util.toInt
import org.signal.core.util.update
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
import org.signal.storageservice.protos.groups.AccessControl
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember
import org.signal.storageservice.protos.groups.local.DecryptedTimer
import org.signal.storageservice.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.Contact
import org.thoughtcrime.securesms.backup.v2.proto.Group
@@ -35,15 +44,16 @@ import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTableCursorUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.util.toByteArray
@@ -103,7 +113,8 @@ fun RecipientTable.getGroupsForBackup(): BackupGroupIterator {
"${RecipientTable.TABLE_NAME}.${RecipientTable.EXTRAS}",
"${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY}",
"${GroupTable.TABLE_NAME}.${GroupTable.SHOW_AS_STORY_STATE}",
"${GroupTable.TABLE_NAME}.${GroupTable.TITLE}"
"${GroupTable.TABLE_NAME}.${GroupTable.TITLE}",
"${GroupTable.TABLE_NAME}.${GroupTable.V2_DECRYPTED_GROUP}"
)
.from(
"""
@@ -117,25 +128,6 @@ fun RecipientTable.getGroupsForBackup(): BackupGroupIterator {
return BackupGroupIterator(cursor)
}
/**
* Takes a [BackupRecipient] and writes it into the database.
*/
fun RecipientTable.restoreRecipientFromBackup(recipient: BackupRecipient, backupState: BackupState): RecipientId? {
// TODO Need to handle groups
// TODO Also, should we move this when statement up to mimic the export? Kinda weird that this calls distributionListTable functions
return when {
recipient.contact != null -> restoreContactFromBackup(recipient.contact)
recipient.group != null -> restoreGroupFromBackup(recipient.group)
recipient.distributionList != null -> SignalDatabase.distributionLists.restoreFromBackup(recipient.distributionList, backupState)
recipient.self != null -> Recipient.self().id
recipient.releaseNotes != null -> restoreReleaseNotes()
else -> {
Log.w(TAG, "Unrecognized recipient type!")
null
}
}
}
/**
* Given [AccountData], this will insert the necessary data for the local user into the [RecipientTable].
*/
@@ -171,11 +163,11 @@ fun RecipientTable.clearAllDataForBackupRestore() {
SqlUtil.resetAutoIncrementValue(writableDatabase, RecipientTable.TABLE_NAME)
RecipientId.clearCache()
ApplicationDependencies.getRecipientCache().clear()
ApplicationDependencies.getRecipientCache().clearSelf()
AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.clearSelf()
}
private fun RecipientTable.restoreContactFromBackup(contact: Contact): RecipientId {
fun RecipientTable.restoreContactFromBackup(contact: Contact): RecipientId {
val id = getAndPossiblyMergePnpVerified(
aci = ACI.parseOrNull(contact.aci?.toByteArray()),
pni = PNI.parseOrNull(contact.pni?.toByteArray()),
@@ -206,7 +198,7 @@ private fun RecipientTable.restoreContactFromBackup(contact: Contact): Recipient
return id
}
private fun RecipientTable.restoreReleaseNotes(): RecipientId {
fun RecipientTable.restoreReleaseNotes(): RecipientId {
val releaseChannelId: RecipientId = insertReleaseChannelRecipient()
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelId)
@@ -215,13 +207,16 @@ private fun RecipientTable.restoreReleaseNotes(): RecipientId {
return releaseChannelId
}
private fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId {
fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId {
val masterKey = GroupMasterKey(group.masterKey.toByteArray())
val groupId = GroupId.v2(masterKey)
val placeholderState = DecryptedGroup.Builder()
.revision(GroupsV2StateProcessor.PLACEHOLDER_REVISION)
.build()
val operations = AppDependencies.groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(masterKey))
val decryptedState = if (group.snapshot == null) {
DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
} else {
group.snapshot.toDecryptedGroup(operations)
}
val values = ContentValues().apply {
put(RecipientTable.GROUP_ID, groupId.toString())
@@ -236,20 +231,154 @@ private fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId {
}
val recipientId = writableDatabase.insert(RecipientTable.TABLE_NAME, null, values)
val groupValues = ContentValues().apply {
put(GroupTable.RECIPIENT_ID, recipientId)
put(GroupTable.GROUP_ID, groupId.toString())
put(GroupTable.TITLE, group.name)
put(GroupTable.V2_MASTER_KEY, masterKey.serialize())
put(GroupTable.V2_DECRYPTED_GROUP, placeholderState.encode())
put(GroupTable.V2_REVISION, placeholderState.revision)
put(GroupTable.SHOW_AS_STORY_STATE, group.storySendMode.toGroupShowAsStoryState().code)
val restoredId = SignalDatabase.groups.create(masterKey, decryptedState)
if (restoredId != null) {
SignalDatabase.groups.setShowAsStoryState(restoredId, group.storySendMode.toGroupShowAsStoryState())
}
writableDatabase.insert(GroupTable.TABLE_NAME, null, groupValues)
return RecipientId.from(recipientId)
}
private fun Group.AccessControl.AccessRequired.toLocal(): AccessControl.AccessRequired {
return when (this) {
Group.AccessControl.AccessRequired.UNKNOWN -> AccessControl.AccessRequired.UNKNOWN
Group.AccessControl.AccessRequired.ANY -> AccessControl.AccessRequired.ANY
Group.AccessControl.AccessRequired.MEMBER -> AccessControl.AccessRequired.MEMBER
Group.AccessControl.AccessRequired.ADMINISTRATOR -> AccessControl.AccessRequired.ADMINISTRATOR
Group.AccessControl.AccessRequired.UNSATISFIABLE -> AccessControl.AccessRequired.UNSATISFIABLE
}
}
private fun Group.AccessControl.toLocal(): AccessControl {
return AccessControl(members = this.members.toLocal(), attributes = this.attributes.toLocal(), addFromInviteLink = this.addFromInviteLink.toLocal())
}
private fun Group.Member.Role.toLocal(): Member.Role {
return when (this) {
Group.Member.Role.UNKNOWN -> Member.Role.UNKNOWN
Group.Member.Role.DEFAULT -> Member.Role.DEFAULT
Group.Member.Role.ADMINISTRATOR -> Member.Role.ADMINISTRATOR
}
}
private fun AccessControl.AccessRequired.toSnapshot(): Group.AccessControl.AccessRequired {
return when (this) {
AccessControl.AccessRequired.UNKNOWN -> Group.AccessControl.AccessRequired.UNKNOWN
AccessControl.AccessRequired.ANY -> Group.AccessControl.AccessRequired.ANY
AccessControl.AccessRequired.MEMBER -> Group.AccessControl.AccessRequired.MEMBER
AccessControl.AccessRequired.ADMINISTRATOR -> Group.AccessControl.AccessRequired.ADMINISTRATOR
AccessControl.AccessRequired.UNSATISFIABLE -> Group.AccessControl.AccessRequired.UNSATISFIABLE
}
}
private fun AccessControl.toSnapshot(): Group.AccessControl {
return Group.AccessControl(members = members.toSnapshot(), attributes = attributes.toSnapshot(), addFromInviteLink = addFromInviteLink.toSnapshot())
}
private fun Member.Role.toSnapshot(): Group.Member.Role {
return when (this) {
Member.Role.UNKNOWN -> Group.Member.Role.UNKNOWN
Member.Role.DEFAULT -> Group.Member.Role.DEFAULT
Member.Role.ADMINISTRATOR -> Group.Member.Role.ADMINISTRATOR
}
}
private fun DecryptedGroup.toSnapshot(): Group.GroupSnapshot? {
if (revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION || revision == GroupsV2StateProcessor.PLACEHOLDER_REVISION) {
return null
}
return Group.GroupSnapshot(
title = Group.GroupAttributeBlob(title = title),
avatarUrl = avatar,
disappearingMessagesTimer = Group.GroupAttributeBlob(disappearingMessagesDuration = disappearingMessagesTimer?.duration ?: 0),
accessControl = accessControl?.toSnapshot(),
version = revision,
members = members.map { it.toSnapshot() },
membersPendingProfileKey = pendingMembers.map { it.toSnapshot() },
membersPendingAdminApproval = requestingMembers.map { it.toSnapshot() },
inviteLinkPassword = inviteLinkPassword,
description = Group.GroupAttributeBlob(descriptionText = description),
announcements_only = isAnnouncementGroup == EnabledState.ENABLED,
members_banned = bannedMembers.map { it.toSnapshot() }
)
}
private fun Group.Member.toLocal(): DecryptedMember {
return DecryptedMember(aciBytes = userId, role = role.toLocal(), profileKey = profileKey, joinedAtRevision = joinedAtVersion)
}
private fun DecryptedMember.toSnapshot(): Group.Member {
return Group.Member(userId = aciBytes, role = role.toSnapshot(), profileKey = profileKey, joinedAtVersion = joinedAtRevision)
}
private fun Group.MemberPendingProfileKey.toLocal(operations: GroupsV2Operations.GroupOperations): DecryptedPendingMember {
return DecryptedPendingMember(
serviceIdBytes = member!!.userId,
role = member.role.toLocal(),
addedByAci = addedByUserId,
timestamp = timestamp,
serviceIdCipherText = operations.encryptServiceId(ServiceId.Companion.parseOrNull(member.userId))
)
}
private fun DecryptedPendingMember.toSnapshot(): Group.MemberPendingProfileKey {
return Group.MemberPendingProfileKey(
member = Group.Member(
userId = serviceIdBytes,
role = role.toSnapshot()
),
addedByUserId = addedByAci,
timestamp = timestamp
)
}
private fun Group.MemberPendingAdminApproval.toLocal(): DecryptedRequestingMember {
return DecryptedRequestingMember(
aciBytes = userId,
profileKey = profileKey,
timestamp = timestamp
)
}
private fun DecryptedRequestingMember.toSnapshot(): Group.MemberPendingAdminApproval {
return Group.MemberPendingAdminApproval(
userId = aciBytes,
profileKey = profileKey,
timestamp = timestamp
)
}
private fun Group.MemberBanned.toLocal(): DecryptedBannedMember {
return DecryptedBannedMember(
serviceIdBytes = userId,
timestamp = timestamp
)
}
private fun DecryptedBannedMember.toSnapshot(): Group.MemberBanned {
return Group.MemberBanned(
userId = serviceIdBytes,
timestamp = timestamp
)
}
private fun Group.GroupSnapshot.toDecryptedGroup(operations: GroupsV2Operations.GroupOperations): DecryptedGroup {
return DecryptedGroup(
title = title?.title ?: "",
avatar = avatarUrl,
disappearingMessagesTimer = DecryptedTimer(duration = disappearingMessagesTimer?.disappearingMessagesDuration ?: 0),
accessControl = accessControl?.toLocal(),
revision = version,
members = members.map { member -> member.toLocal() },
pendingMembers = membersPendingProfileKey.map { pending -> pending.toLocal(operations) },
requestingMembers = membersPendingAdminApproval.map { requesting -> requesting.toLocal() },
inviteLinkPassword = inviteLinkPassword,
description = description?.descriptionText ?: "",
isAnnouncementGroup = if (announcements_only) EnabledState.ENABLED else EnabledState.DISABLED,
bannedMembers = members_banned.map { it.toLocal() }
)
}
private fun Contact.toLocalExtras(): RecipientExtras {
return RecipientExtras(
hideStory = this.hideStory
@@ -331,6 +460,8 @@ class BackupGroupIterator(private val cursor: Cursor) : Iterator<BackupRecipient
val extras = RecipientTableCursorUtil.getExtras(cursor)
val showAsStoryState: GroupTable.ShowAsStoryState = GroupTable.ShowAsStoryState.deserialize(cursor.requireInt(GroupTable.SHOW_AS_STORY_STATE))
val decryptedGroup: DecryptedGroup = DecryptedGroup.ADAPTER.decode(cursor.requireBlob(GroupTable.V2_DECRYPTED_GROUP)!!)
return BackupRecipient(
id = cursor.requireLong(RecipientTable.ID),
group = BackupGroup(
@@ -338,7 +469,7 @@ class BackupGroupIterator(private val cursor: Cursor) : Iterator<BackupRecipient
whitelisted = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
hideStory = extras?.hideStory() ?: false,
storySendMode = showAsStoryState.toGroupStorySendMode(),
name = cursor.requireString(GroupTable.TITLE) ?: ""
snapshot = decryptedGroup.toSnapshot()
)
)
}
@@ -393,6 +524,6 @@ private fun Group.StorySendMode.toGroupShowAsStoryState(): GroupTable.ShowAsStor
private val Contact.formattedE164: String?
get() {
return e164?.let {
PhoneNumberFormatter.get(ApplicationDependencies.getApplication()).format(e164.toString())
PhoneNumberFormatter.get(AppDependencies.application).format(e164.toString())
}
}

View File

@@ -11,17 +11,18 @@ import org.thoughtcrime.securesms.backup.v2.database.restoreSelfFromBackup
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.subscription.Subscriber
import org.thoughtcrime.securesms.util.ProfileUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
@@ -33,12 +34,12 @@ import kotlin.jvm.optionals.getOrNull
object AccountDataProcessor {
fun export(emitter: BackupFrameEmitter) {
val context = ApplicationDependencies.getApplication()
val context = AppDependencies.application
val self = Recipient.self().fresh()
val record = recipients.getRecordForSync(self.id)
val subscriber: Subscriber? = SignalStore.donationsValues().getSubscriber()
val subscriber: InAppPaymentSubscriberRecord? = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
emitter.emit(
Frame(
@@ -47,7 +48,7 @@ object AccountDataProcessor {
givenName = self.profileName.givenName,
familyName = self.profileName.familyName,
avatarUrlPath = self.profileAvatar ?: "",
subscriptionManuallyCancelled = SignalStore.donationsValues().isUserManuallyCancelled(),
subscriptionManuallyCancelled = InAppPaymentsRepository.isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION),
username = self.username.getOrNull(),
subscriberId = subscriber?.subscriberId?.bytes?.toByteString() ?: defaultAccountRecord.subscriberId,
subscriberCurrencyCode = subscriber?.currencyCode ?: defaultAccountRecord.subscriberCurrencyCode,
@@ -80,7 +81,7 @@ object AccountDataProcessor {
SignalStore.account().setRegistered(true)
val context = ApplicationDependencies.getApplication()
val context = AppDependencies.application
val settings = accountData.accountSettings
if (settings != null) {
@@ -101,19 +102,27 @@ object AccountDataProcessor {
SignalStore.storyValues().userHasSeenGroupStoryEducationSheet = settings.hasSeenGroupStoryEducationSheet
SignalStore.storyValues().viewedReceiptsEnabled = settings.storyViewReceiptsEnabled ?: settings.readReceipts
if (accountData.subscriptionManuallyCancelled) {
SignalStore.donationsValues().updateLocalStateForManualCancellation()
} else {
SignalStore.donationsValues().clearUserManuallyCancelled()
if (accountData.subscriberId.size > 0) {
val remoteSubscriberId = SubscriberId.fromBytes(accountData.subscriberId.toByteArray())
val localSubscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
val subscriber = InAppPaymentSubscriberRecord(
remoteSubscriberId,
accountData.subscriberCurrencyCode,
InAppPaymentSubscriberRecord.Type.DONATION,
localSubscriber?.requiresCancel ?: false,
InAppPaymentsRepository.getLatestPaymentMethodType(InAppPaymentSubscriberRecord.Type.DONATION)
)
InAppPaymentsRepository.setSubscriber(subscriber)
}
if (accountData.subscriberId.size > 0) {
val subscriber = Subscriber(SubscriberId.fromBytes(accountData.subscriberId.toByteArray()), accountData.subscriberCurrencyCode)
SignalStore.donationsValues().setSubscriber(subscriber)
if (accountData.subscriptionManuallyCancelled) {
SignalStore.donationsValues().updateLocalStateForManualCancellation(InAppPaymentSubscriberRecord.Type.DONATION)
}
if (accountData.avatarUrlPath.isNotEmpty()) {
ApplicationDependencies.getJobManager().add(RetrieveProfileAvatarJob(Recipient.self().fresh(), accountData.avatarUrlPath))
AppDependencies.jobManager.add(RetrieveProfileAvatarJob(Recipient.self().fresh(), accountData.avatarUrlPath))
}
if (accountData.usernameLink != null) {

View File

@@ -7,29 +7,28 @@ package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.database.getCallsForBackup
import org.thoughtcrime.securesms.backup.v2.database.getAdhocCallsForBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreCallLogFromBackup
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.database.SignalDatabase
typealias BackupCall = org.thoughtcrime.securesms.backup.v2.proto.Call
object AdHocCallBackupProcessor {
object CallLogBackupProcessor {
val TAG = Log.tag(CallLogBackupProcessor::class.java)
val TAG = Log.tag(AdHocCallBackupProcessor::class.java)
fun export(emitter: BackupFrameEmitter) {
SignalDatabase.calls.getCallsForBackup().use { reader ->
SignalDatabase.calls.getAdhocCallsForBackup().use { reader ->
for (callLog in reader) {
if (callLog != null) {
emitter.emit(Frame(call = callLog))
emitter.emit(Frame(adHocCall = callLog))
}
}
}
}
fun import(call: BackupCall, backupState: BackupState) {
fun import(call: AdHocCall, backupState: BackupState) {
SignalDatabase.calls.restoreCallLogFromBackup(call, backupState)
}
}

View File

@@ -19,7 +19,7 @@ object ChatItemBackupProcessor {
val TAG = Log.tag(ChatItemBackupProcessor::class.java)
fun export(exportState: ExportState, emitter: BackupFrameEmitter) {
SignalDatabase.messages.getMessagesForBackup(exportState.backupTime).use { chatItems ->
SignalDatabase.messages.getMessagesForBackup(exportState.backupTime, exportState.allowMediaBackup).use { chatItems ->
for (chatItem in chatItems) {
if (exportState.threadIds.contains(chatItem.chatId)) {
emitter.emit(Frame(chatItem = chatItem))

View File

@@ -10,9 +10,13 @@ import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.database.BackupRecipient
import org.thoughtcrime.securesms.backup.v2.database.getAllForBackup
import org.thoughtcrime.securesms.backup.v2.database.getCallLinksForBackup
import org.thoughtcrime.securesms.backup.v2.database.getContactsForBackup
import org.thoughtcrime.securesms.backup.v2.database.getGroupsForBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreRecipientFromBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreContactFromBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreFromBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreGroupFromBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreReleaseNotes
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.proto.ReleaseNotes
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
@@ -60,10 +64,26 @@ object RecipientBackupProcessor {
state.recipientIds.add(it.id)
emitter.emit(Frame(recipient = it))
}
SignalDatabase.callLinks.getCallLinksForBackup().forEach {
state.recipientIds.add(it.id)
emitter.emit(Frame(recipient = it))
}
}
fun import(recipient: BackupRecipient, backupState: BackupState) {
val newId = SignalDatabase.recipients.restoreRecipientFromBackup(recipient, backupState)
val newId = when {
recipient.contact != null -> SignalDatabase.recipients.restoreContactFromBackup(recipient.contact)
recipient.group != null -> SignalDatabase.recipients.restoreGroupFromBackup(recipient.group)
recipient.distributionList != null -> SignalDatabase.distributionLists.restoreFromBackup(recipient.distributionList, backupState)
recipient.self != null -> Recipient.self().id
recipient.releaseNotes != null -> SignalDatabase.recipients.restoreReleaseNotes()
recipient.callLink != null -> SignalDatabase.callLinks.restoreFromBackup(recipient.callLink)
else -> {
Log.w(TAG, "Unrecognized recipient type!")
null
}
}
if (newId != null) {
backupState.backupToLocalRecipientId[recipient.id] = newId
}

View File

@@ -1,75 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.stream
import org.signal.libsignal.protocol.kdf.HKDF
import java.io.FilterOutputStream
import java.io.OutputStream
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class BackupEncryptedOutputStream(key: ByteArray, backupId: ByteArray, wrapped: OutputStream) : FilterOutputStream(wrapped) {
val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val mac: Mac = Mac.getInstance("HmacSHA256")
var finalMac: ByteArray? = null
init {
if (key.size != 32) {
throw IllegalArgumentException("Key must be 32 bytes!")
}
if (backupId.size != 16) {
throw IllegalArgumentException("BackupId must be 32 bytes!")
}
val extendedKey = HKDF.deriveSecrets(key, backupId, "20231003_Signal_Backups_EncryptMessageBackup".toByteArray(), 80)
val macKey = extendedKey.copyOfRange(0, 32)
val cipherKey = extendedKey.copyOfRange(32, 64)
val iv = extendedKey.copyOfRange(64, 80)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
}
override fun write(b: Int) {
throw UnsupportedOperationException()
}
override fun write(data: ByteArray) {
write(data, 0, data.size)
}
override fun write(data: ByteArray, off: Int, len: Int) {
cipher.update(data, off, len)?.let { ciphertext ->
mac.update(ciphertext)
super.write(ciphertext)
}
}
override fun flush() {
cipher.doFinal()?.let { ciphertext ->
mac.update(ciphertext)
super.write(ciphertext)
}
finalMac = mac.doFinal()
super.flush()
}
override fun close() {
flush()
super.close()
}
fun getMac(): ByteArray {
return finalMac ?: throw IllegalStateException("Mac not yet available! You must call flush() before asking for the mac.")
}
}

View File

@@ -41,18 +41,21 @@ class EncryptedBackupReader(
val stream: InputStream
init {
val keyMaterial = key.deriveSecrets(aci)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(keyMaterial.iv))
}
val keyMaterial = key.deriveBackupSecrets(aci)
validateMac(keyMaterial.macKey, streamLength, dataStream())
val inputStream = dataStream()
val iv = inputStream.readNBytesOrThrow(16)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv))
}
stream = GZIPInputStream(
CipherInputStream(
TruncatingInputStream(
wrapped = dataStream(),
wrapped = inputStream,
maxBytes = streamLength - MAC_SIZE
),
cipher

View File

@@ -9,11 +9,11 @@ import org.signal.core.util.stream.MacOutputStream
import org.signal.core.util.writeVarInt32
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.io.IOException
import java.io.OutputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.Mac
@@ -33,28 +33,29 @@ class EncryptedBackupWriter(
private val append: (ByteArray) -> Unit
) : BackupExportWriter {
private val mainStream: GZIPOutputStream
private val mainStream: PaddedGzipOutputStream
private val macStream: MacOutputStream
init {
val keyMaterial = key.deriveSecrets(aci)
val keyMaterial = key.deriveBackupSecrets(aci)
val iv: ByteArray = Util.getSecretBytes(16)
outputStream.write(iv)
outputStream.flush()
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(keyMaterial.iv))
init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv))
}
val mac = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec(keyMaterial.macKey, "HmacSHA256"))
update(iv)
}
macStream = MacOutputStream(outputStream, mac)
val cipherStream = CipherOutputStream(macStream, cipher)
mainStream = GZIPOutputStream(
CipherOutputStream(
macStream,
cipher
)
)
mainStream = PaddedGzipOutputStream(cipherStream)
}
override fun write(header: BackupInfo) {

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.stream
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import java.io.FilterOutputStream
import java.io.OutputStream
import java.util.zip.GZIPOutputStream
/**
* GZIPs the content of the provided [outputStream], but also adds padding to the end of the stream using the same algorithm as [PaddingInputStream].
* We do this to fit files into a smaller number of size buckets to avoid fingerprinting. And it turns out that bolting on zeros to the end of a GZIP stream is
* fine, because GZIP is smart enough to ignore it. This means readers of this data don't have to do anything special.
*/
class PaddedGzipOutputStream private constructor(private val outputStream: SizeObservingOutputStream) : GZIPOutputStream(outputStream) {
constructor(outputStream: OutputStream) : this(SizeObservingOutputStream(outputStream))
override fun finish() {
super.finish()
val totalLength = outputStream.size
val paddedSize: Long = PaddingInputStream.getPaddedSize(totalLength)
val paddingToAdd: Int = (paddedSize - totalLength).toInt()
outputStream.write(ByteArray(paddingToAdd))
}
/**
* We need to know the size of the *compressed* stream to know how much padding to add at the end.
*/
private class SizeObservingOutputStream(val wrapped: OutputStream) : FilterOutputStream(wrapped) {
var size: Long = 0L
private set
override fun write(b: Int) {
wrapped.write(b)
size++
}
override fun write(b: ByteArray) {
wrapped.write(b)
size += b.size
}
override fun write(b: ByteArray, off: Int, len: Int) {
wrapped.write(b, off, len)
size += len
}
}
}

View File

@@ -0,0 +1,289 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import android.os.Parcelable
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import kotlinx.parcelize.Parcelize
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Icons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
/**
* Notifies the user of an issue with their backup.
*/
class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
companion object {
private const val ARG_ALERT = "alert"
fun create(backupAlert: BackupAlert): BackupAlertBottomSheet {
return BackupAlertBottomSheet().apply {
arguments = bundleOf(ARG_ALERT to backupAlert)
}
}
}
private val backupAlert: BackupAlert by lazy(LazyThreadSafetyMode.NONE) {
BundleCompat.getParcelable(requireArguments(), ARG_ALERT, BackupAlert::class.java)!!
}
@Composable
override fun SheetContent() {
BackupAlertSheetContent(
backupAlert = backupAlert,
onPrimaryActionClick = this::performPrimaryAction,
onSecondaryActionClick = this::performSecondaryAction
)
}
@Stable
private fun performPrimaryAction() {
when (backupAlert) {
BackupAlert.GENERIC -> {
// TODO [message-backups] -- Back up now
}
BackupAlert.PAYMENT_PROCESSING -> {
// TODO [message-backups] -- Silence
}
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> {
// TODO [message-backups] -- Download media now
}
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> {
// TODO [message-backups] -- Download media now
}
}
dismissAllowingStateLoss()
}
@Stable
private fun performSecondaryAction() {
when (backupAlert) {
BackupAlert.GENERIC -> {
// TODO [message-backups] - Dismiss and notify later
}
BackupAlert.PAYMENT_PROCESSING -> error("PAYMENT_PROCESSING state does not support a secondary action.")
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> {
// TODO [message-backups] - Silence and remind on last day
}
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> {
// TODO [message-backups] - Silence forever
}
}
dismissAllowingStateLoss()
}
}
@Composable
private fun BackupAlertSheetContent(
backupAlert: BackupAlert,
onPrimaryActionClick: () -> Unit,
onSecondaryActionClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
BottomSheets.Handle()
Spacer(modifier = Modifier.size(26.dp))
val iconColors = rememberBackupsIconColors(backupAlert = backupAlert)
Icons.BrushedForeground(
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] final asset
contentDescription = null,
foregroundBrush = iconColors.foreground,
modifier = Modifier
.size(88.dp)
.background(color = iconColors.background, shape = CircleShape)
.padding(20.dp)
)
Text(
text = stringResource(id = rememberTitleResource(backupAlert = backupAlert)),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
when (backupAlert) {
BackupAlert.GENERIC -> GenericBody()
BackupAlert.PAYMENT_PROCESSING -> PaymentProcessingBody()
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> MediaBackupsAreOffBody()
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> MediaWillBeDeletedTodayBody()
}
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert)
val padBottom = if (secondaryActionResource > 0) 16.dp else 56.dp
Buttons.LargeTonal(
onClick = onPrimaryActionClick,
modifier = Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(top = 60.dp, bottom = padBottom)
) {
Text(text = stringResource(id = rememberPrimaryActionResource(backupAlert = backupAlert)))
}
if (secondaryActionResource > 0) {
TextButton(onClick = onSecondaryActionClick, modifier = Modifier.padding(bottom = 32.dp)) {
Text(text = stringResource(id = secondaryActionResource))
}
}
}
}
@Composable
private fun GenericBody() {
Text(text = "TODO")
}
@Composable
private fun PaymentProcessingBody() {
Text(text = "TODO")
}
@Composable
private fun MediaBackupsAreOffBody() {
Text(text = "TODO")
}
@Composable
private fun MediaWillBeDeletedTodayBody() {
Text(text = "TODO")
}
@Composable
private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors {
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.GENERIC, BackupAlert.PAYMENT_PROCESSING -> BackupsIconColors.Warning
BackupAlert.MEDIA_BACKUPS_ARE_OFF, BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> BackupsIconColors.Error
}
}
}
@Composable
@StringRes
private fun rememberTitleResource(backupAlert: BackupAlert): Int {
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.GENERIC -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy
BackupAlert.PAYMENT_PROCESSING -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy
}
}
}
@Composable
private fun rememberPrimaryActionResource(backupAlert: BackupAlert): Int {
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.GENERIC -> android.R.string.ok // TODO [message-backups] -- Finalized copy
BackupAlert.PAYMENT_PROCESSING -> android.R.string.ok // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> android.R.string.ok // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> android.R.string.ok // TODO [message-backups] -- Finalized copy
}
}
}
@Composable
private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.GENERIC -> android.R.string.cancel // TODO [message-backups] -- Finalized copy
BackupAlert.PAYMENT_PROCESSING -> -1
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> android.R.string.cancel // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> android.R.string.cancel // TODO [message-backups] -- Finalized copy
}
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewGeneric() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.GENERIC,
onPrimaryActionClick = {},
onSecondaryActionClick = {}
)
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewPayment() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.PAYMENT_PROCESSING,
onPrimaryActionClick = {},
onSecondaryActionClick = {}
)
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewMedia() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.MEDIA_BACKUPS_ARE_OFF,
onPrimaryActionClick = {},
onSecondaryActionClick = {}
)
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewDelete() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.MEDIA_WILL_BE_DELETED_TODAY,
onPrimaryActionClick = {},
onSecondaryActionClick = {}
)
}
}
@Parcelize
enum class BackupAlert : Parcelable {
GENERIC,
PAYMENT_PROCESSING,
MEDIA_BACKUPS_ARE_OFF,
MEDIA_WILL_BE_DELETED_TODAY
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
sealed interface BackupsIconColors {
@get:Composable
val foreground: Brush
@get:Composable
val background: Color
object Normal : BackupsIconColors {
override val foreground: Brush
@Composable get() = remember {
Brush.linearGradient(
colors = listOf(Color(0xFF316ED0), Color(0xFF558BE2)),
start = Offset(x = 0f, y = Float.POSITIVE_INFINITY),
end = Offset(x = Float.POSITIVE_INFINITY, y = 0f)
)
}
override val background: Color @Composable get() = MaterialTheme.colorScheme.primaryContainer
}
object Warning : BackupsIconColors {
override val foreground: Brush @Composable get() = SolidColor(Color(0xFFC86600))
override val background: Color @Composable get() = Color(0xFFF9E4B6)
}
object Error : BackupsIconColors {
override val foreground: Brush @Composable get() = SolidColor(MaterialTheme.colorScheme.error)
override val background: Color @Composable get() = Color(0xFFFFD9D9)
}
}

View File

@@ -1,19 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
data class MessageBackupsFlowState(
val selectedMessageBackupsType: MessageBackupsType? = null,
val availableBackupsTypes: List<MessageBackupsType> = emptyList(),
val selectedPaymentGateway: GatewayResponse.Gateway? = null,
val availablePaymentGateways: List<GatewayResponse.Gateway> = emptyList(),
val pin: String = "",
val pinKeyboardType: PinKeyboardType = SignalStore.pinValues().keyboardType
)

View File

@@ -32,8 +32,8 @@ import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeature
import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeatureRow
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeatureRow
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.devicetransfer.moreoptions.MoreTransferOrRestoreOptionsMode
import org.thoughtcrime.securesms.util.navigation.safeNavigate

View File

@@ -0,0 +1,217 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui.status
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.Icons
import org.signal.core.ui.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
import kotlin.math.max
import kotlin.math.min
private const val NONE = -1
/**
* Displays a "heads up" widget containing information about the current
* status of the user's backup.
*/
@Composable
fun BackupStatus(
data: BackupStatusData,
onActionClick: () -> Unit = {}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.border(1.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.38f), shape = RoundedCornerShape(12.dp))
.fillMaxWidth()
.padding(14.dp)
) {
val foreground: Brush = data.iconColors.foreground
Icons.BrushedForeground(
painter = painterResource(id = data.iconRes),
contentDescription = null,
foregroundBrush = foreground,
modifier = Modifier
.background(color = data.iconColors.background, shape = CircleShape)
.padding(8.dp)
)
Column(
modifier = Modifier
.padding(start = 12.dp)
.weight(1f)
) {
Text(
text = stringResource(id = data.titleRes),
style = MaterialTheme.typography.bodyMedium
)
if (data.progress >= 0f) {
LinearProgressIndicator(
progress = data.progress,
strokeCap = StrokeCap.Round,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
)
}
if (data.statusRes != NONE) {
Text(
text = stringResource(id = data.statusRes),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (data.actionRes != NONE) {
Buttons.Small(
onClick = onActionClick,
modifier = Modifier.padding(start = 8.dp)
) {
Text(text = stringResource(id = data.actionRes))
}
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun BackupStatusPreview() {
Previews.Preview {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
BackupStatus(
data = BackupStatusData.CouldNotCompleteBackup
)
BackupStatus(
data = BackupStatusData.NotEnoughFreeSpace
)
BackupStatus(
data = BackupStatusData.RestoringMedia(50, 100)
)
}
}
}
/**
* Sealed interface describing status data to display in BackupStatus widget.
*
* TODO [message-requests] - Finalize assets and text
*/
sealed interface BackupStatusData {
@get:DrawableRes
val iconRes: Int
@get:StringRes
val titleRes: Int
val iconColors: BackupsIconColors
@get:StringRes
val actionRes: Int get() = NONE
@get:StringRes
val statusRes: Int get() = NONE
val progress: Float get() = NONE.toFloat()
/**
* Generic failure
*/
object CouldNotCompleteBackup : BackupStatusData {
override val iconRes: Int = R.drawable.symbol_backup_light
override val titleRes: Int = R.string.default_error_msg
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
}
/**
* User does not have enough space on their device to complete backup restoration
*/
object NotEnoughFreeSpace : BackupStatusData {
override val iconRes: Int = R.drawable.symbol_backup_light
override val titleRes: Int = R.string.default_error_msg
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
override val actionRes: Int = R.string.registration_activity__skip
}
/**
* Restoring media, finished, and paused states.
*/
data class RestoringMedia(
val bytesDownloaded: Long,
val bytesTotal: Long,
val status: Status = Status.NONE
) : BackupStatusData {
override val iconRes: Int = R.drawable.symbol_backup_light
override val iconColors: BackupsIconColors = BackupsIconColors.Normal
override val titleRes: Int = when (status) {
Status.NONE -> R.string.default_error_msg
Status.LOW_BATTERY -> R.string.default_error_msg
Status.WAITING_FOR_INTERNET -> R.string.default_error_msg
Status.WAITING_FOR_WIFI -> R.string.default_error_msg
Status.FINISHED -> R.string.default_error_msg
}
override val statusRes: Int = when (status) {
Status.NONE -> R.string.default_error_msg
Status.LOW_BATTERY -> R.string.default_error_msg
Status.WAITING_FOR_INTERNET -> R.string.default_error_msg
Status.WAITING_FOR_WIFI -> R.string.default_error_msg
Status.FINISHED -> R.string.default_error_msg
}
override val progress: Float = if (bytesTotal > 0) {
min(1f, max(0f, bytesDownloaded.toFloat() / bytesTotal))
} else {
0f
}
}
/**
* Describes the status of an in-progress media download session.
*/
enum class Status {
NONE,
LOW_BATTERY,
WAITING_FOR_INTERNET,
WAITING_FOR_WIFI,
FINISHED
}
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.view.LayoutInflater
import android.view.ViewGroup
@@ -30,26 +30,23 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.updateLayoutParams
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
import java.util.Currency
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessageBackupsCheckoutSheet(
messageBackupsType: MessageBackupsType,
availablePaymentGateways: List<GatewayResponse.Gateway>,
messageBackupTier: MessageBackupTier,
availablePaymentMethods: List<InAppPaymentData.PaymentMethodType>,
onDismissRequest: () -> Unit,
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
onPaymentMethodSelected: (InAppPaymentData.PaymentMethodType) -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismissRequest,
@@ -57,22 +54,25 @@ fun MessageBackupsCheckoutSheet(
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
messageBackupsType = messageBackupsType,
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = onPaymentGatewaySelected
messageBackupTier = messageBackupTier,
availablePaymentGateways = availablePaymentMethods,
onPaymentGatewaySelected = onPaymentMethodSelected
)
}
}
@Composable
private fun SheetContent(
messageBackupsType: MessageBackupsType,
availablePaymentGateways: List<GatewayResponse.Gateway>,
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
messageBackupTier: MessageBackupTier,
availablePaymentGateways: List<InAppPaymentData.PaymentMethodType>,
onPaymentGatewaySelected: (InAppPaymentData.PaymentMethodType) -> Unit
) {
val resources = LocalContext.current.resources
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
val backupTypeDetails = remember(messageBackupTier) {
getTierDetails(messageBackupTier)
}
val formattedPrice = remember(backupTypeDetails.pricePerMonth) {
FiatMoneyUtil.format(resources, backupTypeDetails.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
@@ -88,7 +88,7 @@ private fun SheetContent(
)
MessageBackupsTypeBlock(
messageBackupsType = messageBackupsType,
messageBackupsType = backupTypeDetails,
isSelected = false,
onSelected = {},
enabled = false,
@@ -101,25 +101,27 @@ private fun SheetContent(
) {
availablePaymentGateways.forEach {
when (it) {
GatewayResponse.Gateway.GOOGLE_PAY -> GooglePayButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.GOOGLE_PAY)
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> GooglePayButton {
onPaymentGatewaySelected(InAppPaymentData.PaymentMethodType.GOOGLE_PAY)
}
GatewayResponse.Gateway.PAYPAL -> PayPalButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.PAYPAL)
InAppPaymentData.PaymentMethodType.PAYPAL -> PayPalButton {
onPaymentGatewaySelected(InAppPaymentData.PaymentMethodType.PAYPAL)
}
GatewayResponse.Gateway.CREDIT_CARD -> CreditOrDebitCardButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.CREDIT_CARD)
InAppPaymentData.PaymentMethodType.CARD -> CreditOrDebitCardButton {
onPaymentGatewaySelected(InAppPaymentData.PaymentMethodType.CARD)
}
GatewayResponse.Gateway.SEPA_DEBIT -> SepaButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.SEPA_DEBIT)
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> SepaButton {
onPaymentGatewaySelected(InAppPaymentData.PaymentMethodType.SEPA_DEBIT)
}
GatewayResponse.Gateway.IDEAL -> IdealButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.IDEAL)
InAppPaymentData.PaymentMethodType.IDEAL -> IdealButton {
onPaymentGatewaySelected(InAppPaymentData.PaymentMethodType.IDEAL)
}
InAppPaymentData.PaymentMethodType.UNKNOWN -> error("Unsupported payment method type $it")
}
}
}
@@ -221,30 +223,7 @@ private fun CreditOrDebitCardButton(
@Preview
@Composable
private fun MessageBackupsCheckoutSheetPreview() {
val paidTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
title = "Text + All your media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!"
)
)
)
val availablePaymentGateways = GatewayResponse.Gateway.values().toList()
val availablePaymentGateways = InAppPaymentData.PaymentMethodType.values().toList() - InAppPaymentData.PaymentMethodType.UNKNOWN
Previews.Preview {
Column(
@@ -252,7 +231,7 @@ private fun MessageBackupsCheckoutSheetPreview() {
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
messageBackupsType = paidTier,
messageBackupTier = MessageBackupTier.PAID,
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = {}
)

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.Image
import androidx.compose.foundation.background

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.os.Bundle
import androidx.activity.compose.setContent
@@ -32,6 +32,10 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
fun MessageBackupsScreen.next() {
val nextScreen = viewModel.goToNextScreen(this)
if (nextScreen == MessageBackupsScreen.COMPLETED) {
finishAfterTransition()
return
}
if (nextScreen != this) {
navController.navigate(nextScreen.name)
}
@@ -53,7 +57,7 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
NavHost(
navController = navController,
startDestination = MessageBackupsScreen.EDUCATION.name,
startDestination = if (state.currentMessageBackupTier == null) MessageBackupsScreen.EDUCATION.name else MessageBackupsScreen.TYPE_SELECTION.name,
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
@@ -88,9 +92,9 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
MessageBackupsTypeSelectionScreen(
selectedBackupsType = state.selectedMessageBackupsType,
availableBackupsTypes = state.availableBackupsTypes,
onMessageBackupsTypeSelected = viewModel::onMessageBackupsTypeUpdated,
selectedBackupTier = state.selectedMessageBackupTier,
availableBackupTiers = state.availableBackupTiers,
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
onNavigationClick = navController::popOrFinish,
onReadMoreClicked = {},
onNextClicked = { MessageBackupsScreen.TYPE_SELECTION.next() }
@@ -99,11 +103,11 @@ class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
dialog(route = MessageBackupsScreen.CHECKOUT_SHEET.name) {
MessageBackupsCheckoutSheet(
messageBackupsType = state.selectedMessageBackupsType!!,
availablePaymentGateways = state.availablePaymentGateways,
messageBackupTier = state.selectedMessageBackupTier!!,
availablePaymentMethods = state.availablePaymentMethods,
onDismissRequest = navController::popOrFinish,
onPaymentGatewaySelected = {
viewModel.onPaymentGatewayUpdated(it)
onPaymentMethodSelected = {
viewModel.onPaymentMethodUpdated(it)
MessageBackupsScreen.CHECKOUT_SHEET.next()
}
)

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