Compare commits

...

1353 Commits

Author SHA1 Message Date
Alex Hart
c503df5eec Bump version to 6.22.2 2023-05-26 15:55:12 -03:00
Alex Hart
1f242473fe Updated baseline profile. 2023-05-26 15:51:55 -03:00
Alex Hart
f7db5f8ae0 Updated language translations. 2023-05-26 15:46:39 -03:00
Greyson Parrelli
3b88d7cf94 Update notifications after transaction completes. 2023-05-26 14:23:07 -04:00
Nicholas Tinsley
cac82f2eba Make license screen content static. 2023-05-26 11:56:27 -04:00
Greyson Parrelli
5811b469cf Observe empty state on main thread. 2023-05-26 09:35:07 -04:00
Nicholas Tinsley
af1175f32e Update CameraX. 2023-05-25 18:03:45 -04:00
Nicholas Tinsley
7a5ce5761f Add tap to send debug log to account locked screen.
Addresses #12950.
2023-05-25 18:02:25 -04:00
Nicholas
34104355cb Bump version to 6.22.1 2023-05-25 17:12:21 -04:00
Nicholas
143c1255d8 Updated baseline profile. 2023-05-25 17:11:52 -04:00
Nicholas
a9d0e5ac81 Updated language translations. 2023-05-25 17:09:14 -04:00
Greyson Parrelli
c8b3ee51ed Acquire group lock before processing a message batch. 2023-05-25 16:07:26 -04:00
Cody Henthorne
c964067139 Fix placeholder in username string. 2023-05-25 15:59:32 -04:00
Nicholas Tinsley
539f590c4c Disconnect Bluetooth SCO when user cancels recording. 2023-05-25 11:57:13 -04:00
Nicholas Tinsley
71dddd4a1b Add some more Bluetooth connection logging. 2023-05-25 11:55:42 -04:00
Alex Hart
792f5dd7b5 Always display bottom bar. 2023-05-25 12:49:36 -03:00
Nicholas
a3af49d92a Bump version to 6.22.0 2023-05-24 12:15:26 -04:00
Nicholas
4289b43a81 Updated baseline profile. 2023-05-24 12:14:47 -04:00
Nicholas
7f55623acf Updated language translations. 2023-05-24 12:11:15 -04:00
Nicholas Tinsley
52060b65be Disable all icons other than the active one. 2023-05-24 12:05:23 -04:00
Clark
fd826749e4 Edit message design tweaks. 2023-05-24 12:05:23 -04:00
Clark
627e15c3dd Add thumbnail for when editing message with media. 2023-05-24 12:05:23 -04:00
Clark
90f6890180 Enqueue thread update job after transaction completes. 2023-05-24 12:05:23 -04:00
Nicholas Tinsley
61eb397d2b Simplify notification for saving media.
Addresses #11759.
2023-05-24 12:05:23 -04:00
Greyson Parrelli
3a5e5364c7 Remove support for legacy gv1 sync messages. 2023-05-24 12:05:23 -04:00
Greyson Parrelli
25779d04a6 Regularly run account consistency checks. 2023-05-24 12:05:23 -04:00
Clark
242900e87f Dont requery attachments and add all jobs at once. 2023-05-24 12:05:23 -04:00
Nicholas Tinsley
05f07d1788 Handle SmsRetriever initialization cancellation. 2023-05-24 12:05:23 -04:00
Alex Hart
f961f4ccac Add initial CFV2 long press state implementation. 2023-05-24 12:05:23 -04:00
Nicholas Tinsley
145377b05f Add accessibility labels to navigate up button in ConversationFragment.
Addresses #12951.
2023-05-24 12:05:23 -04:00
Cody Henthorne
bc88887195 Animate CFv2 with keyboard opening or closing. 2023-05-24 12:05:23 -04:00
Nicholas Tinsley
5362b1c21c Prevent NPE when finishing voice memo recording. 2023-05-24 12:05:23 -04:00
Clark
0cfd3265ba Fix post transaction tasks not actually running. 2023-05-24 12:05:23 -04:00
Cody Henthorne
1099128513 Add rendering and handling for various disabled input states in CFv2. 2023-05-24 12:05:23 -04:00
Greyson Parrelli
ad50c81a6b Remove unnecessary validation check. 2023-05-24 12:05:23 -04:00
Clark
0817f113c6 Schedule media downloads after successful transaction. 2023-05-24 12:05:23 -04:00
Clark
4a9a07a9ef Run post transaction tasks only after root transaction ends. 2023-05-24 12:05:23 -04:00
Nicholas
61f50cfe60 Add license screen to settings page. 2023-05-24 12:05:23 -04:00
Nicholas
92888778c2 Restart websocket immediately upon network change. 2023-05-24 12:05:23 -04:00
Alex Hart
987f9b9dba Allow call links to exist in the calls tab. 2023-05-24 12:05:23 -04:00
Clark
97d95f37cc Rotate profile key when contact hidden. 2023-05-24 11:29:59 -04:00
Clark
836cd04564 Inline message processing when we can. 2023-05-24 11:29:59 -04:00
Clark
c26f54161d Use original message id for edit message history. 2023-05-24 11:29:59 -04:00
Clark
b540009ce6 Only call start foreground once from FCM. 2023-05-24 11:29:59 -04:00
Alex Hart
e58e209950 Remove ripple from tab buttons. 2023-05-24 11:29:58 -04:00
Alex Hart
4597a23104 Fix new call item margins. 2023-05-24 11:29:58 -04:00
Alex Hart
3aacf4bcd2 Add search highlight to call rows. 2023-05-24 11:29:58 -04:00
Alex Hart
6e6b663fac Reposition unread dots according to figma. 2023-05-24 11:29:58 -04:00
Alex Hart
6dad7eafcf Fix call tab color and spacing. 2023-05-24 11:29:58 -04:00
Alex Hart
5a38143987 Integrate call links create/update/read apis. 2023-05-24 11:29:58 -04:00
Greyson Parrelli
4d6d31d624 Make attachment count/size remote configurable. 2023-05-24 11:29:58 -04:00
Greyson Parrelli
938c82be3f Inline the calls tab feature flag. 2023-05-24 11:29:58 -04:00
Greyson Parrelli
dc2e249566 Add QR scanning to username link flow. 2023-05-24 11:29:58 -04:00
Greyson Parrelli
bb8fdcabcb Update to libsignal 0.25.0 2023-05-24 11:29:58 -04:00
Greyson Parrelli
6cf4dbc78c Add pre-alpha support for SVR2. 2023-05-24 11:29:58 -04:00
Jim Gustafson
8cd0ac5451 Update to RingRTC v2.27.0 2023-05-24 11:29:58 -04:00
Clark
33745f0b0c Fix edit message showing twice in notifications. 2023-05-24 11:29:58 -04:00
Nicholas
29ffed219f Bump version to 6.21.3 2023-05-24 10:52:40 -04:00
Nicholas
9629d0f715 Updated baseline profile. 2023-05-24 10:51:42 -04:00
Nicholas
01651984d7 Updated language translations. 2023-05-24 10:36:15 -04:00
Nicholas Tinsley
c7c15250ca Clarify app icon string descriptions. 2023-05-24 10:00:38 -04:00
Greyson Parrelli
36b96eafcc Bump version to 6.21.2 2023-05-19 16:37:26 -04:00
Greyson Parrelli
94f930ee22 Updated language translations. 2023-05-19 16:37:12 -04:00
Greyson Parrelli
3f740d2904 Tweak network timeout settings. 2023-05-19 16:30:19 -04:00
Nicholas Tinsley
f8529adfcf Design tweaks for app icon switching. 2023-05-19 16:30:19 -04:00
Alex Hart
77ccbdd322 Deduplicate in migration to prevent constraint breakage. 2023-05-19 16:30:19 -04:00
Cody Henthorne
f2846efd2c Fix split second spoiler reveal when quoting a message with a spoiler. 2023-05-19 16:30:19 -04:00
Nicholas Tinsley
131f9c4bc9 Move app icon composables outside of mutable Fragment class.
This way, the composables do not receive an implicit mutable parameter, which allows the compiler to mark them as skippable.
2023-05-19 16:30:19 -04:00
Greyson Parrelli
d7c06fff50 Bump version to 6.21.1 2023-05-18 20:35:52 -04:00
Greyson Parrelli
09a22d9dc4 Updated language translations. 2023-05-18 20:35:52 -04:00
Greyson Parrelli
0fbab04253 Animate transitions in icon selection. 2023-05-18 20:35:51 -04:00
Nicholas
c963e99dca Introduce the ability to change the app icon. 2023-05-18 20:35:51 -04:00
Alex Hart
7a555d127f Tighten migration and remove null peer events. 2023-05-18 20:35:51 -04:00
Cody Henthorne
866408f673 Limit body ranges processed on received messages. 2023-05-18 20:35:51 -04:00
Greyson Parrelli
a7e5ab1a6a Update inbound attachment processing. 2023-05-18 12:22:35 -04:00
Alex Hart
14d16d61e6 Only update text fields if contents changed. 2023-05-18 10:27:17 -03:00
Greyson Parrelli
b988e4a813 Disable foreign key constraints during backup restore updgrades. 2023-05-17 18:44:23 -04:00
Greyson Parrelli
ae33c8db1b Bump version to 6.21.0 2023-05-17 15:50:42 -04:00
Greyson Parrelli
9f1c5ac6bb Updated language translations. 2023-05-17 15:45:48 -04:00
Alex Hart
448e7d0739 Don't collapsed missed calls with outgoing or incoming. 2023-05-17 15:30:27 -04:00
Clark
8971ff9057 Fix for ForegroundServiceDidNotStartInTimeException for FcmFetchForegroundService. 2023-05-17 15:30:26 -04:00
Clark
2d6b16b2ce Introduce extra caching for group message processing. 2023-05-17 15:30:26 -04:00
Greyson Parrelli
44ab1643fa Fix group membership recipient remapping. 2023-05-17 15:30:26 -04:00
Cody Henthorne
a64bffd83a Complete text formatting. 2023-05-17 15:30:26 -04:00
Clark
534c5c3c64 Try not blocking main threads to start foreground service. 2023-05-17 15:30:26 -04:00
Cody Henthorne
99d3f9918f Fix crash and bug with ellipsizing 'About' in Settings screen.
Fixes #12895
Closes #12905
2023-05-17 15:30:26 -04:00
Ehren Kret
aaebf029db Remove unused capabilities. 2023-05-17 15:30:26 -04:00
Greyson Parrelli
e2c2ace0e3 Add initial storage interfaces for kyber prekeys. 2023-05-17 15:30:26 -04:00
Nicholas Tinsley
c76002663f Push bubbled conversation onto back stack. 2023-05-17 15:30:26 -04:00
Nicholas Tinsley
c5317370c8 Prevent duplicate reactions bottom sheet. 2023-05-17 15:30:26 -04:00
Cody Henthorne
4b09f4a654 Add basic attachment keyboard support to CFv2. 2023-05-17 15:30:26 -04:00
Alex Hart
0c57113d8e Add migration for call link recipient link. 2023-05-17 15:30:26 -04:00
Alex Hart
d7dd77a5af Add additional logging around thumbnail loading. 2023-05-17 15:30:26 -04:00
Greyson Parrelli
5c5b88ebcc Pluralize some strings. 2023-05-17 15:30:26 -04:00
Alex Hart
8df0248d4f Utilize receipts instead of messagerecord count for views. 2023-05-17 15:30:26 -04:00
Alex Hart
58e48fdf14 Fix switch toggling after first toggle. 2023-05-17 15:30:26 -04:00
Alex Hart
407fc56218 Utilize conversationRecipient for displaying whom a payment is to or from in conversation. 2023-05-17 15:30:26 -04:00
Alex Hart
398527d3f1 Ignore table update when remap already exists. 2023-05-17 15:30:26 -04:00
Greyson Parrelli
59745a695c Add context to some strings. 2023-05-17 15:30:26 -04:00
Cody Henthorne
2aaeda6ca8 Add initial send support to CFv2. 2023-05-17 15:30:26 -04:00
Alex Hart
5c78de2f46 Update nullability of CallLinkTable column. 2023-05-17 15:30:09 -04:00
Clark
93efc21452 Propogate read sync message to latest revision. 2023-05-17 15:30:09 -04:00
Clark
50ad005e7c Update edit message history dialog to match designs. 2023-05-17 15:30:09 -04:00
Clark
71c3bcdd29 Convert MSL delete entries for recipient to collection query. 2023-05-17 15:30:08 -04:00
Clark
e5bf04a407 Cleanup Recipient.externalPush to use RecipientId cache. 2023-05-17 15:30:08 -04:00
Clark
fe8b2cb761 Reduce db operations when updating thread snippet. 2023-05-17 15:30:08 -04:00
Clark
7c37f929a5 Go back to enqueuing thread update job. 2023-05-17 15:30:08 -04:00
Cody Henthorne
1b82d10b39 Squelch notifications in noisy groups and during large initial message processing. 2023-05-17 15:30:08 -04:00
Clark
6b6e2490e7 Add performance logging for message processing. 2023-05-17 15:30:08 -04:00
Greyson Parrelli
ebee54cf92 Add button to grow query area in Spinner. 2023-05-17 15:30:08 -04:00
Greyson Parrelli
b7acfb0dcc Add a better editor to Spinner. 2023-05-17 15:30:08 -04:00
Greyson Parrelli
65dab45582 Update string for sticker uninstall button label. 2023-05-17 15:30:08 -04:00
Greyson Parrelli
43086f9582 Fix more validation spots around unknown UUIDs.
This was a legacy path that got missed.
2023-05-17 15:30:08 -04:00
Clark
ed1aa74aff Reduce number of thread snippet updates from receipts. 2023-05-17 15:30:08 -04:00
Cody Henthorne
3ba128793a Display thread header in CFv2. 2023-05-17 15:30:08 -04:00
Clark
ffbbdc1576 Add PushProcessMessageJobV2 to reserved job queue. 2023-05-17 15:30:08 -04:00
Clark
6ff55cfff7 Reduce expensive group operations during message processing. 2023-05-17 15:29:31 -04:00
Clark
e9f1f781e1 Reduce number of db calls to getGroup. 2023-05-17 15:29:31 -04:00
Greyson Parrelli
6da36fe098 Deprecate the SyncMessage.pniIdentity field. 2023-05-17 15:29:31 -04:00
Greyson Parrelli
acb6510312 Switch to libsignal for PIN hashing. 2023-05-17 15:29:30 -04:00
Greyson Parrelli
13248506c5 Bump version to 6.20.5 2023-05-17 15:25:38 -04:00
Greyson Parrelli
fb9740f5c3 Updated language translations. 2023-05-17 15:25:16 -04:00
Nicholas Tinsley
61bf788f52 Tear down Bluetooth connection after voice memo recording. 2023-05-17 14:30:30 -04:00
Greyson Parrelli
fd116e0178 Bump version to 6.20.4 2023-05-15 21:29:08 -04:00
Greyson Parrelli
661d981231 Updated language translations. 2023-05-15 21:28:16 -04:00
Greyson Parrelli
6a701591af End transaction before handling db error. 2023-05-15 21:14:29 -04:00
Greyson Parrelli
02431c6ef4 Refactor array creation to a function. 2023-05-15 13:21:57 -04:00
Nicholas Tinsley
802b179880 Update license to AGPLv3. 2023-05-15 10:23:28 -04:00
Cody Henthorne
6c2104b84b Fix media not auto-downloading in groups bug. 2023-05-12 15:39:32 -04:00
Cody Henthorne
a3f432dc88 Bump version to 6.20.3 2023-05-12 14:14:43 -04:00
Cody Henthorne
7f4caedf40 Updated baseline profile. 2023-05-12 14:04:16 -04:00
Cody Henthorne
eebd06f0d8 Updated language translations. 2023-05-12 13:59:34 -04:00
Greyson Parrelli
0ea66b6bb0 Update apkdiff to ignore baseline profile. 2023-05-12 10:38:04 -04:00
Greyson Parrelli
084cdd7200 Fix de-duping migration when resolving date conflicts. 2023-05-12 09:45:02 -04:00
Greyson Parrelli
2eff9e0230 Update default conflict method to be 'ignore'. 2023-05-12 09:26:44 -04:00
Greyson Parrelli
6615bc4a2a Fix param on sync message. 2023-05-11 18:47:04 -04:00
Greyson Parrelli
387f18be98 Avoid some 401 errors during story sends. 2023-05-11 16:54:57 -04:00
Cody Henthorne
3b5a3eccfe Bump version to 6.20.2 2023-05-11 15:34:49 -04:00
Cody Henthorne
36083a8bd9 Updated baseline profile. 2023-05-11 15:28:56 -04:00
Cody Henthorne
7e16825bf4 Updated language translations. 2023-05-11 15:26:01 -04:00
Greyson Parrelli
9ba2724d0a Fix V190 database migration with a new migration. 2023-05-11 15:21:22 -04:00
Greyson Parrelli
7866851f5d Temporarily give up on the V190 migration. 2023-05-11 11:17:01 -04:00
Greyson Parrelli
c938035ec1 Improve error handling around unknown UUIDs. 2023-05-11 11:15:13 -04:00
Nicholas Tinsley
965fdc5e9b Fix linked devices reminder appearing all the time. 2023-05-11 11:08:06 -04:00
Nicholas Tinsley
eab932b4a0 Fix parsing for registration 502 errors. 2023-05-11 10:58:04 -04:00
Cody Henthorne
27a24262c8 Bump version to 6.20.1 2023-05-10 16:18:21 -04:00
Cody Henthorne
31584de225 Updated baseline profile. 2023-05-10 16:10:41 -04:00
Cody Henthorne
6b2f90019a Updated language translations. 2023-05-10 16:05:55 -04:00
Nicholas
208147db9e Make change number error notifications more prominent. 2023-05-10 15:59:26 -04:00
Nicholas
e4f70fa4fe Delegate to system to handle rotation in video call PiP. 2023-05-10 15:59:26 -04:00
Cody Henthorne
4d09abd0d3 Fix group state loss during concurrent updates.
Storage sync and a message process can both attempt to create a group at the same time. Message processing caches the local group state and performs networking which allows the cached state to become stale. The state was being used to decide to call create instead of update and the create would fail silently as the group record already exists. This would cause state to not be persisted and result in odd double events.
2023-05-10 15:59:26 -04:00
Greyson Parrelli
1304f4dc39 Update apkdiff.py to return non-zero exit codes on mismatch. 2023-05-10 15:59:26 -04:00
Nicholas Tinsley
ac027d9267 Store linked devices presence in separate key than reminder. 2023-05-10 15:59:26 -04:00
Cody Henthorne
0b6d343616 Fix tint on call rows in chat settings. 2023-05-10 15:59:26 -04:00
Nicholas
92e8f125f9 Improve nullability for setting communication devices. 2023-05-10 15:59:26 -04:00
Greyson Parrelli
bef15482af Add unique index on message (sentTimestamp, author, thread). 2023-05-10 15:59:26 -04:00
Greyson Parrelli
93d78b3b2e Improve conditional logic around prekey refresh schedule. 2023-05-09 15:35:48 -04:00
Cody Henthorne
d38b7deeeb Bump version to 6.20.0 2023-05-09 14:17:49 -04:00
Cody Henthorne
b656e1dd0a Updated baseline profile. 2023-05-09 13:53:50 -04:00
Cody Henthorne
f1cec895b9 Updated language translations. 2023-05-09 13:48:06 -04:00
Greyson Parrelli
9bf6922d97 Ensure users have a service identifier before sending receipts. 2023-05-09 13:41:28 -04:00
Greyson Parrelli
41fc4096e4 Fix migration crash if user is unregistered. 2023-05-09 13:41:28 -04:00
Nicholas
e46564cb7e Add jitter to backup scheduling. 2023-05-09 13:41:28 -04:00
Clark
77751c1d28 Add read through cache for thread id. 2023-05-09 13:41:28 -04:00
Clark
054a1e4017 Reduce number of db calls when processing data messages. 2023-05-09 13:41:28 -04:00
Greyson Parrelli
da27d74111 Improve rendering of nulls in Spinner results. 2023-05-09 13:41:28 -04:00
Greyson Parrelli
970278228d Replace 'audio call' with 'voice call'. 2023-05-09 13:41:28 -04:00
Greyson Parrelli
ee3f2d62cf Add extra guard against inserting unnecessary error messages. 2023-05-09 13:41:28 -04:00
Greyson Parrelli
ec6d5031cf Made table headers in Spinner sticky. 2023-05-09 13:41:28 -04:00
Greyson Parrelli
80f338c3af Increase Spinner query history to 25 items. 2023-05-09 13:41:28 -04:00
Cody Henthorne
268c9a1c26 Fix conversation list not updating with current state. 2023-05-09 13:41:27 -04:00
Jon Chambers
e8e01b5965 Remove vestiges of CDS "classic". 2023-05-09 13:41:27 -04:00
Alex Hart
23a042b667 Fix translatable value. 2023-05-09 13:41:27 -04:00
Clark
c2c1537858 Disable interactions while user is unregistered or expired. 2023-05-09 13:41:27 -04:00
Cody Henthorne
65d5f4c426 Add ConversationAdapterV2. 2023-05-09 13:41:27 -04:00
Alex Hart
a1eb33b1f6 CallLinkTable migration to add necessary columns for integration. 2023-05-09 13:41:27 -04:00
Alex Hart
9eadd92d05 Ensure websocket state changes are handled on main thread. 2023-05-05 13:55:45 -03:00
Nicholas
b0e1294584 Only show linked devices reminder if devices previously linked. 2023-05-05 12:49:18 -03:00
Nicholas
f1fd29a477 Use Bluetooth headset mic to record voice notes. 2023-05-05 12:49:18 -03:00
Cody Henthorne
fc9a6b98d1 Add sync group sent text message processing test. 2023-05-05 12:49:18 -03:00
Alex Hart
305f6c610c Fix bad formatting in CallTable. 2023-05-05 12:49:16 -03:00
Alex Hart
66f4732db5 Reimplement MessageRequestViewModel for CFV2. 2023-05-05 12:48:53 -03:00
Nicholas
ccdfa546b4 Prevent launching multiple audio device dialogs during call. 2023-05-05 12:48:53 -03:00
Greyson Parrelli
855e194baa Add initial username link screen + QR code generation. 2023-05-05 12:48:53 -03:00
Alex Hart
e0c06615fb Upgrade to libsignal 0.23.1 2023-05-05 12:48:53 -03:00
Alex Hart
03a4809866 Add local directory to .gitignore. 2023-05-05 12:48:53 -03:00
Clark
0aa7bd6a4e Reorder security provider initialization. 2023-05-05 12:48:53 -03:00
Greyson Parrelli
78b530f8b8 Show toast to internal users for invalid messages. 2023-05-05 12:48:53 -03:00
Nicholas Tinsley
ace47c61b1 Update top-level LICENSE file to AGPL 2023-05-05 12:48:53 -03:00
Clark
29796f51d7 Try calling startForeground in onCreate. 2023-05-05 12:48:53 -03:00
Alex Hart
4d2ce7a2be Batch call event syncs.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2023-05-05 12:48:53 -03:00
Alex Hart
8dc45263cd Add requestLayout when textfields are updated.
These fields appear to not resize themselves correctly and there appears
to be a possible race where they can end up wrongly ellipsizing
themselves.
2023-05-05 12:48:53 -03:00
Greyson Parrelli
d8d95d8efe Update SQLCipher to 4.5.4-S1 2023-05-05 12:48:53 -03:00
Greyson Parrelli
f081591354 Add Android 14 improvements for dynamic shortcuts.
Closes #12923
Co-authored-by: Yuichi Araki <yaraki@google.com>
2023-05-05 12:48:53 -03:00
Nicholas
35e96fecdb Launch the MediaPreviewV2Activity in its own stack from Bubbles. 2023-05-05 12:48:53 -03:00
Nicholas
841fbfa7ee Improve error handling for external failures during registration.
Addresses #10711 and #12927.
2023-05-05 12:48:53 -03:00
Nicholas
89397ae7cc Picture-in-Picture call improvements. 2023-05-05 12:48:53 -03:00
Alex Hart
6c57c2ac2a Fix crash from external share. 2023-05-05 12:48:53 -03:00
Jim Gustafson
40663eb52f Update to RingRTC v2.26.3 2023-05-05 12:48:53 -03:00
Greyson Parrelli
90c9cc17b9 Handle unregistered responses in more locations.
There were some send jobs where we knew users were unregistered, but we
weren't marking them as such in the DB.
2023-05-05 12:48:53 -03:00
Nicholas
125c4f43cf Make audio device button directly toggle when only two devices are present. 2023-05-05 12:48:53 -03:00
Greyson Parrelli
634e4abcc1 Use the word 'chat' instead of 'conversation'. 2023-05-05 12:48:53 -03:00
Greyson Parrelli
a5431330d1 Ensure user has a serviceId/e164 before attempting a read receipt. 2023-05-05 12:48:53 -03:00
Alex Hart
30fc6d94c5 Flesh out event listeners and add load sequencing to CFV2. 2023-05-05 12:48:53 -03:00
Alex Hart
694d8f1984 Add scroll buttons to CFV2. 2023-05-05 12:48:53 -03:00
Alex Hart
bff8fc8230 Add call link details screen scaffolding. 2023-05-05 12:48:53 -03:00
Alex Hart
5f7414e84c Bump version to 6.19.8 2023-05-05 12:47:12 -03:00
Greyson Parrelli
8c707555f2 Fix bad migration state that could happen during a device transfer. 2023-05-05 12:42:31 -03:00
Greyson Parrelli
63ce2de3bf Add some more missing indexes for foreign keys and create test. 2023-05-05 09:27:40 -04:00
Alex Hart
5e86cca277 Ensure call events are reverse-chron by timestamp. 2023-05-05 10:12:58 -03:00
Alex Hart
4bbed2601c Bump version to 6.19.7 2023-05-04 13:57:20 -03:00
Alex Hart
e0bda8cf53 Fix group call ring state for some calls. 2023-05-04 13:31:09 -03:00
Greyson Parrelli
a1c807d65b Force usage of best index for conversation query. 2023-05-04 09:35:45 -04:00
Greyson Parrelli
77f5c290cc Add 'if not exists' to index migration. 2023-05-04 09:33:29 -04:00
Alex Hart
4294b446f3 Hide call events from hidden and blocked contacts. 2023-05-04 10:02:15 -03:00
Alex Hart
664e8e5526 Bump version to 6.19.6 2023-05-03 13:23:13 -03:00
Alex Hart
264ade3db9 Updated baseline profile. 2023-05-03 13:16:07 -03:00
Alex Hart
1a6e5e9e2b Updated language translations. 2023-05-03 13:11:34 -03:00
Greyson Parrelli
6aa4bb549a Add database indices to improve message delete performance. 2023-05-03 09:59:47 -04:00
Alex Hart
dd76909f02 Bump version to 6.19.5 2023-05-02 14:51:17 -03:00
Alex Hart
a454adece8 Updated baseline profile. 2023-05-02 14:50:48 -03:00
Alex Hart
f2adc4d283 Updated language translations. 2023-05-02 14:46:00 -03:00
Clark
6af9835f74 Fix sync edit message group validation. 2023-05-02 12:41:33 -04:00
Alex Hart
b823baa387 Animate when entering and exiting call log multiselect. 2023-05-02 13:24:08 -03:00
Alex Hart
6791d2d46e Add animation when switching from chats to calls. 2023-05-02 13:10:39 -03:00
Alex Hart
3c343af562 Fix dancing call icon when a new message is recieved. 2023-05-02 13:10:23 -03:00
Greyson Parrelli
fe9ed4c5f7 Attempt to repair local recipient state in the V185 migration. 2023-05-02 10:04:57 -04:00
Alex Hart
7374e7ee23 Fix PiP crash on devices that lie about support.
Fixes #12924
2023-05-02 09:51:45 -03:00
Alex Hart
3c9c0e244a Bump version to 6.19.4 2023-05-01 15:31:50 -03:00
Alex Hart
64c219b02d Updated baseline profile. 2023-05-01 15:31:15 -03:00
Alex Hart
7e68f8faf2 Updated language translations. 2023-05-01 15:26:06 -03:00
Greyson Parrelli
c868098042 Fix 'Sent from' section in message details. 2023-05-01 13:13:13 -04:00
Greyson Parrelli
b3c0cda2be Fix rendering of some update message types. 2023-05-01 10:09:57 -04:00
Nicholas
2176d1f3df Bump version to 6.19.3 2023-04-25 16:19:26 -04:00
Nicholas
979b9859af Updated baseline profile. 2023-04-25 16:08:42 -04:00
Nicholas
a9ce4a1aed Updated language translations. 2023-04-25 16:03:22 -04:00
Greyson Parrelli
a01fb7ff1c Fix foreign key constraint issues with backup restore. 2023-04-25 15:52:09 -04:00
Alex Hart
0e631508b2 Fix call log multiselect deletions. 2023-04-25 16:42:33 -03:00
Greyson Parrelli
eb9915d445 Fix FTS rebuild retry. 2023-04-25 14:45:39 -04:00
Alex Hart
eedf7d4280 Update threads on call event message deletes. 2023-04-25 14:49:49 -03:00
Cody Henthorne
aaca487b8f Improve performance around marking messages read. 2023-04-25 11:29:45 -04:00
Alex Hart
a7d6c0f25c Prevent filtering when in multiselect mode. 2023-04-25 11:46:37 -03:00
Nicholas
158a250357 Force WiFi-to-cellular popup to use light mode text color. 2023-04-25 10:03:34 -04:00
Cody Henthorne
b9d7d19dea Fix multiple issues with rendering spoilers as story captions. 2023-04-25 09:51:11 -04:00
Alex Hart
a837f86999 Disable scrolling when context menu is open. 2023-04-25 10:36:27 -03:00
Cody Henthorne
a0e4b1aaf9 Fix weird highlight shown after revealing a spoiler. 2023-04-24 22:45:06 -04:00
Cody Henthorne
4d10be2aa5 Fix spoiler reveal in full screen media viewer. 2023-04-24 21:27:11 -04:00
Nicholas
8c74ae2fec Bump version to 6.19.2 2023-04-24 19:21:41 -04:00
Nicholas
7773341546 Updated baseline profile. 2023-04-24 19:20:42 -04:00
Nicholas
50747aa0d0 Updated language translations. 2023-04-24 19:17:10 -04:00
Greyson Parrelli
353434e1ef Possible fix for SQLite error. 2023-04-24 19:12:11 -04:00
Nicholas
d70213e031 Mute the camera if Signal loses focus.
Addresses #12754.
2023-04-24 19:12:10 -04:00
Cody Henthorne
763e14f25f Do not reveal spoilers in quotes. 2023-04-24 19:12:10 -04:00
Cody Henthorne
806e81743c Fix spoiler rendering in conversation list. 2023-04-24 19:12:10 -04:00
Clark
b0b2b02a49 Fix draft edit messages not being cleared. 2023-04-24 19:12:10 -04:00
Clark Chen
5acf442279 Exit edit message when quoting a message. 2023-04-24 19:12:10 -04:00
Clark
324e83489e Fix edit message with media. 2023-04-24 19:12:10 -04:00
Clark
8505530547 Fix link preview thumbnail not matching when receiving quote of edited message. 2023-04-24 09:43:41 -07:00
Alex Hart
43565d3414 Call icon and toast restructure. 2023-04-24 13:41:20 -03:00
Clark
9cb8fc8ef5 Fix edit message when editing message sent to self. 2023-04-24 08:19:07 -07:00
Greyson Parrelli
a11c34d1f6 Fix migration crash for unregistered users. 2023-04-24 10:14:43 -04:00
Alex Hart
03ef778dee Hide call button when entering action mode. 2023-04-24 10:40:48 -03:00
Alex Hart
fa61fa301c Fix incoming call notification on locked screen. 2023-04-24 10:16:03 -03:00
Cody Henthorne
b4f6177e87 Bump version to 6.19.1 2023-04-21 15:43:00 -04:00
Cody Henthorne
f5c16bf824 Updated baseline profile. 2023-04-21 15:33:37 -04:00
Cody Henthorne
c911dfa9f2 Updated language translations. 2023-04-21 15:30:24 -04:00
Clark Chen
297eb55c61 Correctly hide edit message from message selection menu. 2023-04-21 14:34:00 -04:00
Cody Henthorne
4fce7cc3cc Use distinct timestamp for sync message expire timer updates. 2023-04-21 13:44:34 -04:00
Cody Henthorne
1d793de213 Fix notification profile UI state bug and crash. 2023-04-21 13:37:03 -04:00
Alex Hart
211d79d14d Display caller avatar when showing CallStyle notification. 2023-04-21 14:34:47 -03:00
Cody Henthorne
f14bce9849 Fix unable to verify signed group change warning. 2023-04-21 12:37:28 -04:00
Cody Henthorne
8baf07a11c Refine link preview domain restrictions. 2023-04-21 11:40:16 -04:00
Alex Hart
a917dace6e Rev calls tab flag. 2023-04-20 17:47:12 -03:00
Clark
b3fef6c31e Stop dropping sync edit sent messages. 2023-04-20 15:41:50 -04:00
Cody Henthorne
05b4fea3fc Bump version to 6.19.0 2023-04-20 15:26:27 -04:00
Cody Henthorne
ab1ad28377 Updated baseline profile. 2023-04-20 15:01:17 -04:00
Cody Henthorne
5beb90dc19 Updated language translations. 2023-04-20 14:56:40 -04:00
Alex Hart
9e2e345a3e Add fix for toggle position on reglock. 2023-04-20 13:50:12 -04:00
Alex Hart
70f774dce9 Slight reorg of call links package. 2023-04-20 13:50:12 -04:00
Cody Henthorne
203b16e5a9 Update copy/dialogs for registration flow. 2023-04-20 13:50:12 -04:00
Clark
b3974d6e64 Resolve ANRs from job manager blocking incoming message observer. 2023-04-20 13:50:12 -04:00
Alex Hart
dc153ff4e6 Add support for jumping to quoted messages in CFV2. 2023-04-20 13:50:12 -04:00
Clark
5ddd7cdb9e Add sync message support for edit message. 2023-04-20 13:50:12 -04:00
Clark
85787ba1df Fix race for edit message notifying listeners. 2023-04-20 13:50:12 -04:00
Alex Hart
560b2f7d6f Utilize CallStyle for incoming and ongoing calls. 2023-04-20 13:50:12 -04:00
Alex Hart
8260be4bff Upgrade Kotlin to 1.8.10 and Core-KTX to 1.10.0 2023-04-20 13:50:12 -04:00
Alex Hart
65e0fae3f4 Add CFV2 Scroll-to-position wiring. 2023-04-20 13:50:12 -04:00
Alex Hart
e32b81dc2a Add new tab icons. 2023-04-20 13:50:12 -04:00
Alex Hart
10bcadb26a Allow two lines in contact refresh row item summary.
Fixes #12894
2023-04-20 13:50:12 -04:00
Alex Hart
93228f4be7 Prevent sending stickers to stories. 2023-04-20 13:50:12 -04:00
Alex Hart
e6a4067a35 Fix disposable leak in ShareActivity. 2023-04-20 13:50:12 -04:00
Alex Hart
baf9cd0909 Fix refresh of toggle state when dialog is dismissed in several cases. 2023-04-20 13:50:12 -04:00
Alex Hart
9081230286 Add ScrollToPositionDelegate and install in calls log fragment. 2023-04-20 13:50:12 -04:00
Greyson Parrelli
6db71f4a39 Improve performance of finding message positions in chats. 2023-04-20 13:50:12 -04:00
Iñaqui
d628921e48 Proceed with a stun server fallback 2023-04-20 13:50:12 -04:00
Alex Hart
240f27fbad Add correct id to barrier in call log row. 2023-04-20 13:50:12 -04:00
Clark
23a3d78deb Merge v186 and v185 migrations. 2023-04-20 13:50:12 -04:00
Alex Hart
47ebcc0f05 Upgrade ConstraintLayout to 2.1.4 2023-04-20 13:50:12 -04:00
Greyson Parrelli
3264a0a795 Fix receipts for stories. 2023-04-20 13:50:12 -04:00
Greyson Parrelli
deeaf2ba2e Put guard against self-foreign-keys causing infinite recursion. 2023-04-20 13:50:12 -04:00
Greyson Parrelli
646f79be7d Fix DatabaseConsistencyTest. 2023-04-20 13:50:12 -04:00
Clark
07f6baf7c1 Add message editing feature. 2023-04-20 13:50:12 -04:00
Cody Henthorne
4f06a0d27c Improve performance from thread being updated to data available to render. 2023-04-20 13:50:12 -04:00
Alex Hart
9d17bf473c Add several callbacks to v2 convo fragment. 2023-04-20 13:50:11 -04:00
Greyson Parrelli
279ad7945e Move to defined from_recipient_id and to_recipient_id columns on message table. 2023-04-20 13:50:11 -04:00
Alex Hart
d079f85eca Add GiphyMp4 controller to V2 Conversation Fragment. 2023-04-20 13:50:11 -04:00
Greyson Parrelli
bd8060e533 Updated error string. 2023-04-20 13:50:11 -04:00
Alex Hart
0b21481539 Add toast when long pressing camera button and video is not supported. 2023-04-20 13:50:11 -04:00
Alex Hart
3090a8521c Merge V2 Conversation Fragment behind an internal setting. 2023-04-20 13:50:11 -04:00
Alex Hart
5959545ae9 Add call log search support for group names. 2023-04-20 13:50:11 -04:00
Clark
e7f8d36199 Fix multidevice contact sync update job reporting wrong content length. 2023-04-20 13:50:11 -04:00
Alex Hart
09cf8074aa Allow users to select a compact tab bar. 2023-04-20 13:50:11 -04:00
Cody Henthorne
06f19aa6cd Bump version to 6.18.4 2023-04-20 13:49:24 -04:00
Cody Henthorne
7a4f522144 Updated baseline profile. 2023-04-20 13:48:22 -04:00
Cody Henthorne
a2985830c5 Updated language translations. 2023-04-20 13:29:59 -04:00
Alex Hart
358da730cd Add e164 in service address in SyncMessageProcessor. 2023-04-20 11:01:33 -03:00
Cody Henthorne
2c25a0494b Bump version to 6.18.3 2023-04-17 14:09:43 -04:00
Cody Henthorne
16b384925e Updated baseline profile. 2023-04-17 14:05:11 -04:00
Cody Henthorne
fd03cc6319 Updated language translations. 2023-04-17 13:59:34 -04:00
Alex Hart
a9fc5c6331 Remove Google Pay check at launch. 2023-04-17 14:24:13 -03:00
Cody Henthorne
dad9d0b708 Fix missing e164 in message processing. 2023-04-17 13:02:37 -04:00
Cody Henthorne
e2ade166ec Fix bugs with switching and render wired headset state in calls. 2023-04-17 12:20:48 -04:00
Greyson Parrelli
e33a68b203 Bump version to 6.18.2 2023-04-14 16:39:30 -04:00
Greyson Parrelli
ad52062195 Updated language translations. 2023-04-14 16:39:30 -04:00
Greyson Parrelli
236e0faace Retry FTS rebuild 3 times.
Looks like we still have the old connection pull after a backup restore.
This gives it 3 chances.

Fixes #12902
2023-04-14 16:39:28 -04:00
Alex Hart
7b4d2661ad Add state check to onDismiss to prevent crash. 2023-04-14 16:30:07 -04:00
Alex Hart
03b5170b97 Add logging around in-thread image sizing. 2023-04-14 16:30:07 -04:00
Alex Hart
ea6d1a9381 Fix case where device lied about pip mode support. 2023-04-14 16:30:07 -04:00
Greyson Parrelli
aff5c2aa16 Bump version to 6.18.1 2023-04-13 17:35:13 -04:00
Greyson Parrelli
829abf169a Updated language translations. 2023-04-13 17:34:44 -04:00
Greyson Parrelli
56e008ea4f Add database consistency test, fix calling migration. 2023-04-13 17:26:26 -04:00
Alex Hart
2a16d8baed Mark all call events as read whenever we enter the calls tab. 2023-04-13 17:26:26 -04:00
Alex Hart
ee89629738 Fix missed group call label. 2023-04-13 17:26:26 -04:00
Alex Hart
3e63ac46b4 Ensure message deletion marks event deleted. 2023-04-13 17:26:26 -04:00
Alex Hart
c881c67f5e Fix child filtering. 2023-04-13 17:26:26 -04:00
Alex Hart
a3574292c6 Fix Call Log snap and ordering. 2023-04-13 17:18:59 -04:00
Alex Hart
94b308cecb Add logging around next/previous moves for story viewer. 2023-04-13 17:18:59 -04:00
Alex Hart
d3e83b12d9 Utilize SERIAL instead of BOUNDED executor when marking stories as viewed. 2023-04-13 17:18:59 -04:00
Alex Hart
d77555266b Prevent deleting call events with DELETION_TIMESTAMP set to 0. 2023-04-13 17:18:59 -04:00
Alex Hart
3451ac4504 Move LifecycleDisposable to core-util. 2023-04-13 17:18:59 -04:00
Greyson Parrelli
b51973f1d5 Bump version to 6.18.0 2023-04-12 16:59:33 -04:00
Greyson Parrelli
f568002e5c Updated language translations. 2023-04-12 16:58:53 -04:00
Greyson Parrelli
321cced323 Ignore broken story unit tests. 2023-04-12 16:58:42 -04:00
Alex Hart
4359336fd5 Move load-state into its own data-store. 2023-04-12 16:31:35 -04:00
Alex Hart
e8570c3680 Add call tab event grouping. 2023-04-12 16:31:35 -04:00
Alex Hart
fd1ff5e438 Extract post mark as read request body. 2023-04-12 16:31:35 -04:00
Clark
026d029614 Fix tapping too fast breaking my stories viewer. 2023-04-12 16:31:35 -04:00
Clark Chen
ef058a1644 Inline export account data feature flag. 2023-04-12 16:31:27 -04:00
Cody Henthorne
a35a167e7a Fix spoiler animation running after view is returned to cache. 2023-04-12 16:31:27 -04:00
Cody Henthorne
36ef36be61 Cleanup MessageContentProcessorTestV2. 2023-04-12 16:31:27 -04:00
Bernie Dolan
a874efee4e Update payments to 4.1.0 2023-04-12 16:31:27 -04:00
Cody Henthorne
07c32e2a35 Fallback back to Telephony create thread in MMS export on failure. 2023-04-12 16:31:27 -04:00
Cody Henthorne
055f4b09ee Add additional backup folder failure debug info. 2023-04-12 16:31:27 -04:00
Cody Henthorne
737b1c962a Retry backup verify on security exception. 2023-04-12 16:31:27 -04:00
Cody Henthorne
99ac2cb333 Allow spoiler paint to be tinted independently per renderer. 2023-04-12 16:31:27 -04:00
Alex Hart
a183057b32 Update call state icons and text. 2023-04-12 16:31:27 -04:00
Alex Hart
2883c16560 Extract 'invite to signal' into an InviteActions object. 2023-04-12 16:31:27 -04:00
Greyson Parrelli
d88534e71f Improve spoiler drawing performance. 2023-04-12 16:31:27 -04:00
Greyson Parrelli
a56e9e502e Move LeakCanary into its own variant. 2023-04-12 16:31:27 -04:00
Alex Hart
433e8266c9 Add stricter call row identification. 2023-04-12 16:31:27 -04:00
Alex Hart
490feb358c Update ConversationOptionsMenuProvider to utilize snapshot data class. 2023-04-12 16:31:19 -04:00
Clark
27e3c883c3 Update notification on profile name fetch or change. 2023-04-12 16:31:19 -04:00
Greyson Parrelli
71e2b8225a Debounce thread updates for incoming messages. 2023-04-12 16:31:19 -04:00
Greyson Parrelli
6d4906dfa8 Add microbenchmarks for message decryption. 2023-04-12 16:31:19 -04:00
Aaron Labiaga
0156e74f5a Improve transition to PiP mode.
Use setAutoEnterEnable to true for smooth transition to
Picture-in-Picture when in gestural navigation mode.

Closes #12878
2023-04-12 16:29:48 -04:00
Clark
c834cb6ff7 Fix design assumption invalidated crash in MediaPreviewAdapter. 2023-04-11 10:34:18 -04:00
Clark
48360d08d4 Integrate contact hiding with message requests. 2023-04-11 10:34:18 -04:00
Greyson Parrelli
74877b839e Bump version to 6.17.3 2023-04-11 10:33:28 -04:00
Greyson Parrelli
39e04bef17 Updated language translations. 2023-04-11 10:32:42 -04:00
Greyson Parrelli
c5af204de3 Prevent FK violation from bad decryption insert.
Fixes #12880
2023-04-11 10:02:00 -04:00
Greyson Parrelli
ca0dd03042 Allow websocket retries when proxy is set. 2023-04-11 10:01:37 -04:00
Greyson Parrelli
f8f70ed3e1 Bump version to 6.17.2 2023-04-10 11:40:29 -04:00
Greyson Parrelli
0510588a09 Updated language translations. 2023-04-10 11:35:03 -04:00
Alex Hart
1d8fc4b7fd Fix mic tinting in small call button resource. 2023-04-10 12:01:13 -03:00
Greyson Parrelli
b7f5333b39 Reduce message observer max background time to 2 minutes.
Seeing some increased battery usage issues. 5 minutes was probably too
high.
2023-04-10 09:45:57 -04:00
Greyson Parrelli
544121d035 Minimize lock window in IncomingMessageObserver.
In particular, this was done to avoid a possible deadlock that could
occur between the IncomingMessageObserver lock and the JobManager lock.
2023-04-10 09:42:33 -04:00
Greyson Parrelli
4da4de3b99 Hopeful fix for bluetooth selection issues. 2023-04-07 09:08:41 -04:00
Alex Hart
0b62c0346b Bump version to 6.17.1 2023-04-06 17:12:11 -03:00
Alex Hart
292956b18c Updated baseline profile. 2023-04-06 17:12:00 -03:00
Alex Hart
925f347050 Updated language translations. 2023-04-06 17:09:41 -03:00
Greyson Parrelli
ea150939cb Fix issue where audio selections weren't persisting in UI. 2023-04-06 14:35:46 -04:00
Clark
57f490e5db Fix link previews not being treated as media message. 2023-04-06 13:48:11 -04:00
Alex Hart
a141fdaf7d Move state check to audio handler thread. 2023-04-06 14:16:55 -03:00
Clark
16f1fbf583 Only trigger image edit drag after user moves at least 3 pixels. 2023-04-06 11:52:06 -04:00
Alex Hart
9d4e13cd08 Wrap hidePicker dismiss call in ISE catch. 2023-04-06 10:46:11 -03:00
Alex Hart
73a7063867 Fix toolbar state management and pip. 2023-04-06 10:42:02 -03:00
Alex Hart
cd5c253a78 Clarify logging around group ring state. 2023-04-06 10:31:37 -03:00
Alex Hart
372c6f6ba3 Bump version to 6.17.0 2023-04-05 16:47:03 -03:00
Alex Hart
d6f4a89326 Updated baseline profile. 2023-04-05 16:46:56 -03:00
Alex Hart
60905c7409 Updated language translations. 2023-04-05 16:44:15 -03:00
Clark Chen
f3490d07bf Fix settings icons for older API levels. 2023-04-05 16:40:23 -03:00
Alex Hart
b99855afbe Fix avatar photo editing. 2023-04-05 16:40:23 -03:00
Alex Hart
921c903190 Allow forwarding of contacts. 2023-04-05 16:40:23 -03:00
Alex Hart
9771b53c79 Add logging to where we mark StoryContent as Ready. 2023-04-05 16:40:23 -03:00
Nicholas
0ab5bbb240 Detect and recover from SCO interruptions. 2023-04-05 16:40:23 -03:00
Alex Hart
fef533f101 Upgrade CameraX to 1.2.2 2023-04-05 16:40:23 -03:00
Alex Hart
3399af5a96 Fix mic toggle state. 2023-04-05 16:40:23 -03:00
Alex Hart
022195508a Swap AlertDialog.Builder for MaterialAlertDialogBuilder. 2023-04-05 16:40:23 -03:00
Alex Hart
d3daaff6a4 Update collapsed toolbar state for group calling. 2023-04-05 16:40:23 -03:00
Nicholas
89a3c62637 Only deauthorize on identified connection. 2023-04-05 16:40:23 -03:00
Alex Hart
6da9db6d86 Allow MediaSelectionActivity to ignore uiMode configuration changes. 2023-04-05 16:40:23 -03:00
Alex Hart
c254b08e33 Add the join / return button to call log items. 2023-04-05 16:40:23 -03:00
Alex Hart
9d575650d1 Add create call link sheet. 2023-04-05 16:40:23 -03:00
Greyson Parrelli
d8ac5a390a Write to MSL before sending a sync message. 2023-04-05 16:40:23 -03:00
Alex Hart
60842a10ff Align donate for a friend duration with data from gift badge. 2023-04-05 16:40:23 -03:00
Alex Hart
1cab6f87a0 Remove extra rows from PhoneNumberPrivacySettingsFragment. 2023-04-05 16:40:23 -03:00
Nicholas
a0aeac767d New Android 12+ audio route picker for calls. 2023-04-05 16:40:23 -03:00
Greyson Parrelli
99bd8e82ca Do not process messages while change number is happening. 2023-04-05 16:40:23 -03:00
Greyson Parrelli
bbdf54097e Prevent certain types of circular job dependencies. 2023-04-05 16:40:23 -03:00
Greyson Parrelli
2a9576baf5 Convert FastJobStorage to kotlin. 2023-04-05 16:40:23 -03:00
Clark Chen
2ca4c2d1c1 Fix gradle verification for Mac/Windows. 2023-04-05 16:40:23 -03:00
Greyson Parrelli
3231f8302c Do not add dependencies on previous message sends if you have no media.
This was an accidental carry-over from the PushMediaSendJob ->
IndividualSendJob rename.

Previously, PushMediaSendJob basically always had media, so this check
wasn't needed. Now it rarely has media, so we have to add it.
2023-04-05 16:40:23 -03:00
Greyson Parrelli
e0be9b4ef5 Fix resend operation for sync messages.
We shouldn't be using sealed sender for any sync messages.
2023-04-05 16:40:23 -03:00
Clark
83b0963533 Add fix for m4a mapping to audio/mpeg. 2023-04-05 16:40:23 -03:00
Alex Hart
f9548dcffe Add support for group call disposition.
Co-authored-by: Cody Henthorne <cody@signal.org>
2023-04-05 16:40:23 -03:00
Greyson Parrelli
e94a84d4ec Fixed MessageProcessingPerformanceTest. 2023-04-05 16:40:23 -03:00
Greyson Parrelli
db5f8707ec Remove TracingExecutors. 2023-04-05 16:40:23 -03:00
Greyson Parrelli
0f15562a28 Rename ComposeFragment.SheetContent -> FragmentContent. 2023-04-05 16:40:23 -03:00
Greyson Parrelli
b300f223ba Update AGP to 7.4.2 2023-04-05 16:40:23 -03:00
Clark
ad9337021c Streamline export account data to not save to disk. 2023-04-04 12:16:45 -03:00
Greyson Parrelli
5e94c350ed Add dependency for kotlinx immutable collections. 2023-04-04 12:16:45 -03:00
Clark
666020c3dc Add learn more link to export account data. 2023-04-04 12:16:45 -03:00
Alex Hart
f249a6edd5 Add StatusBarColorNestedScrollConnection. 2023-04-04 12:16:45 -03:00
Cody Henthorne
2e45bd719a Add kotlin/proto level message processing. 2023-04-04 12:16:45 -03:00
Clark
28f27915c5 Add support for time stickers in image editor. 2023-04-04 12:16:45 -03:00
Alex Hart
08ebca501b Bump version to 6.16.2 2023-04-04 11:07:18 -03:00
Alex Hart
417cda1d38 Updated baseline profile. 2023-04-04 11:06:48 -03:00
Alex Hart
dd730f5fbf Updated language translations. 2023-04-04 11:01:17 -03:00
Nicholas Tinsley
77bb3702a9 Add more detail to 502 errors during registration. 2023-04-03 14:43:26 -04:00
Nicholas Tinsley
5046f58c6f Constrain end of code entry subheader. 2023-04-03 14:32:17 -04:00
Greyson Parrelli
d02f605874 De-pluralize some strings. 2023-03-31 15:01:11 -04:00
Nicholas
36a8c4d8ba Bump version to 6.16.1 2023-03-30 18:33:53 -04:00
Nicholas
25f0427585 Updated baseline profile. 2023-03-30 18:33:00 -04:00
Nicholas
5a501f4815 Updated language translations. 2023-03-30 18:27:30 -04:00
Greyson Parrelli
de0a37d356 Fix possible dangling thread records. 2023-03-30 17:17:10 -04:00
Clark
5c65d5435c Fix rtl hiding conversation title. 2023-03-30 16:29:08 -04:00
Greyson Parrelli
8d6a4c2888 Fix SKDM processing. 2023-03-30 15:48:53 -04:00
Nicholas
b4a7ffdc12 Bump version to 6.16.0 2023-03-29 14:18:55 -04:00
Nicholas
5dd10f6fcc Updated baseline profile. 2023-03-29 14:16:21 -04:00
Nicholas
e76b5007e0 Updated language translations. 2023-03-29 14:13:51 -04:00
Nicholas Tinsley
16e8f9633e fixup! Add benchmark for conversation open. 2023-03-29 12:44:27 -04:00
Clark Chen
cb4a45fea3 Fix empty name crash when fetching first alpha recipient row. 2023-03-29 10:55:00 -04:00
Cody Henthorne
0017b7af26 Ignore contact joined message when determining if we should apply universal disappearing messages. 2023-03-28 16:06:23 -04:00
Greyson Parrelli
5f645193e4 Create Buttons.ActionButton component. 2023-03-28 16:02:45 -04:00
Cody Henthorne
607a06d379 Enable scheduled backups regardless of API version. 2023-03-28 09:24:11 -04:00
Cody Henthorne
149955e07a Fix crash when searching and lowercase snippet differs from input snippet. 2023-03-27 21:16:36 -04:00
Greyson Parrelli
80b9e4e7ae Updated username education icons. 2023-03-27 13:05:38 -04:00
Greyson Parrelli
f02ac86e45 Updated strings for username education fragment. 2023-03-27 11:07:43 -04:00
elena
45e96f0efe Show character count when writing a payment note.
Close #12616
2023-03-27 09:47:30 -04:00
Cody Henthorne
06894d6a7e Fix formatting on long text messages. 2023-03-24 17:15:02 -04:00
Greyson Parrelli
b67dfe10d4 Add some accessibility labels for the camera screen. 2023-03-24 16:44:05 -04:00
Greyson Parrelli
b9b6a57e2c Fix possible username conflict in storage update. 2023-03-24 16:32:38 -04:00
Cody Henthorne
ba2d005b2a Fix search result styling for formatting and query highlighting. 2023-03-24 15:49:27 -04:00
Cody Henthorne
f53679f24a Fix spoiler rendering in quotes. 2023-03-24 15:49:27 -04:00
Cody Henthorne
7eb00e41a2 Fix rendering of links and mentions covered by spoilers. 2023-03-24 15:49:26 -04:00
Jim Gustafson
168e37c3fc Update to RingRTC v2.26.1 2023-03-24 15:49:26 -04:00
Clark
98438ff8e4 Update async layout inflater to fix AppCompat views. 2023-03-24 15:49:26 -04:00
Clark
d6a9ed1a8d Add setting for requesting user account data. 2023-03-24 15:49:26 -04:00
Jim Gustafson
b194c0e84b Update to RingRTC v2.26.0 2023-03-24 15:49:26 -04:00
Nicholas
ed67e7ac04 Ignore smartwatch as possible headset. 2023-03-24 15:49:26 -04:00
Cody Henthorne
43cd647036 Add spoiler format style. 2023-03-24 15:49:26 -04:00
Greyson Parrelli
5d6889786c Bump version to 6.15.3 2023-03-24 14:39:52 -04:00
Greyson Parrelli
53d4e5c4d1 Updated language translations. 2023-03-24 14:39:52 -04:00
Greyson Parrelli
87918da943 Shorten lifespan of buffered store. 2023-03-24 14:39:51 -04:00
Greyson Parrelli
5914a4d1cf Improved logging around backup export. 2023-03-24 14:39:51 -04:00
Alex Hart
351baa4135 Update content descriptions for call toggles. 2023-03-24 14:39:51 -04:00
Alex Hart
1a71e1a5ae Fix cases where first letter is not an integer or character. 2023-03-24 14:39:51 -04:00
Alex Hart
3ce68a7df8 Allow toggle to be manually slid on preference screens. 2023-03-24 14:39:51 -04:00
Alex Hart
e83c2f1e05 Fix bottom row overlap in group creation / add activities. 2023-03-24 09:47:54 -03:00
Alex Hart
684e53402e Close keyboard after successful profile save. 2023-03-24 09:40:59 -03:00
Greyson Parrelli
db1853f775 Fix timestamp logs on call messages. 2023-03-23 18:58:49 -04:00
Greyson Parrelli
aad835323b Bump version to 6.15.2 2023-03-23 18:22:09 -04:00
Greyson Parrelli
d6f6633c73 Updated language translations. 2023-03-23 18:22:09 -04:00
Greyson Parrelli
76984ab042 Improve logging around message sending.
There were some message types where we didn't log the timestamp.
The timestamp is important for debugging issues (let's us locate an
error more precisely in the logs).
2023-03-23 18:08:39 -04:00
Greyson Parrelli
d58c4ef439 Unify locks in protocol stores. 2023-03-23 15:37:45 -04:00
Alex Hart
2763cfe6f4 Reintroduce labels for incoming call screen. 2023-03-23 16:10:25 -03:00
Greyson Parrelli
454e9a99fc Fix possible NPE in ConversationListFragment. 2023-03-23 14:41:40 -04:00
Alex Hart
aeb250cae1 Fix ISK minimum currency precision. 2023-03-23 14:41:25 -03:00
Greyson Parrelli
34367b4e70 Bump version to 6.15.1 2023-03-23 13:34:32 -04:00
Greyson Parrelli
451537d320 Updated language translations. 2023-03-23 13:34:32 -04:00
Greyson Parrelli
53d4825e12 Fully rebuild FTS after a backup restore. 2023-03-23 13:34:32 -04:00
Greyson Parrelli
24ee4a869f Fix empty state incorrectly showing in search. 2023-03-23 13:32:51 -04:00
Alex Hart
6ae3fb49e0 Add transitions to call state pill. 2023-03-23 13:32:51 -04:00
Alex Hart
8f9713a2c0 Add new call toast and remove call button labels. 2023-03-23 13:32:51 -04:00
Alex Hart
7a2ad37333 Add proper tinting to new chat icons. 2023-03-23 13:32:51 -04:00
Alex Hart
2509d1be73 Fix text alignment and padding on onboarding cards. 2023-03-23 13:32:51 -04:00
Alex Hart
19f4073068 Fix listener behavior for manual directory refresh invocation. 2023-03-23 13:32:51 -04:00
Alex Hart
fd612525a1 Fix touch interactions with MaterialSwitch in preferences. 2023-03-23 13:32:39 -04:00
Greyson Parrelli
631b428a84 Fix log statement.
We were printing out the envelope deviceId, which isn't populated with
sealed sender and was always 0. Should have used the one from the
decryption result.
2023-03-23 13:26:06 -04:00
Greyson Parrelli
09cd581cf4 Bump version to 6.15.0 2023-03-22 14:30:28 -04:00
Greyson Parrelli
fc1ea458f7 Updated language translations. 2023-03-22 14:28:36 -04:00
Clark
247edce7b0 Catch exceptions in share repository for blob provider IO exceptions. 2023-03-22 14:28:36 -04:00
Alex Hart
57a2a32c71 Update call button iconography and colours. 2023-03-22 14:28:36 -04:00
Clark
d9c1ecab9b Fix precaching of conversation list items. 2023-03-22 14:28:36 -04:00
Alex Hart
c70f1f5d75 Fix call screen transition. 2023-03-22 14:28:36 -04:00
Alex Hart
c26cc56f20 Fix bottom bar state handling and active state when menu is open. 2023-03-22 14:28:36 -04:00
Nicholas Tinsley
ca21ab667a Force LTR layout direction on NumericKeyboardView. 2023-03-22 14:28:36 -04:00
Clark
e2ae0063a5 Fix send button disappearing for voice drafts. 2023-03-22 14:28:36 -04:00
Alex Hart
eb150d9a15 Update SwitchMaterial to the new MaterialSwitch. 2023-03-22 14:28:36 -04:00
Cody Henthorne
ee48e6c347 Add sync message handling and stop formatting behavior. 2023-03-22 14:28:36 -04:00
Nicholas
cedf512726 Fix PanicKit for PIN lock.
Fixes #12816.
2023-03-22 14:28:10 -04:00
Clark
2256c8591a Add special audio recording sample rate for Xiaomi Mi 9T. 2023-03-22 14:28:10 -04:00
Alex Hart
1056adb591 Move distribution type operation into ConversationViewModel. 2023-03-22 14:28:10 -04:00
Alex Hart
53716019b6 Remove QuoteRestorationTask in favour of using DraftViewModel to resolve it. 2023-03-22 14:28:10 -04:00
Alex Hart
30f6faf3d7 Move mute handling into ConversationViewModel. 2023-03-22 14:28:10 -04:00
Alex Hart
2a43ffad4f Extract ConversationParentFragment Options Menu into a MenuProvider. 2023-03-22 14:28:10 -04:00
Alex Hart
f9ed5c4d03 Correct some icon tinting. 2023-03-22 14:28:10 -04:00
Cody Henthorne
25028e0e6f Add additional text formatting support. 2023-03-22 14:28:10 -04:00
Alex Hart
1c3636eedd Add undo-ability to call tab deletion. 2023-03-22 14:28:10 -04:00
Alex Hart
4d735d23b6 Remove unnecessary method calls in options menu code. 2023-03-22 14:28:10 -04:00
Greyson Parrelli
834d0a1cee Trigger an automatic session reset after failing to send a retry receipt. 2023-03-22 14:28:09 -04:00
Alex Hart
166e555d32 Kill two unused classes. 2023-03-22 14:28:09 -04:00
Greyson Parrelli
7f963d7628 Keep protocol error logs longer. 2023-03-22 14:28:09 -04:00
Alex Hart
cebe600014 Update bottom bar to support just calls and chats. 2023-03-22 14:28:09 -04:00
Alex Hart
5c688289a5 Ensure we do not stage shared element transition view when opening media from a bubble. 2023-03-22 14:28:09 -04:00
Greyson Parrelli
bf611f3a56 Fix potential NPE in SNC dialog. 2023-03-22 14:28:09 -04:00
Clark
150c42c590 Add notification for failed story messages. 2023-03-22 14:28:09 -04:00
Clark
069b707d9d Add dark mode for location picker. 2023-03-22 14:28:09 -04:00
Alex Hart
8c0d979abd Add call tab bottom bar. 2023-03-22 14:28:09 -04:00
Alex Hart
545f1fa5a4 Add call tab info screen. 2023-03-22 14:28:09 -04:00
Greyson Parrelli
49a814abef Show blocked users as 'skipped' when sending to curated story list. 2023-03-22 14:28:09 -04:00
Clark
17fc0dc0a1 Add indicator and story ring for stories in chat selection. 2023-03-22 14:28:09 -04:00
Greyson Parrelli
7c8de901f1 Store Job data as bytes. 2023-03-22 14:28:09 -04:00
Alex Hart
b5af581205 Set proper filter labeling on call tab. 2023-03-22 14:28:09 -04:00
Alex Hart
de73744432 Add new symbols for call tab. 2023-03-22 14:28:09 -04:00
Alex Hart
ce3770a0fb Add new call screen for calls tab. 2023-03-22 14:28:09 -04:00
Greyson Parrelli
1210b2af0f Some additional decryption perf improvements. 2023-03-22 14:28:09 -04:00
Greyson Parrelli
c6861f1778 Add support for the ManifestRecord.sourceDevice field. 2023-03-22 14:28:09 -04:00
Clark
906dd5cb40 Drop link preview thumbnail from forward if URI isn't present. 2023-03-22 14:27:59 -04:00
Clark
97b349b0de Add benchmark for conversation open. 2023-03-20 17:39:09 -04:00
Clark
f3b830ae20 Fix dark mode for compose bottom sheets. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
7d7e6e5013 Update SQLCipher to 4.5.3-FTS-S3 2023-03-20 17:39:09 -04:00
Alex Hart
8ca596580c Add info action wiring in calls tab. 2023-03-20 17:39:09 -04:00
Alex Hart
7521520b26 Ensure scrolling properly highlights action bar in calls tab. 2023-03-20 17:39:09 -04:00
Alex Hart
18554170f2 Update call tab to display unread missed call count. 2023-03-20 17:39:09 -04:00
Alex Hart
cd5a3768eb Fix back handling between tabs. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
cf64f06c36 Add a new test case for recipient merging. 2023-03-20 17:39:09 -04:00
Alex Hart
88de0f21e7 Add initial implementation of calls tab behind a feature flag. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
d1373d2767 Remove queue drained constraint from receipt jobs. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
baece9823b Remove log when enqueuing job within a transaction.
Found the bug I put the logging in for, and now this log happens way to
much after the decryption batching.
2023-03-20 17:39:09 -04:00
Greyson Parrelli
e18b2d263c Fix rendering of story replies in quote thread view. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
d12830cb66 Add language support for Uyghur. 2023-03-20 17:39:09 -04:00
Cody Henthorne
59141bc6a4 Improve delete thread performance. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
431e366e76 Add in possible recovery for DB error handler.
A bad FTS index can result in the corruption handler being triggered.
We can attempt to rebuild it to see if that helps.
2023-03-20 17:39:09 -04:00
Nicholas
66cb2a04c3 Rename properties of AccountAttributes. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
90cc672c37 Convert MessageTable to kotlin. 2023-03-20 17:39:09 -04:00
Clark
c2a76c4313 Convert ConversationTitleView to a ConstraintLayout. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
ee685936c5 Updated MessageProcessingPerformanceTest to use websocket injection. 2023-03-20 17:39:09 -04:00
Alex Hart
a7bca89889 Perform username deletion if no local name is set. 2023-03-20 17:39:09 -04:00
Clark
39f5aebbec Add support for scheduling media to multiple contacts. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
35571e7ab2 Added another RecipientTable.getAndPossiblyMerge test case. 2023-03-20 17:39:09 -04:00
Clark
ed2d6ea903 Only setup mock data once for baseline profiles and benchmarks. 2023-03-20 17:39:09 -04:00
Alex Hart
e1e117ce73 Increase logging around username synchronization. 2023-03-20 17:39:09 -04:00
Greyson Parrelli
894095414a Perform message decryptions in batches. 2023-03-20 17:39:09 -04:00
Clark
04baa7925f Add support for baseline profiles. 2023-03-20 17:39:08 -04:00
Clark
79a062c838 Introduce thread priorities for threads and handlerthreads. 2023-03-20 17:39:08 -04:00
Greyson Parrelli
2cef06cd6e Bump version to 6.14.5 2023-03-20 17:37:51 -04:00
Greyson Parrelli
af4b98f424 Updated language translations. 2023-03-20 17:37:51 -04:00
Greyson Parrelli
cd66ba60e3 Disable view precaching of chat list to fix selection checkmark bug. 2023-03-20 17:37:49 -04:00
Greyson Parrelli
2d2a1049a4 Make onboarding card close button background borderless. 2023-03-20 17:37:24 -04:00
Greyson Parrelli
03aa6a1d61 Fix potential crash when starting IncomingMessageObserver service. 2023-03-20 17:37:24 -04:00
Greyson Parrelli
6c6d4e801f Fix crash when starting multiple audio records. 2023-03-20 17:37:24 -04:00
Cody Henthorne
a6d7b0c7bf Fix crash in multishare flow. 2023-03-20 17:37:24 -04:00
Cody Henthorne
f3c6f2e3c5 Bump version to 6.14.4 2023-03-15 19:55:20 -04:00
Cody Henthorne
4dc5ada717 Updated language translations. 2023-03-15 19:37:09 -04:00
Nicholas Tinsley
c01d542ec2 Better handling of push timeouts during registration. 2023-03-15 17:34:40 -04:00
Cody Henthorne
af7987d743 Bump version to 6.14.3 2023-03-14 16:42:02 -04:00
Cody Henthorne
37a7516b7e Updated language translations. 2023-03-14 15:08:53 -04:00
Cody Henthorne
dae0559568 Do not crash when backup process encounters an unexpected security exception. 2023-03-14 11:13:17 -04:00
Cody Henthorne
904817b498 Fix payment reaction notification. 2023-03-13 10:08:39 -04:00
Nicholas
9087f427a5 If push challenge times out, don't try again. 2023-03-13 09:50:54 -04:00
Cody Henthorne
f24d82bf04 Attempt to fix scheduled backups again. 2023-03-13 09:19:54 -04:00
Alex Hart
10e55765c1 Bump version to 6.14.2 2023-03-10 15:21:32 -04:00
Alex Hart
f205fece67 Updated language translations. 2023-03-10 15:17:36 -04:00
Nicholas Tinsley
6fb3167157 Don't reset session on return from captcha. 2023-03-10 13:49:04 -05:00
Nicholas
f22daccde6 Support pasting in verification code view. 2023-03-10 13:00:34 -05:00
Alex Hart
643d96a896 Bump version to 6.14.1 2023-03-08 15:53:23 -04:00
Alex Hart
04664d34e4 Updated language translations. 2023-03-08 15:51:32 -04:00
Alex Hart
7fd5b72204 Allow nullability of Intent parameter in converted services. 2023-03-08 15:44:47 -04:00
Alex Hart
fd3a509231 Bump version to 6.14.0 2023-03-08 15:16:41 -04:00
Alex Hart
9b672a520a Updated language translations. 2023-03-08 15:12:21 -04:00
Alex Hart
2d3e8ef31c Replace CardView usages with MaterialCardView. 2023-03-08 15:06:50 -04:00
Cody Henthorne
f1c2ee9b32 Stabilize message processing tests and add inline decryption timings. 2023-03-08 15:06:50 -04:00
Alex Hart
68a0cb40a6 Update several AndroidX libraries.
activity -> 1.6.1
appcompat -> 1.6.1
fragment -> 1.5.5
navigation -> 2.5.3
core-ktx -> 1.9.0
safe-args -> 2.5.3
2023-03-08 15:06:50 -04:00
Alex Hart
68a50798f2 Update phone number listing rules. 2023-03-08 15:06:50 -04:00
Alex Hart
73151e8ff6 Upgrade core-ktx to 1.8.0 2023-03-08 15:06:50 -04:00
Alex Hart
b7da4b93db Upgrade CameraX to 1.2.1 2023-03-08 15:06:50 -04:00
Nicholas
bd373a3045 Improve registration network reliability. 2023-03-08 15:06:50 -04:00
Alex Hart
7c94d570cb Update copy of GiftMessageView. 2023-03-08 15:06:50 -04:00
Greyson Parrelli
8d8f5fb9e4 Update icons for nightly and nightlyStaging. 2023-03-08 15:06:50 -04:00
Greyson Parrelli
1b2cb2637f Perform decryptions inline. 2023-03-08 15:06:50 -04:00
Alex Hart
e222f96310 Add username sync job to be run after new registrations. 2023-03-08 15:06:50 -04:00
Nicholas
877a62b809 Convert VersionTracker to Kotlin and add RefreshAttributesJob. 2023-03-08 15:06:50 -04:00
Greyson Parrelli
81fc99724d Move the Job#onSubmit call to be outside of the JobController lock. 2023-03-06 10:48:18 -05:00
Cody Henthorne
6e8f3d1e71 Fix scheduled message changing disappearing messages bug. 2023-03-06 09:47:12 -05:00
Alex Hart
33ab25a557 Add global formatter to gradle build files. 2023-03-06 10:46:51 -04:00
Cody Henthorne
c30e3664b8 Improve performance of message processing.
Rearranging code allows us to skip expensive calls or duplicating work
already spent to resolve a recipient.
2023-03-04 10:52:21 -05:00
Nicholas
bb8c7bab20 Finish registration activity upon phone number entry cancellation. 2023-03-04 10:52:21 -05:00
Nicholas Tinsley
194681abb7 Change reply icon color. 2023-03-04 10:52:21 -05:00
Alex Hart
c56564014b Add ktlint checking to :build-logic:plugins and split buildQa out into its own task for readability. 2023-03-04 10:52:21 -05:00
Cody Henthorne
c0aff46e31 Add message processing performance test. 2023-03-04 10:52:21 -05:00
Jim Gustafson
f719dcca6d Update to RingRTC v2.25.1 2023-03-04 10:52:21 -05:00
Alex Hart
abd1582422 Fix kdoc in MediaPreviewRepository. 2023-03-04 10:52:21 -05:00
Greyson Parrelli
ec2565263e Initial refactor of the message decryption flow. 2023-03-04 10:52:21 -05:00
Alex Hart
c1a94be9cd Set media preview background to correct color. 2023-03-04 10:52:21 -05:00
Cody Henthorne
e303e80f17 Fix instrumentation tests and group member regression. 2023-03-04 10:52:21 -05:00
Alex Hart
018f6ac7aa Update compose BOM to 2023.01.00 2023-03-04 10:52:21 -05:00
Alex Hart
6f9d3f02f1 Privatize Scaffold preview. 2023-03-04 10:52:21 -05:00
Alex Hart
9b2ccd43c8 Make radio-row preview interactive. 2023-03-04 10:52:21 -05:00
Alex Hart
bd078274b5 Fix RadioRow vertical alignment. 2023-03-04 10:52:21 -05:00
Ehren Kret
9cfb95fee7 Update Github CI to build with more cores and limit metaspace size. 2023-03-04 10:52:03 -05:00
Alex Hart
0f18fa329d Fix crash when inserting group member remaps. 2023-03-04 10:52:03 -05:00
Nicholas
428ef554a3 Add bottom sheet reminder for linked devices on re-register. 2023-03-04 10:52:03 -05:00
Alex Hart
8ca8e5d8f9 Add scaffold preview. 2023-03-04 10:52:03 -05:00
bijaykumarpun
5634e9834d Fix incorrect underlines rendering for empty lines in image editor.
Fixes #12807
Closes #12808
2023-03-04 10:52:03 -05:00
David
b437cb0344 Fix parameter order in getAccessMapFor.
Closes #12812
2023-03-04 10:52:03 -05:00
Alex Hart
3695d7a5f1 Fix marquee scroll behavior in VoiceNotePlayerView. 2023-03-04 10:52:03 -05:00
Alex Hart
7010b19fea Remove username creation state from passphrase required activity. 2023-03-04 10:52:03 -05:00
Alex Hart
3f62221182 Add unit test for build-logic static ip tool. 2023-03-04 10:52:03 -05:00
Alex Hart
43aad90ee4 Fix code formatting. 2023-03-04 10:51:41 -05:00
Alex Hart
aa28668315 Cannot add build-logic tasks to QA. 2023-03-04 10:51:41 -05:00
Alex Hart
9ea392fb4e Utilize non-default arg. 2023-03-04 10:51:41 -05:00
Alex Hart
d0c858221e Add a couple unit tests for StaticIpResolver and string into qa. 2023-03-04 10:51:41 -05:00
Alex Hart
a95e695a97 Start StaticIpResolver testing. 2023-03-04 10:51:41 -05:00
Nicholas Tinsley
8910eac6e0 Fix crash in RegistrationCompleteFragment. 2023-03-04 10:51:41 -05:00
Cody Henthorne
e635c3030e Fix scheduled backup jobs cancelling itself. 2023-03-04 10:51:41 -05:00
Nicholas Tinsley
8cbad2c3a6 Update SMS export string. 2023-03-04 10:51:41 -05:00
Alex Hart
45a04423b0 Add PNP settings. 2023-03-04 10:51:41 -05:00
Clark
f3693c966a Improve conversation list cold start performance. 2023-03-04 10:51:41 -05:00
Cody Henthorne
10e8c6d795 Skip attachments with unrecoverable errors during sms export. 2023-03-04 10:51:41 -05:00
Greyson Parrelli
57e8684bb3 Always use a foreground service when processing high-priority FCM pushes. 2023-03-04 10:51:41 -05:00
Greyson Parrelli
f91c400f6c Convert build-logic build.gradle to kotlin. 2023-03-04 10:51:41 -05:00
Ehren Kret
40f86ed2be Enable gradle caching on the Github CI system. 2023-03-04 10:51:20 -05:00
Alex Hart
ca8add87c6 Update design for who can find me fragment. 2023-03-03 10:40:55 -05:00
Alex Hart
a9c4fcf894 Refresh onboarding cards. 2023-03-03 10:40:55 -05:00
Nicholas
6bc5b19b1e Convert RegistrationCompleteFragment to Kotlin. 2023-03-03 10:40:55 -05:00
Nicholas
4990243a91 Ask for profile name on re-register if none present for number. 2023-03-03 10:40:55 -05:00
Greyson Parrelli
6922886395 Fix a bunch of random lint warnings. 2023-03-03 10:40:55 -05:00
Greyson Parrelli
ce4b7c2d7f Convert StaticIpResolver to kotlin. 2023-03-03 10:40:55 -05:00
Greyson Parrelli
21deb6803c Delete the buildSrc directory.
Moved all of the stuff we were using it for into build-logic.
2023-03-03 10:40:55 -05:00
Greyson Parrelli
873552436a Remove some unused RecipientTable code. 2023-03-03 10:40:55 -05:00
Greyson Parrelli
8334db5273 Validate E164's in ContactRecords. 2023-03-03 10:40:55 -05:00
Greyson Parrelli
33828439fb Use the websocket for FCM fetches. 2023-03-03 10:40:55 -05:00
Clark
4f31dc36ba Improve cold start by postponing voice note service creation. 2023-03-03 10:40:55 -05:00
Cody Henthorne
0a971569d9 Bump version to 6.13.6 2023-03-03 10:28:27 -05:00
Cody Henthorne
06476c80f8 Updated language translations. 2023-03-03 10:23:22 -05:00
Nicholas
d1d73fef30 Support multiple sequential captcha challenges. 2023-03-03 09:51:27 -05:00
Nicholas
89ad213994 Bump version to 6.13.5 2023-03-01 10:47:05 -05:00
Nicholas
6969c6d6ee Updated language translations. 2023-03-01 10:45:56 -05:00
Cody Henthorne
b82f6f83ec Fix network on main thread crash. 2023-03-01 10:17:12 -05:00
Nicholas
d3572f92f5 Bump version to 6.13.4 2023-02-28 15:44:20 -05:00
Nicholas
41126ba913 Updated language translations. 2023-02-28 15:40:43 -05:00
Nicholas Tinsley
0a00413228 Clear unauthorized banner on registration. 2023-02-28 11:42:28 -05:00
Nicholas Tinsley
e810eeec58 Add debugging logs for PIN re-register flow. 2023-02-28 11:24:06 -05:00
Nicholas
5f0035b2d0 Sort country codes as strings rather than ints. 2023-02-28 09:07:20 -05:00
Cody Henthorne
a20d5fd6cf Fix crash when trying to cancel alarm without permission. 2023-02-27 16:57:51 -05:00
Cody Henthorne
191b2076c3 Bump version to 6.13.3 2023-02-24 19:14:41 -05:00
Cody Henthorne
35779e8df3 Updated language translations. 2023-02-24 19:03:50 -05:00
Cody Henthorne
06bec76371 Use recovery flow for change number when possible. 2023-02-24 16:22:43 -05:00
Cody Henthorne
ff76c4cdef Fix keyboard not always auto-showing in registration screens. 2023-02-24 15:59:58 -05:00
Cody Henthorne
42da07b763 Fix incorrect fcm status when reregistering with recovery. 2023-02-24 15:51:30 -05:00
Cody Henthorne
3e69ef8acc Attempt to auto-resolve after being locked out if local data is available. 2023-02-24 11:24:34 -05:00
Nicholas Tinsley
ab48aa5766 Do not force +1 country code when restoring registration state. 2023-02-24 11:05:45 -05:00
Cody Henthorne
e37d3be73a Bump version to 6.13.2 2023-02-23 20:20:50 -05:00
Cody Henthorne
16c2609dab Updated language translations. 2023-02-23 20:01:05 -05:00
Nicholas
e4d4a5d9e0 Adapt change number flow to use V2 API. 2023-02-23 19:56:32 -05:00
Greyson Parrelli
a552a5a5bc Updated in-app language selector to always use native language name. 2023-02-23 19:11:46 -05:00
Greyson Parrelli
13d48b880b Add language support for Cantonese. 2023-02-23 18:58:39 -05:00
Cody Henthorne
6cb8c7a8a9 Ensure attributes are updated with latest properties. 2023-02-23 16:51:25 -05:00
Chris Eager
ae3ff21689 Update spam reporting token JSON field name. 2023-02-23 16:27:14 -05:00
Cody Henthorne
d3c3986100 Bump version to 6.13.1 2023-02-23 12:38:57 -05:00
Cody Henthorne
6f3c095a95 Updated language translations. 2023-02-23 12:30:21 -05:00
Cody Henthorne
6ee04f6574 Fix crash after entering incorrect pin for registration lock. 2023-02-23 12:24:39 -05:00
Nicholas
f3922c4156 Fix bottom sheet behavior and design. 2023-02-23 12:24:39 -05:00
Cody Henthorne
2ffc576387 Fix verification file for windows builders. 2023-02-23 12:24:39 -05:00
Cody Henthorne
583f7db554 Clear old group rings on startup. 2023-02-23 12:24:39 -05:00
Cody Henthorne
1cffd88af2 Fix crashes during skip SMS flow. 2023-02-23 12:24:39 -05:00
Cody Henthorne
01351125f1 Fix reporting token data bug. 2023-02-23 08:08:21 -05:00
Greyson Parrelli
19d67d1111 Bump version to 6.13.0 2023-02-22 22:35:00 -05:00
Greyson Parrelli
8cec6a8b0c Updated language translations. 2023-02-22 22:34:30 -05:00
Greyson Parrelli
e8b3d2c7aa Improve logging around message processing. 2023-02-22 22:26:14 -05:00
Cody Henthorne
62414e72b5 Add support for pin entry sad paths. 2023-02-22 22:26:14 -05:00
Nicholas
afb9b76208 Bug fixes for the new registration flow. 2023-02-22 22:26:14 -05:00
Cody Henthorne
4f458a022f Add skip SMS flow. 2023-02-22 22:26:14 -05:00
Nicholas
a47e3900c1 Implement session-based account registration API. 2023-02-22 22:11:58 -05:00
Jim Gustafson
3de17fa2d0 Update to RingRTC v2.25.0 2023-02-22 19:09:55 -05:00
Clark
7abf358ac4 Pre-cache conversation_list_item_view to speed up cold start. 2023-02-22 16:50:08 -05:00
Greyson Parrelli
64d5cbce3d Fix bug where you could choose to add someone already in a group. 2023-02-22 16:08:39 -05:00
Greyson Parrelli
691ab353da Fix possible dlist sync crash, improved debugging.
Fixes #12795
2023-02-22 14:18:41 -05:00
Greyson Parrelli
b689ea62a6 Fix using system emoji in condensed message mode. 2023-02-22 13:14:53 -05:00
Greyson Parrelli
17aa0365d6 Handle keepMutedChatsArchived more generically, also apply it to sends.
Fixes #12788
2023-02-22 13:14:44 -05:00
Greyson Parrelli
3f93d4b9fc Change ReportSpamJob lifespan to 1 day. 2023-02-22 10:59:09 -05:00
Greyson Parrelli
263fb9fc04 Use null when submitting empty reporting tokens.
Cleaned up a few things too, just spacing and stuff.
2023-02-22 10:59:04 -05:00
Greyson Parrelli
3ebafca297 Validate that reporting token is non-null and non-empty. 2023-02-22 10:33:56 -05:00
Greyson Parrelli
316df00287 Save username when applying update to AccountRecord. 2023-02-22 09:23:49 -05:00
Greyson Parrelli
b92346d4ae Make our FABs rounded rects again. 2023-02-21 14:42:04 -05:00
Greyson Parrelli
7bdb5fd76c Update material library to 1.8.0
Fixes #12792
2023-02-21 11:32:24 -05:00
Greyson Parrelli
dad9980a80 Add Observable for LiveRecipient. 2023-02-21 11:32:24 -05:00
Greyson Parrelli
21df032b04 Mark Recipient.self() as needing sync after change PNP setting. 2023-02-21 11:32:24 -05:00
Alex Hart
7edebe9fa1 Clean up a couple warnings in MediaPreviewV2Fragment. 2023-02-21 11:32:24 -05:00
Alex Hart
a398745740 Implement username is out of sync banner. 2023-02-21 11:32:24 -05:00
Greyson Parrelli
4954be109c Bump version to 6.12.5 2023-02-21 09:59:42 -05:00
Greyson Parrelli
7380d4b11e Updated language translations. 2023-02-21 09:59:19 -05:00
Greyson Parrelli
d8a6f9c324 Fix possible crash when finishing animation. 2023-02-21 09:45:55 -05:00
Greyson Parrelli
ab9057cb25 Fix possible crash during backup restore. 2023-02-21 09:45:44 -05:00
Greyson Parrelli
ba8ea3b54b Bump version to 6.12.4 2023-02-17 15:33:51 -05:00
Greyson Parrelli
d9aca34eee Updated language translations. 2023-02-17 15:33:09 -05:00
Greyson Parrelli
05d232beec Fix possible crash in conversation fragment startup. 2023-02-17 15:25:45 -05:00
Greyson Parrelli
fde0726500 Fix bug where you couldn't add new raw phone numbers to groups. 2023-02-17 15:17:18 -05:00
Greyson Parrelli
bb8b987833 Ignore lastProfileFetchTime when determining Recipient equality.
Was resulting in some unnecessary Recipient re-renders during
conversation open.
2023-02-17 10:12:42 -05:00
Alex Hart
f066fb8ea2 Tweak media transition fade. 2023-02-16 17:26:53 -04:00
Greyson Parrelli
7738c286c2 Bump version to 6.12.3 2023-02-16 16:08:47 -05:00
Greyson Parrelli
697670b334 Updated language translations. 2023-02-16 16:08:47 -05:00
Greyson Parrelli
4cfba86cb1 Fix bug where username wasn't synced to ContactRecord. 2023-02-16 16:08:47 -05:00
Greyson Parrelli
ce4e84aadc Remove crash for group remap updates. 2023-02-16 16:08:47 -05:00
Cody Henthorne
23d0152767 Use newer APIs for wave form generation. 2023-02-16 16:08:47 -05:00
Greyson Parrelli
d714590d3f Address bioauth issues on API 28. 2023-02-16 16:08:47 -05:00
Alex Hart
65bc1263f3 Add final copy for username deletion snackbar. 2023-02-16 16:08:47 -05:00
Alex Hart
730065fc76 Add support URL for usernames. 2023-02-16 16:08:47 -05:00
Alex Hart
1e10b82769 Fix gif sizing in conversation. 2023-02-16 16:08:47 -05:00
Cody Henthorne
01f477a587 Fix voice note UX issues. 2023-02-16 16:08:47 -05:00
Alex Hart
76383fe1bc Fix RTL corners in AlbumThumbnailView. 2023-02-16 16:08:47 -05:00
Greyson Parrelli
c61f45b88b Remove old private certificate authority. 2023-02-16 16:08:47 -05:00
Greyson Parrelli
e559198495 Fix crash when creating backup. 2023-02-16 16:08:47 -05:00
Greyson Parrelli
8676cb27ae Don't show add contact button if the user has no e164.
Fixes #12570
2023-02-16 16:08:46 -05:00
Alex Hart
7215ca6a28 Fix issue where view padding would not properly update on rotation. 2023-02-16 16:08:46 -05:00
Alex Hart
ef11a8d98d Fix navbar flashing on transform. 2023-02-16 16:08:46 -05:00
Greyson Parrelli
6efd501f1c Don't send MR accept for unblocking.
That's handled by storage service now.
2023-02-15 18:07:40 -05:00
Greyson Parrelli
66c650e859 Bump version to 6.12.2 2023-02-15 17:35:23 -05:00
Greyson Parrelli
8141f7a5cf Updated language translations. 2023-02-15 17:35:23 -05:00
Greyson Parrelli
0fcdf61e76 Revert "Don't run FTS optimize job (for now)."
This reverts commit f26b2c0b2a.
2023-02-15 17:35:23 -05:00
Greyson Parrelli
fa571f14e6 Update SQLCipher to 4.5.3-FTS-S2 2023-02-15 17:35:23 -05:00
Cody Henthorne
583860053b Cancel scheduled message alarm if no messages are scheduled. 2023-02-15 17:35:23 -05:00
Alex Hart
ad70baf557 Add documentation to DisplaySecondaryInformation. 2023-02-15 17:35:23 -05:00
Cody Henthorne
2b0e9783a7 Fix invalid attachment data during sms export. 2023-02-15 17:35:23 -05:00
Alex Hart
c75a9b577d Prefer about over phone number. 2023-02-15 17:35:23 -05:00
Cody Henthorne
e8ff1a04ed Fix scrolling issue in transfer lock dialog for small displays. 2023-02-15 17:35:22 -05:00
Cody Henthorne
a22a696722 Fix missing padding in schedule message time picker. 2023-02-15 17:35:22 -05:00
Alex Hart
66494fa418 Fix transition for very tall images. 2023-02-15 17:35:22 -05:00
Greyson Parrelli
9fd763fe83 Bump version to 6.12.1 2023-02-15 13:32:37 -05:00
Greyson Parrelli
1d508ad5cc Updated language translations. 2023-02-15 13:31:58 -05:00
Alex Hart
c1c7f57ec0 Fix sticker scaling. 2023-02-15 13:31:58 -05:00
Cody Henthorne
6100160e18 Address various issues with dark theme registration flow. 2023-02-15 13:24:15 -05:00
Alex Hart
e2c3db3eda Fix miscalculation of groups in common. 2023-02-15 13:24:15 -05:00
Alex Hart
6759b59507 Fix toolbar height and fade in. 2023-02-15 13:24:15 -05:00
Greyson Parrelli
e36844fe78 Improve logging around unblocking. 2023-02-15 13:24:15 -05:00
Alex Hart
5cf937215a Fix stub of TransferControlsView. 2023-02-15 13:24:15 -05:00
Alex Hart
1b49b9bffb Hide system UI until the shared element transition completes. 2023-02-15 13:24:15 -05:00
Alex Hart
a3a29d5cb2 Prevent shared element animation when we're not on the initial media. 2023-02-15 13:24:15 -05:00
Nicholas
6fbfb87bd6 Restore entered phone number post-captcha. 2023-02-15 13:24:15 -05:00
Alex Hart
2bff2d3a30 Disable shared element transitions from bubble. 2023-02-15 13:24:15 -05:00
Cody Henthorne
a88410faaf Fix incorrect quick react emojis for story replies. 2023-02-15 13:24:15 -05:00
Greyson Parrelli
f26b2c0b2a Don't run FTS optimize job (for now). 2023-02-15 13:24:15 -05:00
Alex Hart
6f1b03eac6 Utilize fade instead of just setting alpha to 0. 2023-02-15 10:57:54 -04:00
Cody Henthorne
9610339f38 Improve UX around seeing audio wave forms.
- Attempts to generate the wave form on download instead on display
- Allows multi-threaded generation of wave forms instead of serial
  executor
2023-02-15 09:43:16 -05:00
Alex Hart
d4ce8458a4 Ensure e164s are pretty-printed. 2023-02-15 10:10:25 -04:00
Greyson Parrelli
384cdf8610 Bump version to 6.12.0 2023-02-14 22:47:48 -05:00
Greyson Parrelli
3ee30808de Updated language translations. 2023-02-14 22:47:20 -05:00
Greyson Parrelli
78c64880f7 Fix instance where PNI may be accessed too early. 2023-02-14 14:51:29 -05:00
Greyson Parrelli
b99ce9cc1d Fix typo in string. 2023-02-14 14:30:25 -05:00
Greyson Parrelli
41f796d809 Update CDSI_MRENCLAVE. 2023-02-14 14:28:22 -05:00
Alex Hart
ec504af593 Improve conversation open speed. 2023-02-14 15:10:08 -04:00
Greyson Parrelli
60874ba57b Fix contact name syncing to storage service. 2023-02-14 14:03:09 -05:00
Greyson Parrelli
4397b5af25 Add support for storing systemNickname from storage service. 2023-02-14 14:03:09 -05:00
Rashad Sookram
07234443c6 Update verification metadata for MacOS. 2023-02-14 14:03:09 -05:00
Alex Hart
c027203e8c Polish thumbnail animation. 2023-02-14 14:03:09 -05:00
Alex Hart
417db2341b Utilize drawable instead of bitmap for transition. 2023-02-14 14:03:09 -05:00
Bernie Dolan
6aa4ef95b5 Update payments to 4.0.0.1 2023-02-14 14:03:09 -05:00
Greyson Parrelli
6145fa213e Move common gradle config into convention plugins. 2023-02-14 14:03:09 -05:00
Greyson Parrelli
9fa4741e49 Update ContactRecord.hidden field to value 20. 2023-02-14 14:03:09 -05:00
Greyson Parrelli
b9d5fb54c3 Allow using the location picker with approximate location. 2023-02-14 14:03:09 -05:00
Greyson Parrelli
c0fe156897 Do not fail message inserts on bad quote attachments.
Fixes #12721
2023-02-14 14:03:09 -05:00
Alex Hart
22cad64089 Clean up ThumbnailView warnings. 2023-02-14 14:03:09 -05:00
Alex Hart
702cf6ef71 Remove unused layout class. 2023-02-14 14:03:09 -05:00
Alex Hart
d7c3112602 Speed up thumbnail transition. 2023-02-14 14:03:09 -05:00
Greyson Parrelli
d9c31a6cd6 Update AGP to 7.4.0 2023-02-14 14:03:09 -05:00
Greyson Parrelli
408c288936 Convert MediaTable to kotlin. 2023-02-14 14:03:09 -05:00
Greyson Parrelli
af6f16bdb6 Move Backups.proto to Wire. 2023-02-14 14:03:09 -05:00
Cody Henthorne
055ceba398 Add 'AnyAddressPorts' calling field trial flag. 2023-02-14 14:03:08 -05:00
Greyson Parrelli
3f81a94176 Fix case where we were performing remote inserts. 2023-02-14 14:02:23 -05:00
Greyson Parrelli
a02d2e467b Revert "Remove the unknown insert validation."
This reverts commit 320669c54e.
2023-02-14 14:02:23 -05:00
Greyson Parrelli
414550861e Prevent recursive early content processing. 2023-02-14 14:02:23 -05:00
Greyson Parrelli
afbce6f800 Re-enable FTS optimization after deletes. 2023-02-14 14:02:23 -05:00
Alex Hart
dda5037429 Add stubbing to ConversationThumbnailView and caching to a typeface. 2023-02-14 14:02:23 -05:00
Greyson Parrelli
ffbebe0670 Update SQLCipher to 4.5.3-FTS-S1 2023-02-14 14:02:23 -05:00
Alex Hart
cf250b4b32 Add catch for candidate generation error to be treated the same as username unavailable. 2023-02-14 14:02:23 -05:00
Nicholas Tinsley
b14aea0922 Support dark mode in verification code keyboard. 2023-02-14 14:02:23 -05:00
Alex Hart
d0de43a6b2 Add thumbnail shared element animation. 2023-02-14 14:02:23 -05:00
Alex Hart
2c48d40375 Update API endpoints and integration for usernames. 2023-02-14 14:02:23 -05:00
Greyson Parrelli
803154c544 Add a new PNP build flavor. 2023-02-14 14:02:23 -05:00
Greyson Parrelli
684150dc1e Handle split contacts in storage service when in PNP mode. 2023-02-14 14:02:22 -05:00
Greyson Parrelli
fdcf0a76e8 Split unregistered contacts when in PNP mode. 2023-02-14 14:02:22 -05:00
Greyson Parrelli
9e056e5dd0 Add support for rendering session switchover events. 2023-02-14 14:02:22 -05:00
Cody Henthorne
03c68375db Fix bad group state when requesting to rejoin a group. 2023-02-14 14:02:22 -05:00
Alex Hart
5d328857aa Upgrade libsignal to 0.22.0 2023-02-14 14:02:22 -05:00
Cody Henthorne
3a0dbe6e67 Use alarm clock for scheduling message sends. 2023-02-14 14:02:22 -05:00
Cody Henthorne
56b35f3767 Fix quoted links from rendering as clickable. 2023-02-14 14:02:22 -05:00
Greyson Parrelli
7f0221c5c6 Prefer MessageRecord mismatches when updating SN's. 2023-02-14 14:02:22 -05:00
Cody Henthorne
23050152de Replace time duration picker dialog for screen lock timeout. 2023-02-14 14:02:22 -05:00
Alex Hart
db65edb7df Mark DSL api discouraged. 2023-02-14 14:02:22 -05:00
Alex Hart
605289aca4 Upgrade ktlint and add twitter compose rules. 2023-02-14 14:02:22 -05:00
Jim Gustafson
52e9b31554 Update to RingRTC v2.24.0 2023-02-14 14:02:22 -05:00
Alex Hart
c8e6ccc0c0 Add extended colors to SignalTheme. 2023-02-14 14:02:22 -05:00
Alex Hart
f20d929292 Add Buttons object for properly themed compose buttons. 2023-02-14 14:02:22 -05:00
Greyson Parrelli
a9accfb074 Bump version to 6.11.7 2023-02-14 14:01:40 -05:00
Greyson Parrelli
8f2d1a2d12 Updated language translations. 2023-02-14 14:01:40 -05:00
Greyson Parrelli
ca8755c6ad Inline the scheduled message feature flag. 2023-02-14 14:01:40 -05:00
Cody Henthorne
dc4eb7911d Bump version to 6.11.6 2023-02-13 13:43:23 -05:00
Cody Henthorne
eb2e0205ae Updated language translations. 2023-02-13 13:31:23 -05:00
Cody Henthorne
7a72a9a0d7 Fix memory leak in conversation fragment. 2023-02-13 13:07:32 -05:00
Alex Hart
805ccc4f7a Bump version to 6.11.5 2023-02-10 16:13:23 -04:00
Alex Hart
499b186b68 Updated language translations. 2023-02-10 16:12:21 -04:00
Cody Henthorne
c741e32824 Fix stale thread id when a conversation is deleted. 2023-02-10 13:08:51 -05:00
Alex Hart
fba4c882cb Bump version to 6.11.4 2023-02-09 16:16:41 -04:00
Alex Hart
24ef853f24 Updated language translations. 2023-02-09 16:16:04 -04:00
Nicholas
9f22ba68ea Set PIN entry text to use dynamic theme colors. 2023-02-09 13:45:26 -05:00
Greyson Parrelli
d8eac87219 Cleanup dangling MSL rows. 2023-02-09 13:41:32 -05:00
Greyson Parrelli
cf71e2cfa8 Bump version to 6.11.3 2023-02-08 21:21:36 -05:00
Greyson Parrelli
7f16d0653c Updated language translations. 2023-02-08 21:20:43 -05:00
Greyson Parrelli
61e127fabf Fix method to find MMS group. 2023-02-08 21:13:14 -05:00
Alex Hart
7ffdf91ce5 Bump version to 6.11.2 2023-02-07 15:06:25 -04:00
Alex Hart
4c26fe432e Updated language translations. 2023-02-07 15:06:25 -04:00
Cody Henthorne
6c78a405bb Fix backup scheduling looping bug. 2023-02-07 15:06:25 -04:00
Cody Henthorne
89b0167fd2 Ensure backup job verification can be cancelled. 2023-02-07 11:19:26 -05:00
Alex Hart
e25133fa29 Bump version to 6.11.1 2023-02-06 17:15:26 -04:00
Alex Hart
4ba77c0f9f Updated language translations. 2023-02-06 17:09:45 -04:00
Nicholas Tinsley
10f376e402 Catch new audio recording error states. 2023-02-06 16:53:08 -04:00
Cody Henthorne
7bae8b6e1b Fix scheduled message sends changing thread disappearing message timer. 2023-02-06 16:53:08 -04:00
Cody Henthorne
67fb9d09d4 Fix scheduled send in note to self with no linked devices. 2023-02-06 16:53:08 -04:00
Nicholas
3b40b10a77 Try to check group mute status for keeping archived. 2023-02-06 16:53:08 -04:00
Cody Henthorne
418b486776 Fix crash when scheduling a message in an empty thread. 2023-02-06 16:53:08 -04:00
Cody Henthorne
1f31f4a50a Adjust SMS phases and show Phase 3 start date. 2023-02-06 16:53:08 -04:00
Cody Henthorne
9b08ebcc1d Update QR code and send symbols. 2023-02-06 16:53:08 -04:00
Nicholas
aec4944c56 Allow V1 groups to be deleted by clearing app data. 2023-02-06 16:53:08 -04:00
Nicholas Tinsley
9a1f8af703 Add Material3 SVG assets to Registration. 2023-02-06 16:53:08 -04:00
Greyson Parrelli
268b11c4e1 Fix bug in MSL table definition. 2023-02-06 16:53:08 -04:00
Nicholas Tinsley
2e3d73f44b Media preview design tweaks. 2023-02-06 16:53:08 -04:00
Alex Hart
f477a4dae9 Add compose bottom-sheet handle. 2023-02-06 16:53:08 -04:00
Greyson Parrelli
a41aed20e1 Fix issue where group stories weren't syncing to linked devices. 2023-02-03 12:08:21 -05:00
Alex Hart
1ed3dbb147 Add svg asset for username megaphone. 2023-02-03 09:46:42 -04:00
Alex Hart
fcfb9fad01 Add svg assets to username education screen. 2023-02-03 09:45:13 -04:00
Alex Hart
25c96a6be6 Resolve crashing when trying to get the header letters for the contacts section of search. 2023-02-03 09:33:18 -04:00
Nicholas Tinsley
90695182f3 Bump version to 6.11.0 2023-02-02 17:55:33 -05:00
Nicholas Tinsley
1c38ab18b8 Updated language translations. 2023-02-02 17:55:12 -05:00
Alex Hart
7c716e5525 Fix slow kotlin build. 2023-02-02 17:22:40 -05:00
Cody Henthorne
56a44ae65c Enforce expected ordering when scheduling text and media messages. 2023-02-02 17:22:40 -05:00
Nicholas
d33aa247db Fix composer voice memo cancellation due to focus loss. 2023-02-02 17:22:40 -05:00
Alex Hart
63a153571d Add generic Compose fragment. 2023-02-02 17:22:40 -05:00
Alex Hart
fb07e897d0 Mark username megaphone completion after hitting continue. 2023-02-02 17:22:40 -05:00
Alex Hart
93387ec79a Add deletion snackbar for usernames with temporary copy. 2023-02-02 17:22:40 -05:00
Alex Hart
cd79dbbb82 Add Username UI updates. 2023-02-02 17:22:40 -05:00
Alex Hart
7fbfc09a89 Refactor ContactSelectionListFragment to use ContactSearch infrastructure. 2023-02-02 17:22:40 -05:00
Alex Hart
0f6bc0471c Add core-ui module and Jetpack Compose. 2023-02-02 17:22:40 -05:00
Alex Hart
ba919d4ecc Add proper styling for text inputs. 2023-02-02 17:22:40 -05:00
Alex Hart
73722297cf Fix membership query to account for active state and mms state. 2023-02-02 17:22:40 -05:00
Greyson Parrelli
6050a9f585 Update feature flag constant. 2023-02-02 17:22:40 -05:00
Cody Henthorne
b243eee4ce Fix incorrect unread count after sending scheduled messages. 2023-02-02 17:22:40 -05:00
Greyson Parrelli
a91a13cead Introduce Wire for proto codegen. 2023-02-02 17:22:40 -05:00
Nicholas
72449fd73e Store & submit spam reporting token from server. 2023-02-02 17:22:40 -05:00
Cody Henthorne
6a8e82ef91 Prevent scheduling of sends when alarm permission is denied. 2023-02-02 17:22:40 -05:00
Greyson Parrelli
987fafff92 Remove is_mms field from MSL tables. 2023-02-02 17:22:40 -05:00
Cody Henthorne
35ff977df9 Fix position calculation for conversations with scheduled messages. 2023-02-01 19:23:12 -05:00
Cody Henthorne
fe2d71fca0 Delete pending scheduled messages when leaving a group. 2023-02-01 19:04:29 -05:00
Greyson Parrelli
a12a246e87 Add foreign key constraint details to Spinner. 2023-02-01 17:41:28 -05:00
Alex Hart
4f387cf8d9 Add username education screen. 2023-02-01 17:41:28 -05:00
Cody Henthorne
dae69744c2 Prevent schedule send UI from showing in story send flow. 2023-02-01 17:41:28 -05:00
Cody Henthorne
4ad233c6d1 Show will send immediately warning if scheduled send is in the past. 2023-02-01 17:41:28 -05:00
Cody Henthorne
b4c572678c Fix incorrect ripple effect in search box.
Fixes #12641
2023-02-01 17:41:28 -05:00
Cody Henthorne
5024998a6f Improve clarity of screen lock timeout duration. 2023-02-01 17:41:28 -05:00
Jim Gustafson
43cde19071 Remove redundant logging.
And move the backup stun server to the end of the list.
2023-02-01 17:41:28 -05:00
Greyson Parrelli
62a2f3d8ba Fix bug when highlighting search results. 2023-02-01 17:41:28 -05:00
Greyson Parrelli
5bc44fa586 Improve network reliability. 2023-02-01 17:41:28 -05:00
Greyson Parrelli
f0b3aa66f7 Revert "Enable gradle configuration cache."
This reverts commit 6e5b4bbc15.
2023-02-01 17:41:28 -05:00
Clark
ef9cd2515e Add new story reaction bar. 2023-02-01 17:41:28 -05:00
Greyson Parrelli
4677f207e7 Rotate scheduled message feature flag. 2023-02-01 17:41:28 -05:00
Bernie Dolan
4c26f3258d Update Mobilecoin testnet enclave values. 2023-02-01 17:41:28 -05:00
Cody Henthorne
77a3037614 Update icons in popup/context menus. 2023-02-01 17:41:28 -05:00
Alex Hart
9600d6f6a9 Add different menu copy for clearing the enabled chat filter. 2023-02-01 17:41:28 -05:00
Alex Hart
36dfa19aec Add "contacts without threads" section to Conversation List Search. 2023-02-01 17:41:28 -05:00
Alex Hart
09902e5d11 Add "Group Members" section to ConversationList search results. 2023-02-01 17:41:27 -05:00
Nicholas Tinsley
e84c6187b9 Bump version to 6.10.5 2023-02-01 16:59:54 -05:00
Nicholas Tinsley
b5d52db57c Add logging for audio recorder exceptions. 2023-02-01 11:41:30 -05:00
Alex Hart
f320cf8833 Add factory for story privacy view model. 2023-02-01 12:22:55 -04:00
Cody Henthorne
c76ca957e1 Prevent keyboard from closing immediately after opening. 2023-02-01 10:35:42 -05:00
Cody Henthorne
56354f6aae Fix memory leak of schedule message observers. 2023-02-01 10:22:17 -05:00
Nicholas Tinsley
2bf84a5f77 Fix country code dropdown during registration. 2023-02-01 09:54:44 -05:00
Nicholas Tinsley
8b23d9a6c4 Bump version to 6.10.4 2023-01-31 17:20:05 -05:00
Nicholas Tinsley
2cae3ddf04 Updated language translations. 2023-01-31 17:20:05 -05:00
Greyson Parrelli
670b6c4c56 Revert "Upgrade to Glide 4.14.2"
This reverts commit 3ee889cb79.
2023-01-31 16:15:33 -05:00
Alex Hart
eceed641bf Fix leak in recording session. 2023-01-31 16:15:33 -05:00
Nicholas
5febe6490c Null check Media URI in save task. 2023-01-31 16:15:33 -05:00
Nicholas
dca47e4cb5 Strip mention Spans out of media captions. 2023-01-31 16:15:33 -05:00
Nicholas
c3bcba6380 Design improvements for registration flow. 2023-01-31 16:15:33 -05:00
Alex Hart
cb01692a50 Fix localization of note to self string in search. 2023-01-31 10:07:50 -04:00
Alex Hart
e90074ffef Fix issue with bottom sheets. 2023-01-31 10:00:25 -04:00
Alex Hart
691520bc75 Clean out dead code from contact search. 2023-01-30 12:52:27 -04:00
Greyson Parrelli
e6de06be6f Bump version to 6.10.3 2023-01-30 10:40:13 -05:00
Greyson Parrelli
a77079ac81 Updated language translations. 2023-01-30 10:39:52 -05:00
Greyson Parrelli
30b58fe5f4 Don't run FTS optimize job (for now). 2023-01-30 10:39:41 -05:00
Greyson Parrelli
7275b95b58 Bump version to 6.10.2 2023-01-27 17:42:22 -05:00
Greyson Parrelli
f04d46b4ed Updated language translations. 2023-01-27 17:42:22 -05:00
Greyson Parrelli
e12bbe943b Restore the 3-dot menu when creating a PIN. 2023-01-27 17:42:22 -05:00
Greyson Parrelli
7348224dc2 Prevent thread trimming from gumming up the database. 2023-01-27 17:42:22 -05:00
Cody Henthorne
30c33fdd77 Fix issues with scheduled messages and quotes.
- Tapping quote in schedule view will jump to message in chat
- Scheduling a quote will not make the quoted message render as "isQuoted"
- Scheduled quotes will not appear in the quoted message's sheet of replies
- Fixes an off-by-N where N = # of scheduled messages when calculating location for jumping to a message
2023-01-27 17:42:22 -05:00
Alex Hart
e7339af119 Add fix for group membership query. 2023-01-27 17:42:22 -05:00
Cody Henthorne
661fff7a0e Fix scheduled messages being sent out of order. 2023-01-27 17:42:22 -05:00
Alex Hart
c37bad0f7a Fix opening filter when swiping from within collapsingtoolbar. 2023-01-27 17:42:22 -05:00
Alex Hart
7f228fc0fd Do not display add to story if stories are disabled. 2023-01-27 17:42:22 -05:00
Cody Henthorne
14cd216668 Fix crash when delete scheduled message dialog is open and message sends. 2023-01-27 17:42:22 -05:00
Cody Henthorne
0cb0ef977c Add calendar icon to pick date and time item.
Co-authored-by: Sgn-32 <49990901+Sgn-32@users.noreply.github.com>
2023-01-27 17:42:22 -05:00
Cody Henthorne
1761529ce9 Disable schedule message for SMS. 2023-01-27 17:42:22 -05:00
Clark
a14fc82e83 Fix scheduled view once message looking weird. 2023-01-27 17:42:22 -05:00
Clark
b94f5501d9 Disable scheduling of voice note messages. 2023-01-27 17:42:21 -05:00
Clark
834283ba9b Hide scheduled messages bar with input panel. 2023-01-27 17:42:21 -05:00
Nicholas
23190a2f6e Fix NumericKeyboardView in RTL. 2023-01-27 11:06:04 -05:00
Alex Hart
04f4cd8edc Fix V172 Migration. 2023-01-27 10:20:44 -04:00
Greyson Parrelli
8f02e4e1f5 Bump version to 6.10.1 2023-01-26 20:25:28 -05:00
Greyson Parrelli
db81a5be04 Updated language translations. 2023-01-26 20:25:28 -05:00
Greyson Parrelli
fe40e37da4 Fix full text search migration after table name change. 2023-01-26 20:25:28 -05:00
Alex Hart
22a4271dfb Rotate paypal recurring donations flag. 2023-01-26 20:25:28 -05:00
Greyson Parrelli
1263b51e03 Patch random place where we forgot to update minSdk. 2023-01-26 20:25:28 -05:00
Nicholas
ca468047ef Adjust media caption height, fix rail visibility. 2023-01-26 20:25:28 -05:00
Clark
958c52a5b8 Force indexes for scheduled message queries. 2023-01-26 20:25:28 -05:00
Greyson Parrelli
9b28585c59 Add foreign key dependency between reactions and messages. 2023-01-26 20:25:27 -05:00
Clark
c5c60b7214 Add permissions dialogs for scheduled messages. 2023-01-26 20:25:27 -05:00
Nicholas
31bcc2e2eb Finish MediaPreviewV2Activity when jumping to a message. 2023-01-26 20:25:27 -05:00
Cody Henthorne
71ecba17fc Fix crash when saving empty formatted text drafts. 2023-01-26 20:25:27 -05:00
Greyson Parrelli
afa5c68312 Periodically optimize the FTS index. 2023-01-26 20:25:27 -05:00
Clark
f3e715e069 Add support for scheduled message sends. 2023-01-26 20:25:27 -05:00
Alex Hart
df695f7611 Fix crash when trying to create new group story.
Adds INNER JOIN to threads table to allow access to date in ORDER BY
2023-01-26 20:25:27 -05:00
Greyson Parrelli
27e1bc0854 Bump version to 6.10.0 2023-01-25 17:11:34 -05:00
Greyson Parrelli
f4371b9e96 Updated language translations. 2023-01-25 17:08:53 -05:00
Cody Henthorne
e0633180ef Fix crash when trying to update a group call without an era id. 2023-01-25 17:02:41 -05:00
Alex Hart
32dd227ab6 Utilize left join instead of inner join when querying groups. 2023-01-25 17:02:41 -05:00
Cody Henthorne
0deed9d4d2 Fix notification sound not respecting notification volume.
Fix is immediate for general messages channel and for future custom channel creation
2023-01-25 17:02:41 -05:00
Cody Henthorne
cc490f4b73 Add text formatting send and receive support for conversations. 2023-01-25 17:02:41 -05:00
Cody Henthorne
aa2075c78f Attempt to fix view jitter when switching keyboards. 2023-01-25 17:02:41 -05:00
Alex Hart
b4a34599d7 Add support for message and thread results. 2023-01-25 17:02:41 -05:00
Nicholas
8dd1d3bdeb Allow user-selected backup time. 2023-01-25 17:02:41 -05:00
Greyson Parrelli
a7d9bd944b Insert session switchover events when appropriate. 2023-01-25 17:02:41 -05:00
Clark
7745ae62ea Add logging for voice note recording events. 2023-01-25 17:02:41 -05:00
Greyson Parrelli
6e5b4bbc15 Enable gradle configuration cache.
Android Studio told me to do this and that it would save me over 7
seconds. We'll see if it breaks anything :p
2023-01-25 17:02:41 -05:00
Clark
e3b38e6d38 Fix some thumbnail images not showing up in MediaGallery. 2023-01-25 17:02:41 -05:00
Clark
25aa4f39a3 Close input streams on failed resource decryption. 2023-01-25 17:02:41 -05:00
Cody Henthorne
17849e20bd Update group receipt table when sync'ing story sends to distribution lists. 2023-01-25 17:02:41 -05:00
Alex Hart
c022172ace Add group member results to contact search. 2023-01-25 17:02:41 -05:00
Nicholas
eaeeb08987 Display message text in Media Preview. 2023-01-25 17:02:41 -05:00
Alex Hart
1b7e4e047c Introduce ManyToMany table for group membership. 2023-01-24 14:18:28 -05:00
Clark
d635683303 Fix share intent not being cleared from recents. 2023-01-24 14:18:28 -05:00
Clark
4dcbbfdd63 Fix voice note draft not being generated on audio focus loss. 2023-01-24 14:18:28 -05:00
Nicholas
150bbf181d Redesign FTUX to use Material Design 3. 2023-01-24 14:18:28 -05:00
Alex Hart
0303467c91 Add PayPal decline code errors. 2023-01-24 14:18:28 -05:00
Nicholas
88da382a6f For calling purposes, categorize hearing aids as Bluetooth headsets. 2023-01-24 14:18:28 -05:00
Alex Hart
5d14166a27 Add support for arbitrary rows in contact search. 2023-01-24 14:18:28 -05:00
Jim Gustafson
d76d13f76c Update to RingRTC v2.23.1 2023-01-24 14:18:28 -05:00
Greyson Parrelli
ad4ec23875 Bump version to 6.9.2 2023-01-24 14:15:52 -05:00
Greyson Parrelli
61df2afc32 Updated language translations. 2023-01-24 14:15:52 -05:00
Nicholas Tinsley
1c6d2f7198 If enabled, don't unarchive muted group chats.
Fixes #12732
2023-01-24 14:13:42 -05:00
Cody Henthorne
df8f9761b2 Fix incorrect total sms export count. 2023-01-24 13:49:25 -05:00
Greyson Parrelli
657c5d2bce Bump version to 6.9.1 2023-01-20 18:12:37 -05:00
Greyson Parrelli
81324c6923 Updated language translations. 2023-01-20 18:12:17 -05:00
Greyson Parrelli
269a2e2990 Improve timer event generation from GV2 sync messages. 2023-01-20 17:58:17 -05:00
clark-signal
73b453b0d4 Fix re-used share intent when restarting task from recent activities. 2023-01-20 13:42:13 -05:00
Cody Henthorne
97604dc4c5 Bump version to 6.9.0 2023-01-19 13:44:21 -05:00
Cody Henthorne
8e1c05ed64 Updated language translations. 2023-01-19 13:38:09 -05:00
Nicholas
231b55a956 Don't show media controls on new pages. 2023-01-19 13:33:07 -05:00
Alex Hart
4fcdee9fa5 Rotate PayPal recurring donations feature flag. 2023-01-19 13:33:07 -05:00
Cody Henthorne
6e2e5e21cc Update copy and behavior of SMS phased removal flow. 2023-01-19 13:33:07 -05:00
Alex Hart
8f49323648 Extract adapter creation from ContactSearchMediator. 2023-01-19 13:33:07 -05:00
Greyson Parrelli
13f969b622 Fix an app migration.
Fixes #12730
2023-01-19 13:33:07 -05:00
Greyson Parrelli
518d9b3984 Update SQLCipher to 4.5.1-S1
This reverts commit d1894caea6.
2023-01-19 13:33:07 -05:00
Alex Hart
8e313f8387 Collapse KnownRecipient / Story into single model. 2023-01-19 13:33:07 -05:00
Nicholas
70c6e9e60f Store additional data that will allow us to reduce the number of verification SMSs. 2023-01-19 13:32:35 -05:00
Cody Henthorne
dcf8a82c37 Fix no snippet being shown for threads.
Snippet query wasn't updated to exclude SMS types after the conjuction
of the tables.
2023-01-19 13:32:35 -05:00
Alex Hart
f368e5b133 Suppress deselection error when opening gallery from chat. 2023-01-19 13:32:35 -05:00
Alex Hart
3ee889cb79 Upgrade to Glide 4.14.2 2023-01-17 14:30:48 -05:00
Greyson Parrelli
3e7dc79fe8 Remove unnecessary code now that minSdk is 21. 2023-01-17 14:30:48 -05:00
Greyson Parrelli
8cfd02aff2 Bump minSdk to 21. 2023-01-17 14:30:48 -05:00
Alex Hart
f36efc562e Fix improper filtering of unread conversations. 2023-01-17 14:30:48 -05:00
Sgn-32
67b6b109de Use ic_save_24_tinted instead of ic_download_24_tinted. 2023-01-17 14:30:48 -05:00
Alex Hart
8fd378db4e Fix issue where links do not render in stories if previews are off. 2023-01-17 14:30:48 -05:00
Cody Henthorne
760ace93d4 Enable PNI group invite processing. 2023-01-17 14:30:48 -05:00
Nicholas
125fd83afa Programmatically dismiss logged out notification on registration. 2023-01-17 14:30:48 -05:00
Alex Hart
7dcb598b66 Inline gift badge flag. 2023-01-17 14:30:48 -05:00
Alex Hart
4917e93d9f Update CameraX to 1.2.0 2023-01-17 14:30:48 -05:00
Alex Hart
1e9115a917 Increment compileSdkVersion to 33. 2023-01-17 14:30:48 -05:00
clark-signal
7af94f60ae Fix story linking from private story reply. 2023-01-17 14:30:48 -05:00
clark-signal
c3c8f8e7e6 Update icons across home screen and settings screen. 2023-01-17 14:30:48 -05:00
clark-signal
011c85c75b Fix "My subscription" dark mode overlapping background bubbles. 2023-01-17 14:30:47 -05:00
Cody Henthorne
90112bec31 Bump version to 6.8.3 2023-01-17 14:15:45 -05:00
Cody Henthorne
bca43fb93b Updated language translations. 2023-01-17 14:00:36 -05:00
Alex Voloshyn
b6ee69d346 Add proguard config for payments. 2023-01-17 09:37:45 -05:00
Alex Hart
b2a4dc303b Bump version to 6.8.2 2023-01-13 16:47:35 -04:00
Alex Hart
8553cf6b96 Updated language translations. 2023-01-13 16:47:23 -04:00
Alex Hart
276f485b49 Fix media review screen view translation when animations are disabled.
Fixes #12698
2023-01-13 10:28:14 -04:00
Greyson Parrelli
54ffb4ad7b Improve network reliability. 2023-01-12 17:54:08 -05:00
Alex Hart
28531bb415 Bump version to 6.8.1 2023-01-12 15:46:19 -04:00
Alex Hart
d0c4cefaad Updated language translations. 2023-01-12 15:41:46 -04:00
Nicholas Tinsley
d765fb1d5d Fix androidTests for registration changes. 2023-01-12 15:36:21 -04:00
Greyson Parrelli
bce2dd1d1b Fix v171 migration.
Copy-pasted from the old one, but forgot that the sms table no longer
exists...
2023-01-12 15:36:21 -04:00
Sgn-32
c099ad0aa7 Replace the old SimpleProgressDialog with the new ProgressCard in WallpaperCropActivity. 2023-01-12 15:36:21 -04:00
Alex Hart
3738daf4a8 Pre-cache max unreserved player count on process startup.
This call was taking up to 20ms to run on the first time we would
load a conversation. Adding a post-render call to app launch reduces
this time to 0ms, as the data has been cached by the system. Doing
it on a background thread means we do not pay for it in TTFF.
2023-01-12 15:36:21 -04:00
Greyson Parrelli
564b9f47ee Add banner warning about API 19 deprecation. 2023-01-12 15:36:21 -04:00
Alex Hart
7617a164fc Bump version to 6.8.0 2023-01-11 14:25:59 -04:00
Alex Hart
46c98f4e0b Updated language translations. 2023-01-11 14:22:49 -04:00
Greyson Parrelli
522346479c Improve network reliability. 2023-01-11 14:18:25 -04:00
Greyson Parrelli
df86d1b4ba Re-run migration to add foreign key to thread table.
Unfortunately the table schema wasn't fixed for new installs, so we need
to run it again.
2023-01-11 14:18:25 -04:00
clark-signal
74760a4a64 Add alternate junit-bom 5.8.1 to fix CI. 2023-01-11 14:18:25 -04:00
clark-signal
9e903a023f Cleanup media rail crossfade animation. 2023-01-11 14:18:25 -04:00
clark-signal
6120902ff5 Prevent system UI from being hidden incorrectly while paging media. 2023-01-11 14:18:25 -04:00
Cody Henthorne
1d2fbf0ebf Make adding properties to media send result easier. 2023-01-11 14:18:25 -04:00
Greyson Parrelli
40f9a25b87 Set Accept-Language header when requesting SMS code. 2023-01-11 14:18:25 -04:00
clark-signal
f2881843db Fix MediaRailAdapter request counting. 2023-01-11 14:18:25 -04:00
Greyson Parrelli
c53b090b76 Fix formatting in LiveRecipient. 2023-01-11 14:18:24 -04:00
Alex Voloshyn
1ba2712375 Update MobileCoin SDK to v4.0.0 2023-01-11 14:18:24 -04:00
Alex Hart
71ff31e91f Fix Stripe json body error handling. 2023-01-11 14:18:24 -04:00
Nicholas
aa9a530e59 Upload PIN to KBS on successful register. 2023-01-11 14:18:24 -04:00
Nicholas
e4cc7f5181 Attempt to Skip PIN Entry on Re-Registration. 2023-01-11 14:18:24 -04:00
Cody Henthorne
13f43799d6 Unify registration response models and processors. 2023-01-11 14:18:24 -04:00
Alex Hart
3543a5ea24 Update donations strings. 2023-01-11 14:18:24 -04:00
Greyson Parrelli
429f89cba1 Add some logging for early delivery receipts. 2023-01-11 14:18:24 -04:00
clark-signal
5b0084a5e2 Fix group story selection crash. 2023-01-11 14:18:24 -04:00
clark-signal
a3bc831346 Fix mentions in drafts being uneditable. 2023-01-11 14:18:24 -04:00
Alex Hart
2fd6b7c49e Fix donation payment update issue. 2023-01-11 14:18:24 -04:00
clark-signal
fa613557e8 Hide "Add as contact" option when no phone number present. 2023-01-11 14:18:20 -04:00
clark-signal
87c366223a Load drafts from DB when opening conversation from notifications. 2023-01-11 14:18:15 -04:00
Alex Hart
aac1d0cedb Utilize switchMapSingle instead of flatMapSingle. 2023-01-11 13:17:26 -04:00
Jim Gustafson
ea12cde1d8 Update to RingRTC v2.23.0 2023-01-11 13:17:25 -04:00
Alex Hart
9e2a5002bc Update paypal payment method endpoint and enable subs in staging. 2023-01-11 13:16:43 -04:00
Alex Hart
396742f3ad Add new chat bubble pulse. 2023-01-11 13:16:43 -04:00
Alex Hart
af0fbdd2b2 Fix story splitting in multishare flow. 2023-01-11 13:16:43 -04:00
Cody Henthorne
bdbeefe08e Update MobileCoin measurements and configuration. 2023-01-11 13:16:43 -04:00
Nicholas
9a763bd726 Upgrade Lottie to 5.2.0. 2023-01-11 13:16:43 -04:00
Alex Hart
9636aa4d37 Bump version to 6.7.6 2023-01-11 11:00:13 -04:00
Alex Hart
410e6abba9 Updated language translations. 2023-01-11 11:00:00 -04:00
Greyson Parrelli
d1894caea6 Revert "Update SQLCipher to 4.5.1-S1"
This reverts commit f1d204b834.
2023-01-11 08:57:56 -05:00
Alex Hart
af335a447f Bump version to 6.7.5 2023-01-10 13:05:34 -04:00
Alex Hart
711eec4bf2 Updated language translations. 2023-01-10 13:03:00 -04:00
Alex Hart
80c5cbe0da Add padding around clear-filter tip. 2023-01-10 12:57:28 -04:00
Alex Hart
84d0283719 Ensure capability check occurs for gift recipients. 2023-01-10 12:57:03 -04:00
Alex Hart
53e347a67d Bump version to 6.7.4 2023-01-09 13:32:28 -04:00
Alex Hart
3d6220737a Updated language translations. 2023-01-09 13:29:12 -04:00
Alex Hart
383525e7b7 Update chat filter behaviour after round of feedback. 2023-01-06 16:40:51 -04:00
Cody Henthorne
3869de414f Bump version to 6.7.3 2023-01-06 14:15:29 -05:00
Cody Henthorne
89bbfd3ded Updated language translations. 2023-01-06 13:06:33 -05:00
Greyson Parrelli
e2fb65920c Ensure SMS and MMS messages are sent appropriately. 2023-01-06 11:27:33 -05:00
Greyson Parrelli
5537039e46 Bump version to 6.7.2 2023-01-05 15:03:41 -05:00
Greyson Parrelli
0a40432ed4 Fix crash that occurs during thread trims by date. 2023-01-05 14:52:51 -05:00
Cody Henthorne
73e46053f0 Fix voice note crash when future failed. 2023-01-05 12:12:03 -05:00
Greyson Parrelli
a835e5d143 Bump version to 6.7.1 2023-01-05 10:14:04 -05:00
Greyson Parrelli
073d5dfe8c Don't mark unauthorized unless we're registered. 2023-01-05 10:05:58 -05:00
Greyson Parrelli
bfba60b6b6 Improve reliability of SqlUtil.getNextAutoIncrementId() 2023-01-05 10:01:33 -05:00
Alex Hart
84d9e1d28e Drop proxy sheet events if fragment state has been saved. 2023-01-05 09:44:26 -04:00
Alex Hart
a14c61c370 Rotate chat-filters flag. 2023-01-05 09:38:54 -04:00
Greyson Parrelli
0db4630f58 Bump version to 6.7.0 2023-01-04 16:57:33 -05:00
Greyson Parrelli
6d2e51def6 Updated language translations. 2023-01-04 16:57:05 -05:00
Nicholas
091eb0aa2b Fix chaining of Stories FTUX for small screens. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
59f05e0815 Improve performance of marking chats read.
SQLite isn't always smart enough to use the best index for a query.
The main improvement here was to force it to use a better index than the
one it was using (which, on my device, happened to by the story index,
which was only minimally useful here).
2023-01-04 16:40:36 -05:00
Greyson Parrelli
a513e93d18 Fix log around sending SKDM's. 2023-01-04 16:40:36 -05:00
Alex Hart
91f6cff4df Do not display recovery card if entropy is not set. 2023-01-04 16:40:36 -05:00
Alex Hart
ec3ec969eb Add updated search hint text when filter is enabled. 2023-01-04 16:40:36 -05:00
Alex Hart
54d0df9a05 Ensure only a single story is displayed above the fold in bottom sheets. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
320669c54e Remove the unknown insert validation.
There's actually a legitimate case where this is ok: right after a
backup restore.

Restoring a backup means that you have possibly carried over some
unknownIds, and if you don't remember your PIN, those items wouldn't be
there remotely. And you _should_ insert them. Otherwise they're lost.

I don't think this validation is worth the trouble of carving out lots
of conditions to allow this usecase.
2023-01-04 16:40:36 -05:00
Alex Hart
bf491c25f7 Update view badge bottom sheet button copy. 2023-01-04 16:40:36 -05:00
Alex Hart
1e153e129c Ensure ShareViewModel#resolve is performed on the background thread.
Fixes #12696
2023-01-04 16:40:36 -05:00
Cody Henthorne
b546d661ba Update 1:1 call event copy. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
6a1a657451 Fix story database observer updates to ensure rings are updated live. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
ece087eaae Require non-null uri in UriAttachment.
Part of an effort to track down a different crash. This should make it
more obvious where the error is being made.
2023-01-04 16:40:36 -05:00
Alex Hart
a04590b658 Ensure pre-upload media is properly fanned out to group stories. 2023-01-04 16:40:36 -05:00
Alex Hart
eb6a14e686 Fix bad background on long text posts. 2023-01-04 16:40:36 -05:00
Alex Hart
c7bb0eadc2 Fix restore-state of filter pull view. 2023-01-04 16:40:36 -05:00
Nicholas
d70fe8f2cd Only notify for unauthorized response to messages endpoint. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
0fd8f73cca Show username if appropriate in group chips. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
b9fc36be5a Prevent crash when adding group member by username. 2023-01-04 16:40:36 -05:00
Alex Hart
4de27482bb Update blocking get call to safeBlockingGet. 2023-01-04 16:40:36 -05:00
Nicholas
ba347301cf Keep media preview image order even for RTL locales.
Addresses #12574.
2023-01-04 16:40:36 -05:00
Alex Hart
296a113c65 Add "You can pull to filter" tip. 2023-01-04 16:40:36 -05:00
Cody Henthorne
43fe789807 Add support for general media attachments to release notes channel messages. 2023-01-04 16:40:36 -05:00
Alex Hart
98dfd5bfbf Lower fade out duration of chat pill to 150ms. 2023-01-04 16:40:36 -05:00
Alex Hart
f387785a46 Add chat filter pill fade out on slide close. 2023-01-04 16:40:36 -05:00
Cody Henthorne
7b3d8d01ae Prevent dismissal of notification.
Fixes #12681
2023-01-03 11:03:06 -05:00
Alex Hart
1712442560 Add chat filter pill color lerp at close apex. 2023-01-03 10:47:53 -04:00
Nicholas
e4ddedcc48 Log type of connected headsets. 2023-01-03 09:33:08 -05:00
Alex Hart
14503b952a Add helper text when dragging filter at a low velocity. 2023-01-03 10:31:31 -04:00
Nicholas
5cb3e1cd02 Launch "Keep Muted Chats Archived". 2023-01-03 09:05:57 -05:00
Nicholas
7959343661 Add local notification when client receives HTTP 403
Also corrects typo in method name.
2023-01-02 17:20:42 -05:00
Alex Hart
52062679d4 Update filter chip padding to match spec. 2023-01-02 15:05:07 -04:00
Alex Hart
9e8350e8c2 Implement chat filter design feedback. 2023-01-02 12:19:04 -04:00
Sgn-32
495c91ba86 Use the correct delivery time in the message details
CLoses #11655
2023-01-02 09:51:20 -05:00
Greyson Parrelli
92b9fda6c7 Convert GroupTable to kotlin.
Also required converting some tests to mockk.
2023-01-01 23:05:02 -05:00
Greyson Parrelli
fecfd7cd78 Remove the rest of MmsSmsTable. 2022-12-31 13:43:12 -05:00
Greyson Parrelli
6cd6073bc7 Migrate most of MmsSmsTable. 2022-12-30 18:24:00 -05:00
Greyson Parrelli
f149c0adb9 Remove MmsSmsColumns.
All the columns got moved to MessageTable.
I kept the types though and renamed the class to MessageTypes because
it's a lot of boring domain-specific code.
2022-12-30 16:54:49 -05:00
Greyson Parrelli
3708cc5583 Add additional protections around recipientIds and threadIds matching. 2022-12-30 16:54:49 -05:00
Greyson Parrelli
4dd8e81db7 Fix some situations where MessageTable actions were doubled. 2022-12-30 16:54:49 -05:00
Cody Henthorne
06b414f4ef Add call disposition syncing. 2022-12-30 16:54:49 -05:00
Nicholas
d471647e12 Animate swapping of play/pause buttons. 2022-12-30 16:54:49 -05:00
Greyson Parrelli
dd3bad858d Prevent scrolling when context menu is showing on story landing page. 2022-12-30 15:11:57 -05:00
Cody Henthorne
0fe6538ce4 Fix media viewer rail items jumping around while paging. 2022-12-30 15:11:57 -05:00
Alex Hart
1e2f7f0775 Add currency selection logic update. 2022-12-30 15:11:57 -05:00
Cody Henthorne
055b4691d7 Fix video playback starting when off screen in media viewer. 2022-12-30 15:11:57 -05:00
Cody Henthorne
ebdfa88882 Schedule ExpireStoriesManager when viewing Stories tab. 2022-12-30 15:11:57 -05:00
Alex Hart
d79c4775b6 Add chat filter animation. 2022-12-30 15:11:56 -05:00
Cody Henthorne
a13599ae2a Add payment activation capability. 2022-12-30 15:11:56 -05:00
Alex Hart
96b2051400 Rotate chat-filters flag for internal testing. 2022-12-30 15:11:56 -05:00
Nicholas
ad6d1a2e8d Update styling of the media rail selection states. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
eada1e96ee Improve emoji search rankings. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
91fbc236ce Rename pnp capability to pni. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
202f20893c Add internal setting for manually initializing PNP mode. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
f1d204b834 Update SQLCipher to 4.5.1-S1 2022-12-30 15:11:56 -05:00
Greyson Parrelli
73e19209ff Fix some issues with projections result from MessageTable migration. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
835bf3998f Perform large inserts in batches during MessageTable migration. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
d83ef56ab1 Remove SmsMessageRecord. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
a84a9c5381 Fetch isQuoted status in bulk during conversation load.
Improves overall time to load a page of messages by ~50%.
2022-12-30 15:11:56 -05:00
Greyson Parrelli
c6f29fc950 Migrate queued jobs during SMS migration. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
4d9dc42868 Improve the performance of the migration by ~4x. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
32b66643c5 Rename PushMediaSendJob -> IndividualSendJob. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
3850c9c89d Remove isMms from MessageId. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
60ae883df6 Rename SignalDatabase.sms/mms to SignalDatabase.messages 2022-12-30 15:11:56 -05:00
Greyson Parrelli
a7e3bdc892 Rename OutgoingMediaMessage -> OutgoingMessage. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
9b60bd9a4b Remove OutgoingTextMessage and PushTextSendJob. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
e9d98b7d39 Migrate SMS messages into the MMS table. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
cb0e7ade14 Bump version to 6.6.3 2022-12-24 11:13:04 -05:00
Greyson Parrelli
268f5c807d Ensure that remapped records are valid.
Fixes #12691
2022-12-24 11:13:04 -05:00
Greyson Parrelli
f6003023bf Bump version to 6.6.2 2022-12-23 12:23:48 -05:00
Nicholas Tinsley
3f160f256a Prevent crash on pause for Media Preview with no fragments. 2022-12-23 12:20:56 -05:00
Greyson Parrelli
9846517075 Bump version to 6.6.1 2022-12-21 13:40:10 -05:00
Greyson Parrelli
0f1cc03dc0 Updated language translations. 2022-12-21 13:40:10 -05:00
Nicholas Tinsley
0e5031ab45 Revert "Switch to BT mic if available for voice memo recording."
This reverts commit 9f6eb142d2.
2022-12-21 13:25:56 -05:00
Greyson Parrelli
0e4926b5ec Bump version to 6.6.0 2022-12-19 18:42:05 -05:00
Greyson Parrelli
a25e7c6d3e Updated language translations. 2022-12-19 18:38:42 -05:00
Cody Henthorne
4081ac2a83 Fix video controls becoming unresponsive after quickly paging. 2022-12-19 14:30:37 -05:00
Alex Hart
98a528f595 Fix recording progress bar when animations are scaled. 2022-12-19 12:46:13 -04:00
Nicholas
680325b5ee Increase MediaPreviewV2 lifecycle logging. 2022-12-16 16:12:11 -05:00
Nicholas
16668574a9 Separate message for media decode failure. 2022-12-16 15:32:59 -05:00
Rashad Sookram
0d8f6de4c1 Refactor bandwidth mode setting. 2022-12-16 15:22:04 -05:00
Alex Hart
4c0a98d526 Add nullability check to video capture callback.
Fixes #12666
2022-12-16 15:22:04 -05:00
Greyson Parrelli
10f78d5daa Change spinner to lazily read database stuff.
Otherwise you get into situations where Spinner will force DB accesses
super early during Application#onCreate on the main thread, which can be
bad when testing large DB migrations.
2022-12-16 15:22:04 -05:00
Cody Henthorne
3ce5a7da67 Fix emoji toggle behavior when in emoji search mode.
When in emoji search, toggle would be set to "emoji" state or
act like in "emoji" state. Fix is to show "keyboard" state still
when in emoji search.
2022-12-16 15:22:04 -05:00
Greyson Parrelli
4d47b9c594 Round Spinner timings to 3 decimal places. 2022-12-16 15:22:04 -05:00
Nicholas
9f6eb142d2 Switch to BT mic if available for voice memo recording.
Addresses #12016.
2022-12-16 15:22:04 -05:00
Nicholas
0e08b4ee26 Correctly animate deletion when attaching multiple media. 2022-12-16 15:22:04 -05:00
Cody Henthorne
9b85907918 Fix flicker of local avatar in call view. 2022-12-16 15:22:04 -05:00
Cody Henthorne
6463dca2c6 Fix media selection dismissing when deselecting last item. 2022-12-16 15:22:04 -05:00
Alex Hart
498b7fee69 Remove SingleLiveEvent from EditAboutViewModel. 2022-12-16 15:22:04 -05:00
Cody Henthorne
3478e13d38 Fix progress dialog deprecation warnings.
Moves everything under our own class and ignores the deprecation. Also
gives us future ability to re-style all blocking UI dialogs in the
future for mat3 compat.
2022-12-16 15:22:04 -05:00
Alex Hart
5f0d37739a Remove SLE from EditProxyViewModel. 2022-12-16 15:22:04 -05:00
Cody Henthorne
c5b4f44ab8 Fix various compiler warnings. 2022-12-16 15:22:04 -05:00
Alex Hart
819c9f61dc Remove SingleLiveEvent from BlockedUsersActivity. 2022-12-16 15:22:04 -05:00
Alex Hart
4f167feaf5 Handle deprecated connectivity intent filter. 2022-12-16 15:22:04 -05:00
Alex Hart
de558bc87c Remove SingleLiveEvent from ConversationSettingsViewModel. 2022-12-16 15:22:04 -05:00
Alex Hart
4a5a65ff6c Remove usage of SingleLiveEvent from MediaCaptureViewModel. 2022-12-16 15:22:04 -05:00
Cody Henthorne
c56e63d62f Convert OutgoingMediaMessage and it's couterparts to kotlin. 2022-12-16 15:22:04 -05:00
Nicholas
8cd9a3cabe Map platform WIRED_HEADPHONES to our WIRED_HEADSET.
Fixes #12622.
2022-12-16 15:22:04 -05:00
Alex Hart
3a8c324c12 Clean up a bunch of warnings. 2022-12-16 15:22:04 -05:00
Cody Henthorne
ff882edeae Enable kotlin for libsignal-service project and convert SignalServiceDataMessage. 2022-12-16 15:22:04 -05:00
Cody Henthorne
fb0aa55cbb Fix instrumentation tests by forcing channel id usage to init channels. 2022-12-16 15:22:04 -05:00
Alex Hart
51015dc898 Clean up warnings in Gradle file. 2022-12-16 15:22:04 -05:00
Cody Henthorne
4af40e7861 Bump version to 6.5.6 2022-12-16 12:47:55 -05:00
Cody Henthorne
24fcc0c3b0 Updated language translations. 2022-12-16 12:40:19 -05:00
Nicholas Tinsley
993fc24dd3 Change inheritance of MediaPreviewV2Activity. 2022-12-16 12:03:01 -05:00
Greyson Parrelli
fddc6bcd5f Update maven endpoint for sqlcipher. 2022-12-16 12:02:42 -05:00
Cody Henthorne
558051086e Bump version to 6.5.5 2022-12-14 13:33:43 -05:00
Cody Henthorne
2c187bc55d Updated language translations. 2022-12-14 12:59:41 -05:00
Cody Henthorne
e947979169 Revert "Fix view flicker when switching between keyboard and attachment/emoji keyboards."
This reverts commit 1618141342.
2022-12-14 12:53:01 -05:00
Greyson Parrelli
08f1ddb212 Guard against potentially double-running a migration. 2022-12-14 11:15:23 -05:00
Cody Henthorne
4c318d8d82 Bump version to 6.5.4 2022-12-13 16:52:10 -05:00
Cody Henthorne
3e6ebfabb0 Updated language translations. 2022-12-13 16:41:01 -05:00
Alex Hart
55f4692d99 Add logging for response fields when an error happens. 2022-12-13 16:36:36 -05:00
Greyson Parrelli
ebe82cf3e6 Add back missing reaction triggers. 2022-12-13 16:36:36 -05:00
Greyson Parrelli
21a8434e4d Attempt to fix SQLite crash in migration. 2022-12-13 10:59:27 -05:00
Greyson Parrelli
4990778a97 Fix recipient remapping of sms/mms records. 2022-12-13 09:54:53 -05:00
Alex Hart
303e5c7996 Remove PayPal order complete sheet. 2022-12-12 16:05:54 -04:00
Alex Hart
599caee229 Add error handling to re-throw Stripe POST errors. 2022-12-12 15:59:34 -04:00
Cody Henthorne
e6f28c6cdd Bump version to 6.5.3 2022-12-12 12:48:59 -05:00
Cody Henthorne
fd3b0ee375 Updated language translations. 2022-12-12 12:41:54 -05:00
Greyson Parrelli
bd11ed9f17 Fix table drop order during backup import.
Fixes #12671
2022-12-12 11:54:05 -05:00
Alex Hart
a6a185004d Only brighten screen when flash is ON and camera is FRONT. 2022-12-12 12:53:25 -04:00
Alex Hart
3cc556d803 Fix issue with cache entry access. 2022-12-12 12:51:57 -04:00
Alex Hart
c3f9984346 Update error handling to include customized action when user cancels PayPal flow. 2022-12-12 11:54:56 -04:00
Cody Henthorne
10df4ee0d1 Add additional info when backup verification fails. 2022-12-12 10:49:55 -05:00
Greyson Parrelli
c03a183904 Fix transaction issue on backup restore. 2022-12-12 10:03:15 -05:00
Greyson Parrelli
a2893fbec7 Fix possible null column crash in V166 migration.
Fixes #12672
2022-12-12 09:30:49 -05:00
Alex Hart
19cbace33d Fix group search predicate causing crashing when creating group story. 2022-12-12 10:26:22 -04:00
Alex Hart
8a78481cca Bump version to 6.5.2 2022-12-09 14:34:51 -04:00
Alex Hart
e1fd254d15 Updated language translations. 2022-12-09 14:16:58 -04:00
Alex Hart
019219f1e1 Rotate paypal one-time flag. 2022-12-09 14:02:27 -04:00
Greyson Parrelli
ad3c04cb52 Fix ambiguous column in query. 2022-12-09 11:10:30 -05:00
Greyson Parrelli
61f9dc7498 Fix possible issue with reproducible builds.
- Needed to update apkdiff.py to ignore some new app-signing-related
  files.
- While I was in there, I cleaned up the script a lot to make it easier
  to read as well as extract files that didn't match.
- We also need to guarantee git hashes are the same length -- the script
  we were calling might provide hashes of different length depending on
  how you checked out the code.

Co-authored-by: inthewaves<26474149+inthewaves@users.noreply.github.com>
2022-12-09 08:53:17 -05:00
Alex Hart
4deb16a37a Bump version to 6.5.1 2022-12-08 14:20:33 -04:00
Alex Hart
4129151bd2 Updated language translations. 2022-12-08 14:17:26 -04:00
Cody Henthorne
10cf431537 Revert " Enable kotlin for libsignal-service project and convert SignalServiceDataMessage."
This reverts commit fc2b67aa0f.
2022-12-08 13:07:24 -05:00
Alex Hart
011dd2d973 Fix issue where gift receipt showed boost badge. 2022-12-08 13:45:44 -04:00
Alex Hart
c85c4c5020 Bump version to 6.5.0 2022-12-08 12:20:36 -04:00
Alex Hart
5f1439df00 Updated language translations. 2022-12-08 12:11:48 -04:00
Cody Henthorne
e76bec63a3 Remote ring small groups feature flag. 2022-12-08 12:07:02 -04:00
Cody Henthorne
fc2b67aa0f Enable kotlin for libsignal-service project and convert SignalServiceDataMessage. 2022-12-08 12:07:02 -04:00
Alex Hart
bcd0360dd0 Remove obselete unused dexOptions. 2022-12-08 12:07:02 -04:00
Cody Henthorne
04bf2cd0c2 Ignore decomissioned KBS enclaves when encountered during getToken. 2022-12-08 12:07:02 -04:00
Nicholas
aba51da932 Ensure view binding is valid after Media Preview animations. 2022-12-08 12:07:02 -04:00
Nicholas
f8520d83be Add null checks for FABs in conversation list.
Fixes #12651.
2022-12-08 12:07:02 -04:00
Greyson Parrelli
69003dfbe2 Convert IdentityTable to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
380b377ed8 Ensure we rotate storage id when applying hidden story state or username. 2022-12-08 12:07:02 -04:00
fm-sys
4c5db983e3 Make voice messages long-clickable.
Fixes #12658
2022-12-08 12:07:02 -04:00
Greyson Parrelli
48c887ac03 Add gradle test devices. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
f207a82d2f Show smaller quote chains within larger quote chains. 2022-12-08 12:07:02 -04:00
Cody Henthorne
56f6888d49 Update kotlin to 1.7.20 2022-12-08 12:07:02 -04:00
Alex Hart
66ece479f6 Update access modifiers. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
c1cc2b064c Convert SenderKeyTable to kotlin. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
98980b8192 Convert SenderKeySharedTable to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
79ec76f11f Update tooltip to behave better when content is at edge of screen. 2022-12-08 12:07:02 -04:00
Cody Henthorne
45a1c5c369 Fix mention crash with overlapping ranges. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
2dc41f319c Convert RemappedRecordTables to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
2cdb1b8300 Fix issue where story thumb could show as a chat image preview. 2022-12-08 12:07:02 -04:00
Alex Hart
e846b4e20a Add better onBack handling for donations webviews. 2022-12-08 12:07:02 -04:00
Alex Hart
961057f620 Implement PayPal confirm donation sheet. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
e686a09ce4 Convert GroupReceiptTable to kotlin. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
fc8cf2957f Convert DraftTable to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
0bef37bfc1 Add minimum amount error for boosts. 2022-12-07 13:03:02 -05:00
Cody Henthorne
1618141342 Fix view flicker when switching between keyboard and attachment/emoji keyboards. 2022-12-07 13:03:02 -05:00
Alex Hart
d7fb05f596 Fix integration tests. 2022-12-07 13:03:02 -05:00
Greyson Parrelli
2eb15cc8e3 Convert SearchTable to kotlin. 2022-12-07 13:03:02 -05:00
Alex Hart
424a0233c2 Implement refactor to utilize new donation configuration endpoint. 2022-12-07 13:03:02 -05:00
Alex Hart
40cf87307a Add improved handling for credit card errors. 2022-12-07 13:03:02 -05:00
Sgn-32
643206b946 SubmitDebugLogActivity progress dialog make-over.
Fixes #12656
2022-12-07 13:03:02 -05:00
Varsha
cc95041519 Fix navigation after sending payment from conversation. 2022-12-07 13:03:02 -05:00
Cody Henthorne
45b498f62f Remove unused resources. 2022-12-07 13:03:02 -05:00
Sgn-32
9e6d78ba5f Enable hyphenation on conversation settings buttons.
Closes #12609
2022-12-07 13:03:02 -05:00
Greyson Parrelli
95eba78d9c Improve constraints on thread and message tables. 2022-12-07 13:03:02 -05:00
Alex Hart
5d9f00b268 Fix issue when copying attachment data. 2022-12-07 13:03:02 -05:00
Alex Hart
6a01388e82 Ignore start/end clipping when directed to do so by transform properties. 2022-12-07 13:03:02 -05:00
Ehren Kret
2ef6f78d39 Remove some unused code in ConversationAdapter. 2022-12-07 13:03:02 -05:00
Alex Hart
a754c39599 Bump version to 6.4.2 2022-12-07 10:29:19 -04:00
Alex Hart
14622cd06c Updated language translations. 2022-12-07 10:29:02 -04:00
Cody Henthorne
3132cd1198 Drop group call rings for large groups. 2022-12-06 22:21:14 -05:00
Cody Henthorne
94c35d86e2 Update post translation qa tasks. 2022-12-06 15:11:20 -05:00
Cody Henthorne
3c2c6d782a Revert "Clear formatting when pasting text."
This reverts commit 77be721f5a.

If pasting an image will crash the application, does not handle pasting
via multiple other methods like quick suggestion or via a clipboard
manager like provided by Samsung via their keyboard.
2022-12-06 13:38:39 -05:00
Cody Henthorne
1764b21214 Fix crash when opening notification settings. 2022-12-06 13:11:22 -05:00
Greyson Parrelli
260e572071 Fix bug where disappearing timer was applied to sent group stories. 2022-12-05 17:36:57 -05:00
Greyson Parrelli
54251a27a8 Do not show stories for inactive groups. 2022-12-05 17:20:58 -05:00
Alex Hart
88a8430c31 Bump version to 6.4.1 2022-12-02 13:54:13 -04:00
Alex Hart
678b653873 Updated language translations. 2022-12-02 13:50:44 -04:00
Greyson Parrelli
21592ca5c0 Do not include archived messages in unread count. 2022-12-02 12:38:23 -05:00
gitstart
1bca2f06bd Pause voice memos when you open a video.
Fixes #11156.

Signed-off-by: Nicholas Tinsley <nicholas@signal.org>
2022-12-02 10:47:38 -05:00
Alex Hart
9f166105a6 Remove tinting when forwarding content. 2022-12-02 11:04:31 -04:00
Alex Hart
ea08b59e6b Fix error routing for credit cards. 2022-12-02 11:00:22 -04:00
Alex Hart
9aca0af22c Fix issue with poor sent video viewing behavior. 2022-12-02 10:43:31 -04:00
Alex Hart
591d8c3d1a Separate PayPal flags into one-time and recurring. 2022-12-02 09:13:58 -04:00
Nicholas
22b73494a7 Rename *Database androidTest classes to *Table. 2022-12-01 18:15:37 -05:00
Nicholas
9bb80077c6 Fix jumping from media to message in group converstations. 2022-12-01 18:15:09 -05:00
Cody Henthorne
646f41663f Fix in-chat payment message rendering with long note. 2022-12-01 10:20:27 -05:00
Cody Henthorne
63cca2de66 Bump version to 6.4.0 2022-11-30 20:16:51 -05:00
Cody Henthorne
16361ac489 Updated language translations. 2022-11-30 20:05:03 -05:00
Cody Henthorne
e8f39e8f71 Fix in-chat payment view not updating properly. 2022-11-30 19:58:47 -05:00
Alex Hart
7945b3c971 Fix story sync message behaviour between iOS and Android. 2022-11-30 17:10:36 -05:00
Cody Henthorne
e5d196c642 Log backup verify failure independently from file not found. 2022-11-30 17:10:36 -05:00
Alex Hart
979f87db78 Add initial PayPal implementation behind a feature flag. 2022-11-30 17:10:36 -05:00
Alex Hart
b70b4fac91 Inline gift receive flag. 2022-11-30 17:10:36 -05:00
Nicholas
031d7b9cb0 Remove shrinking animation from opening media preview bottom bar. 2022-11-30 17:10:36 -05:00
Nicholas
c68859c606 Convert registration button to Tonal colorway. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
23804046c6 Always use new foreground service utils. 2022-11-30 17:10:36 -05:00
Alex Hart
7b13550086 Add entry points for adding to a group story. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
7949996c5c Renamed database classes to table classes.
Because they're not databases. They're tables.
2022-11-30 17:10:36 -05:00
Nicholas
b190f9495a Only show "Delete Everywhere" with linked devices.
This applies to Note To Self.
2022-11-30 17:10:36 -05:00
Jim Gustafson
b4c0635a63 Update to RingRTC v2.22.0
Co-authored-by: Jordan Rose <jrose@signal.org>
2022-11-30 17:10:36 -05:00
Nicholas
21bd8a308b Add jump to message shortcut for media viewer. 2022-11-30 17:10:36 -05:00
Nicholas
800405fc3e Add background drawable for play/pause buttons. 2022-11-30 17:10:36 -05:00
Alex
bf18db354c Explicitly declare permissions in Github workflows.
Closes #12476

Signed-off-by: Alex <aleksandrosansan@gmail.com>
2022-11-30 17:10:36 -05:00
Greyson Parrelli
e0b89bedd4 Removed some unused log classes. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
504b7ad5b3 Remove unsupported languages. 2022-11-30 17:10:36 -05:00
Nicholas
0558808370 Unmute Stories when ringer mode changed. 2022-11-30 17:10:36 -05:00
Nicholas
cff3840c51 Show AlertDialogs for registration errors. 2022-11-30 17:10:36 -05:00
Nicholas
a46fc96ff1 Improve media album rail entrance animation. 2022-11-30 17:10:36 -05:00
gitstart
77be721f5a Clear formatting when pasting text.
Fixes #8058
Closes #12614
2022-11-30 17:10:36 -05:00
Greyson Parrelli
023b181917 Update backup passphrase layout spacing.
Fixes #12623
2022-11-30 17:10:36 -05:00
Sgn-32
311ef0d65b Fix video call icons in ConversationListItem.
Closes #12618
2022-11-30 17:10:36 -05:00
Cody Henthorne
74314e08ac Show notification when mentioned in a group story reply. 2022-11-30 17:10:36 -05:00
Rashad Sookram
81df9fcddb Default to staging SFU on staging builds. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
ff64c2a911 Add more locking around attachment deletions. 2022-11-30 17:10:36 -05:00
Cody Henthorne
8a9605ade8 Fix crash when handling expired call offers. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
7a449a971f Update rate limit handling for CDS. 2022-11-30 17:10:36 -05:00
Cody Henthorne
258951dea8 Show excluded count in Story privacy settings overview. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
cdff0a61f2 Change chat badge to show total unread message count. 2022-11-30 17:10:36 -05:00
Alex Hart
2200af9c31 Remove background highlighting from empty lines in image editor.
Co-Authored-By: GitStart <1501599+gitstart@users.noreply.github.com>

Fixes #12612
2022-11-30 17:10:36 -05:00
Cody Henthorne
dfb913cb98 Fix thread update with drafts bugs.
* Fix thread not updating correctly when drafts are present.
* Fix thread delete bug during first message drafting.
2022-11-30 17:10:36 -05:00
Varsha
9ee10512fb Update enclave measurements to v3.0.0 for testnet. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
81c10a1eae Lazily initialize NotificationChannels. 2022-11-30 17:10:36 -05:00
Nicholas Tinsley
3e8b5ca91d Allow remote delete from Media Preview menu. 2022-11-30 17:10:35 -05:00
Cody Henthorne
ba0b0cdefa Bump version to 6.3.6 2022-11-30 17:00:07 -05:00
Alex Hart
f00ee0a226 Fix issue preventing subscriptions from processing. 2022-11-30 16:48:22 -05:00
Cody Henthorne
bd4a69eddc Bump version to 6.3.5 2022-11-29 14:08:09 -05:00
Cody Henthorne
8c95b37826 Updated language translations. 2022-11-29 14:05:17 -05:00
Alex Hart
133d3145d1 Fix error with syncing of remote deletion of stories. 2022-11-29 14:48:08 -04:00
Cody Henthorne
4a0db31103 Bump version to 6.3.4 2022-11-29 11:49:32 -05:00
Cody Henthorne
ce85bb1575 Updated language translations. 2022-11-29 11:39:38 -05:00
Alex Hart
eee4ff3f87 Add new error strings for credit cards. 2022-11-29 11:01:07 -04:00
Greyson Parrelli
f6356c9720 Never show stories from blocked users. 2022-11-28 20:40:50 -05:00
Alex Hart
42d2d415d6 Clean up keyboard fragment when view is detached from window. 2022-11-28 13:01:58 -04:00
Alex Hart
683247bf98 Cleanly exit on KeepAlive 409. 2022-11-28 12:47:25 -04:00
Alex Hart
d7404cf32f Prevent empty or all-whitespace string from being sent as a gift message. 2022-11-28 12:02:25 -04:00
Greyson Parrelli
ec1f771364 Bump version to 6.3.3 2022-11-24 22:35:51 -05:00
Greyson Parrelli
95ac9628fb Updated language translations. 2022-11-24 22:35:30 -05:00
Cody Henthorne
ba68d795af Fix megaphone donate crash. 2022-11-24 22:33:14 -05:00
Alex Hart
245f7d3e03 Bump version to 6.3.2 2022-11-18 17:02:44 -04:00
Alex Hart
972ce41689 Updated language translations. 2022-11-18 16:54:16 -04:00
Alex Hart
be12a17ff7 Add handling for payment_intent with missing status. 2022-11-18 13:22:30 -04:00
Alex Hart
0c615e2fc2 Bump version to 6.3.1 2022-11-17 16:43:49 -04:00
Alex Hart
6829257a83 Updated language translations. 2022-11-17 16:39:38 -04:00
Nicholas
b7b7a04fad Improve animations for video seekbar. 2022-11-17 15:33:15 -05:00
Cody Henthorne
50084f8f73 Fix debuglog system info formatting bug. 2022-11-17 11:54:51 -05:00
Alex Hart
04e8235cfc Add group stories education sheet. 2022-11-17 12:35:17 -04:00
Alex Hart
0df3096241 Fix issue where gallery image was overlapped by count. 2022-11-17 12:18:32 -04:00
Alex Hart
29f22d515a Set story image post minimum duration to 5s. 2022-11-17 12:13:07 -04:00
Alex Hart
9931496b0f Fix crash when toggling pills. 2022-11-17 12:06:36 -04:00
Alex Hart
950363a4e9 Don't wrap donation errors. 2022-11-17 11:07:20 -04:00
Alex Hart
3469e8d0e0 Set brightness to 66% when taking a selfie. 2022-11-17 10:02:02 -04:00
Alex Hart
586339575f Fix menu visibility for chat filters. 2022-11-16 16:53:27 -04:00
Varsha
807a0e02a2 Fix memory leak in payment transfer fragment. 2022-11-16 15:11:09 -05:00
Cody Henthorne
afb2b1a1a2 Do not include self in exported SMS threads. 2022-11-16 14:18:57 -05:00
Alex Hart
a8946961d5 Bump version to 6.3.0 2022-11-16 15:14:49 -04:00
Alex Hart
026aaac451 Updated language translations. 2022-11-16 15:10:26 -04:00
Alex Hart
159f319d77 Update caption bar readability in stories. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
cf00995b6f Guarantee table export order is valid. 2022-11-16 15:05:47 -04:00
Cody Henthorne
7c60c32918 Add re-export SMS support and hard code Phase 0. 2022-11-16 15:05:47 -04:00
Cody Henthorne
fd1d2ec8fc Ignore group ring requests if we are already in the call. 2022-11-16 15:05:47 -04:00
Alex Hart
a11c40e4fe Add credit card support to badge gifting. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
1eb2f51398 Convert AVIF files to jpegs. 2022-11-16 15:05:47 -04:00
Nicholas
13ed122c3e Null check RecyclerView references in search bar callbacks. 2022-11-16 15:05:47 -04:00
Alex Hart
fa02ee1d3d Skip re-emission of duplicate StoryPosts. 2022-11-16 15:05:47 -04:00
Alex Hart
4908e39308 Skip prefetch call if no stories need to be cached. 2022-11-16 15:05:47 -04:00
Alex Hart
ad001d585e Utilize center-inside transform to ensure proper downsampling of cached images. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
3fd5e55363 Improve RecipientDatabase tests. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
ebc1bc3f7f Fix issue where non-ascii characters didn't show inline emoji suggestions.
Fixes #12579
2022-11-16 15:05:47 -04:00
Cody Henthorne
c51e13fd30 Ignore rings from non-admins in announcement only groups and rev feature flag. 2022-11-16 15:05:47 -04:00
Nicholas
fd37613f2f Don't fade in media preview controls if hidden. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
eb921f3103 Don't show megaphones in landscape. 2022-11-16 15:05:47 -04:00
Varsha
d5b6c47670 Fix memory leak in payments home. 2022-11-16 15:05:47 -04:00
Varsha
a4494b58f0 Fix memory leaks in payments home and confirm payment view models. 2022-11-16 15:05:47 -04:00
Varsha
b0c68b12ed Fix memory leak in create payment fragment. 2022-11-16 15:05:47 -04:00
Varsha
b47e5f2fa9 Fix memory leak in contact selection list. 2022-11-16 15:05:47 -04:00
Alex Hart
bba1315906 Add chat filter support behind a flag. 2022-11-16 15:05:47 -04:00
Alex Hart
3e2ecdaaa9 Add blur hashes behind videos. 2022-11-15 16:26:19 -04:00
Nicholas
fb8e81cf50 Center selected item in media rail.
Fixes #12582
2022-11-15 16:26:19 -04:00
Cody Henthorne
52a5fb8ea2 Fix crash when showing a message with a button without media. 2022-11-15 16:26:19 -04:00
Alex Hart
b2f3867b0b Add dynamic duration to stories with captions. 2022-11-15 16:26:19 -04:00
Alex Hart
45ca3bd7cf Show default gallery icon if permissions is disabled or media is not available. 2022-11-15 16:26:19 -04:00
Alex Hart
74b7057608 Brighten camera screen if under 66%. 2022-11-15 16:26:19 -04:00
Robotwombat
3a060c7a79 Update some info on the README.
* Removed the mention of SMS/MMS support.
* Replaced the Signal description with some direct text from either Signal's Play Store listing or from signal.org
* Fixed some capitalization errors
* Replaced "Open Whisper Systems" with "Signal" in the 'Contributing Ideas' section

Closes #12597
2022-11-15 16:26:19 -04:00
Jim Gustafson
de426d22bf Update to RingRTC v2.21.5 2022-11-15 16:26:19 -04:00
Alex Hart
14549fd401 Fix issue where SystemWindwInsetsSetter didn't respect type on older API levels. 2022-11-15 16:26:19 -04:00
Alex Hart
1ff16a2c18 Bump version to 6.2.3 2022-11-15 16:08:06 -04:00
Alex Hart
0174af7b9b Updated language translations. 2022-11-15 16:06:48 -04:00
Alex Hart
e7f1d3fc1a Add JsonCreator annotation to data class constructors. 2022-11-15 15:14:55 -04:00
3058 changed files with 229766 additions and 137968 deletions

View File

@@ -2,3 +2,4 @@ root = true
[*.kt]
indent_size = 2
twitter_compose_allowed_composition_locals=LocalExtendedColors

View File

@@ -8,10 +8,13 @@ on:
- '4.**'
- '5.**'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v3
@@ -21,15 +24,13 @@ jobs:
with:
distribution: temurin
java-version: 11
cache: gradle
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Remove Android 31 (S)
run: $ANDROID_HOME/tools/bin/sdkmanager --uninstall "platforms;android-31"
- name: Build with Gradle
run: ./gradlew qa
run: ./gradlew qa --parallel
- name: Archive reports for failed build
if: ${{ failure() }}

View File

@@ -4,6 +4,9 @@ on:
schedule:
- cron: '0 5 * * *'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
build:

1
.gitignore vendored
View File

@@ -28,3 +28,4 @@ jni/libspeex/.deps/
pkcs11.password
dev.keystore
maps.key
local/

View File

@@ -41,16 +41,6 @@
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
</JetCodeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />

6
.idea/copyright/Signal.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright &amp;#36;today.year Signal Messenger, LLC&#10;SPDX-License-Identifier: AGPL-3.0-only" />
<option name="myName" value="Signal" />
</copyright>
</component>

7
.idea/copyright/profiles_settings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="CopyrightManager">
<settings>
<module2copyright>
<element module="All" copyright="Signal" />
</module2copyright>
</settings>
</component>

View File

@@ -0,0 +1,9 @@
/*
* Copyright ${YEAR} Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")
public @interface ${NAME} {
}

9
.idea/fileTemplates/internal/Class.java generated Normal file
View File

@@ -0,0 +1,9 @@
/*
* Copyright ${YEAR} Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")
public class ${NAME} {
}

9
.idea/fileTemplates/internal/Enum.java generated Normal file
View File

@@ -0,0 +1,9 @@
/*
* Copyright ${YEAR} Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")
public enum ${NAME} {
}

View File

@@ -0,0 +1,9 @@
/*
* Copyright ${YEAR} Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")
public interface ${NAME} {
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright ${YEAR} Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
#parse("File Header.java")
class ${NAME} {
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright ${YEAR} Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
#parse("File Header.java")
enum class ${NAME} {
}

View File

@@ -0,0 +1,9 @@
/*
* Copyright ${YEAR} Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
#parse("File Header.java")

View File

@@ -0,0 +1,11 @@
/*
* Copyright ${YEAR} Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
#parse("File Header.java")
interface ${NAME} {
}

152
LICENSE
View File

@@ -1,23 +1,21 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -72,7 +60,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -619,3 +617,45 @@ Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View File

@@ -1,10 +1,10 @@
# Signal Android
Signal is a messaging app for simple private communication with friends.
Signal is a simple, powerful, and secure messenger.
Signal uses your phone's data connection (WiFi/3G/4G) to communicate securely, optionally supports plain SMS/MMS to function as a unified messenger, and can also encrypt the stored messages on your phone.
Signal uses your phone's data connection (WiFi/3G/4G/5G) to communicate securely. Millions of people use Signal every day for free and instantaneous communication anywhere in the world. Send and receive high-fidelity messages, participate in HD voice/video calls, and explore a growing set of new features that help you stay connected. Signals advanced privacy-preserving technology is always enabled, so you can focus on sharing the moments that matter with the people who matter to you.
Currently available on the Play store and [signal.org](https://signal.org/android/apk/).
Currently available on the Play Store and [signal.org](https://signal.org/android/apk/).
<a href='https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height='80px'/></a>
@@ -18,7 +18,7 @@ Want to live life on the bleeding edge and help out with testing?
You can subscribe to Signal Android Beta releases here:
https://play.google.com/apps/testing/org.thoughtcrime.securesms
If you're interested in a life of peace and tranquility, stick with the standard releases.
## Contributing Code
@@ -28,7 +28,7 @@ If you're new to the Signal codebase, we recommend going through our issues and
For larger changes and feature ideas, we ask that you propose it on the [unofficial Community Forum](https://community.signalusers.org) for a high-level discussion with the wider community before implementation.
## Contributing Ideas
Have something you want to say about Open Whisper Systems projects or want to be part of the conversation? Get involved in the [community forum](https://community.signalusers.org).
Have something you want to say about Signal projects or want to be part of the conversation? Get involved in the [community forum](https://community.signalusers.org).
Help
====
@@ -54,8 +54,8 @@ The form and manner of this distribution makes it eligible for export under the
## License
Copyright 2013-2022 Signal
Copyright 2013-2023 Signal
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
Google Play and the Google Play logo are trademarks of Google LLC.

View File

@@ -1,35 +1,21 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.google.protobuf'
apply plugin: 'androidx.navigation.safeargs'
apply plugin: 'org.jlleitschuh.gradle.ktlint'
apply from: 'translations.gradle'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'app.cash.exhaustive'
apply plugin: 'kotlin-parcelize'
apply from: 'static-ips.gradle'
import com.android.build.api.dsl.ManagedVirtualDevice
repositories {
maven {
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
content {
includeGroupByRegex "org\\.signal.*"
}
}
google()
mavenCentral()
mavenLocal()
maven {
url "https://dl.cloudsmith.io/qxAgwaeEE1vN8aLU/mobilecoin/mobilecoin/maven/"
}
jcenter {
content {
includeVersion "mobi.upod", "time-duration-picker", "1.1.3"
}
}
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.protobuf'
id 'androidx.navigation.safeargs'
id 'org.jlleitschuh.gradle.ktlint'
id 'org.jetbrains.kotlin.android'
id 'app.cash.exhaustive'
id 'kotlin-parcelize'
id 'com.squareup.wire'
id 'android-constants'
id 'translations'
}
apply from: 'static-ips.gradle'
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.18.0'
@@ -45,13 +31,23 @@ protobuf {
}
}
ktlint {
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
version = "0.43.2"
wire {
kotlin {
javaInterop = true
}
sourcePath {
srcDir 'src/main/protowire'
}
}
def canonicalVersionCode = 1163
def canonicalVersionName = "6.2.2"
ktlint {
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
version = "0.47.1"
}
def canonicalVersionCode = 1269
def canonicalVersionName = "6.22.2"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
@@ -66,37 +62,43 @@ def selectableVariants = [
'nightlyProdSpinner',
'nightlyProdPerf',
'nightlyProdRelease',
'nightlyStagingRelease',
'nightlyPnpPerf',
'nightlyPnpRelease',
'playProdDebug',
'playProdSpinner',
'playProdCanary',
'playProdPerf',
'playProdBenchmark',
'playProdInstrumentation',
'playProdRelease',
'playStagingDebug',
'playStagingCanary',
'playStagingSpinner',
'playStagingPerf',
'playStagingInstrumentation',
'playPnpDebug',
'playPnpSpinner',
'playStagingRelease',
'websiteProdSpinner',
'websiteProdRelease',
]
android {
buildToolsVersion BUILD_TOOL_VERSION
compileSdkVersion COMPILE_SDK
namespace 'org.thoughtcrime.securesms'
buildToolsVersion = signalBuildToolsVersion
compileSdkVersion = signalCompileSdkVersion
flavorDimensions 'distribution', 'environment'
useLibrary 'org.apache.http.legacy'
testBuildType 'instrumentation'
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "11"
freeCompilerArgs = ["-Xallow-result-return-type"]
}
dexOptions {
javaMaxHeapSize "4g"
}
signingConfigs {
if (keystores.debug != null) {
debug {
@@ -114,14 +116,19 @@ android {
unitTests {
includeAndroidResources = true
}
managedDevices {
devices {
pixel3api30 (ManagedVirtualDevice) {
device = "Pixel 3"
apiLevel = 30
systemImageSource = "google-atd"
require64Bit = false
}
}
}
}
lintOptions {
checkReleaseBuilds false
abortOnError true
baseline file("lint-baseline.xml")
disable "LintError"
}
sourceSets {
test {
@@ -135,37 +142,37 @@ android {
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JAVA_VERSION
targetCompatibility JAVA_VERSION
sourceCompatibility signalJavaVersion
targetCompatibility signalJavaVersion
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
exclude 'libsignal_jni.dylib'
exclude 'signal_jni.dll'
resources {
excludes += ['LICENSE.txt', 'LICENSE', 'NOTICE', 'asm-license.txt', 'META-INF/LICENSE', 'META-INF/LICENSE.md', 'META-INF/NOTICE', 'META-INF/LICENSE-notice.md', 'META-INF/proguard/androidx-annotations.pro', 'libsignal_jni.dylib', 'signal_jni.dll']
}
}
buildFeatures {
viewBinding true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = '1.3.2'
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion MINIMUM_SDK
targetSdkVersion TARGET_SDK
minSdkVersion signalMinSdkVersion
targetSdkVersion signalTargetSdkVersion
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "Signal");
project.ext.set("archivesBaseName", "Signal")
manifestPlaceholders = [mapsKey:"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"]
@@ -175,11 +182,12 @@ android {
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.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.staging.signal.org\""
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\""
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}"
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\", \"https://sfu.staging.test.voip.signal.org\"}"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
@@ -188,13 +196,12 @@ android {
buildConfigField "String[]", "SIGNAL_STORAGE_IPS", storage_ips
buildConfigField "String[]", "SIGNAL_CDN_IPS", cdn_ips
buildConfigField "String[]", "SIGNAL_CDN2_IPS", cdn2_ips
buildConfigField "String[]", "SIGNAL_CDS_IPS", cds_ips
buildConfigField "String[]", "SIGNAL_KBS_IPS", kbs_ips
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDS_MRENCLAVE", "\"74778bb0f93ae1f78c26e67152bab0bbeb693cd56d1bb9b4e9244157acc58081\""
buildConfigField "String", "CDSI_MRENCLAVE", "\"ef4787a56a154ac6d009138cac17155acd23cfe4329281252365dd7c252e7fbf\""
buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\""
buildConfigField "String", "SVR2_MRENCLAVE", "\"dc9fd472a5a9c871a3c7f76f1af60aa9c1f314abf2e8d1e0c4ba25c8aaa2848c\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
"\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
@@ -203,6 +210,7 @@ android {
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\""
buildConfigField "String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
@@ -215,6 +223,7 @@ android {
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
buildConfigField "String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\""
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\""
buildConfigField "boolean", "TRACING_ENABLED", "false"
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
@@ -224,7 +233,7 @@ android {
splits {
abi {
enable true
enable !project.hasProperty('generateBaselineProfile')
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
@@ -256,6 +265,7 @@ android {
'proguard/proguard-retrofit.pro',
'proguard/proguard-webrtc.pro',
'proguard/proguard-klinker.pro',
'proguard/proguard-mobilecoin.pro',
'proguard/proguard-retrolambda.pro',
'proguard/proguard-okhttp.pro',
'proguard/proguard-ez-vcard.pro',
@@ -285,11 +295,13 @@ android {
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Spinner\""
}
release {
minifyEnabled true
proguardFiles = buildTypes.debug.proguardFiles
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Release\""
}
perf {
initWith debug
isDefault false
@@ -297,6 +309,25 @@ android {
minifyEnabled true
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
buildConfigField "boolean", "TRACING_ENABLED", "true"
}
benchmark {
initWith debug
isDefault false
debuggable false
minifyEnabled true
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Benchmark\""
buildConfigField "boolean", "TRACING_ENABLED", "true"
}
canary {
initWith debug
isDefault false
minifyEnabled false
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Canary\""
}
}
@@ -345,10 +376,8 @@ android {
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.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", "CDS_MRENCLAVE", "\"74778bb0f93ae1f78c26e67152bab0bbeb693cd56d1bb9b4e9244157acc58081\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
@@ -357,6 +386,7 @@ android {
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\""
buildConfigField "String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\""
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\""
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
@@ -364,6 +394,22 @@ android {
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\""
}
pnp {
dimension 'environment'
initWith staging
applicationIdSuffix ".pnp"
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Pnp\""
}
}
lint {
abortOnError true
baseline file('lint-baseline.xml')
checkReleaseBuilds false
disable 'LintError'
}
android.applicationVariants.all { variant ->
@@ -402,7 +448,6 @@ android {
}
dependencies {
implementation libs.androidx.core.ktx
implementation libs.androidx.fragment.ktx
lintChecks project(':lintchecks')
@@ -410,7 +455,7 @@ dependencies {
implementation (libs.androidx.appcompat) {
version {
strictly '1.5.1'
strictly '1.6.1'
}
}
implementation libs.androidx.window.window
@@ -418,11 +463,11 @@ dependencies {
implementation libs.androidx.recyclerview
implementation libs.material.material
implementation libs.androidx.legacy.support
implementation libs.androidx.cardview
implementation libs.androidx.preference
implementation libs.androidx.legacy.preference
implementation libs.androidx.gridlayout
implementation libs.androidx.exifinterface
implementation libs.androidx.compose.rxjava3
implementation libs.androidx.constraintlayout
implementation libs.androidx.multidex
implementation libs.androidx.navigation.fragment.ktx
@@ -441,6 +486,9 @@ dependencies {
implementation libs.androidx.autofill
implementation libs.androidx.biometric
implementation libs.androidx.sharetarget
implementation libs.androidx.profileinstaller
implementation libs.androidx.asynclayoutinflater
implementation libs.androidx.asynclayoutinflater.appcompat
implementation (libs.firebase.messaging) {
exclude group: 'com.google.firebase', module: 'firebase-core'
@@ -477,12 +525,6 @@ dependencies {
exclude group: 'com.google.protobuf'
}
implementation(libs.signal.argon2) {
artifact {
type = "aar"
}
}
implementation libs.signal.ringrtc
implementation libs.leolin.shortcutbadger
@@ -495,7 +537,6 @@ dependencies {
implementation libs.greenrobot.eventbus
implementation libs.waitingdots
implementation libs.google.zxing.android.integration
implementation libs.time.duration.picker
implementation libs.google.zxing.core
implementation libs.google.flexbox
implementation (libs.subsampling.scale.image.view) {
@@ -520,9 +561,12 @@ dependencies {
exclude group: 'org.freemarker'
}
implementation libs.dnsjava
implementation libs.kotlinx.collections.immutable
implementation libs.accompanist.permissions
spinnerImplementation project(":spinner")
spinnerImplementation libs.square.leakcanary
canaryImplementation libs.square.leakcanary
testImplementation testLibs.junit.junit
testImplementation testLibs.assertj.core
@@ -549,6 +593,7 @@ dependencies {
androidTestImplementation testLibs.androidx.test.ext.junit.ktx
androidTestImplementation testLibs.mockito.android
androidTestImplementation testLibs.mockito.kotlin
androidTestImplementation testLibs.mockk.android
androidTestImplementation testLibs.square.okhttp.mockserver
instrumentationImplementation (libs.androidx.fragment.testing) {
@@ -566,6 +611,9 @@ dependencies {
implementation libs.rxdogtag
androidTestUtil testLibs.androidx.test.orchestrator
implementation project(':core-ui')
ktlintRuleset libs.ktlint.twitter.compose
}
def getLastCommitTimestamp() {
@@ -574,7 +622,7 @@ def getLastCommitTimestamp() {
}
new ByteArrayOutputStream().withStream { os ->
def result = exec {
exec {
executable = 'git'
args = ['log', '-1', '--pretty=format:%ct']
standardOutput = os
@@ -586,20 +634,20 @@ def getLastCommitTimestamp() {
def getGitHash() {
if (!(new File('.git').exists())) {
return "abcd1234"
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
}
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
commandLine 'git', 'rev-parse', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim()
return stdout.toString().trim().substring(0, 12)
}
def getCurrentGitTag() {
if (!(new File('.git').exists())) {
return ''
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
}
def stdout = new ByteArrayOutputStream()
@@ -633,13 +681,13 @@ def loadKeystoreProperties(filename) {
if (keystorePropertiesFile.exists()) {
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
return keystoreProperties;
return keystoreProperties
} else {
return null;
return null
}
}
def getDateSuffix() {
static def getDateSuffix() {
def date = new Date()
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
return formattedDate

View File

@@ -0,0 +1,2 @@
# MobileCoin
-keep class com.mobilecoin.** { *; }

View File

@@ -1,15 +1,40 @@
package org.thoughtcrime.securesms
import org.signal.core.util.concurrent.SignalExecutors
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.ApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
import org.thoughtcrime.securesms.logging.PersistentLogger
import org.thoughtcrime.securesms.testing.InMemoryLogger
/**
* Application context for running instrumentation tests (aka androidTests).
*/
class SignalInstrumentationApplicationContext : ApplicationContext() {
val inMemoryLogger: InMemoryLogger = InMemoryLogger()
override fun initializeAppDependencies() {
val default = ApplicationDependencyProvider(this)
ApplicationDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
ApplicationDependencies.getDeadlockDetector().start()
}
override fun initializeLogging() {
persistentLogger = PersistentLogger(this)
Log.initialize({ true }, AndroidLogger(), persistentLogger, inMemoryLogger)
SignalProtocolLoggerProvider.setProvider(CustomSignalProtocolLogger())
SignalExecutors.UNBOUNDED.execute {
Log.blockUntilAllWritesFinished()
LogDatabase.getInstance(this).trimToSize()
}
}
}

View File

@@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.components.settings.app.changenumber
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -17,9 +19,10 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.MockProvider
import org.thoughtcrime.securesms.testing.Post
import org.thoughtcrime.securesms.testing.Put
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
@@ -80,8 +83,10 @@ class ChangeNumberViewModelTest {
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { r ->
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
},
@@ -93,6 +98,7 @@ class ChangeNumberViewModelTest {
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
// THEN
@@ -110,12 +116,15 @@ class ChangeNumberViewModelTest {
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { MockResponse().failure(500) },
Put("/v2/accounts/number") { MockResponse().failure(500) }
)
// WHEN
val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs true
@@ -140,13 +149,16 @@ class ChangeNumberViewModelTest {
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { MockResponse().connectionFailure() },
Put("/v2/accounts/number") { MockResponse().connectionFailure() },
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) }
)
// WHEN
val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
@@ -166,6 +178,8 @@ class ChangeNumberViewModelTest {
* and apply the pending state after confirming the change on the server.
*/
@Test
@FlakyTest
@Ignore("Test sometimes requires manual intervention to continue.")
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() {
// GIVEN
val aci = Recipient.self().requireServiceId()
@@ -177,8 +191,10 @@ class ChangeNumberViewModelTest {
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { r ->
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
MockResponse().timeout()
},
@@ -191,7 +207,8 @@ class ChangeNumberViewModelTest {
)
// WHEN
val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
@@ -221,8 +238,10 @@ class ChangeNumberViewModelTest {
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { r ->
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
@@ -238,6 +257,7 @@ class ChangeNumberViewModelTest {
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
processor.registrationLock() assertIs true
Recipient.self().requirePni() assertIsNot newPni
@@ -259,8 +279,10 @@ class ChangeNumberViewModelTest {
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { r ->
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.deviceMessages.isEmpty()) {
MockResponse().failure(
@@ -285,6 +307,7 @@ class ChangeNumberViewModelTest {
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
// THEN
@@ -303,7 +326,9 @@ class ChangeNumberViewModelTest {
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/accounts/number") { r ->
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
@@ -341,6 +366,7 @@ class ChangeNumberViewModelTest {
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
processor.registrationLock() assertIs true
Recipient.self().requirePni() assertIsNot newPni

View File

@@ -9,11 +9,8 @@ import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
@@ -72,10 +69,10 @@ class ConversationItemPreviewer {
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = System.currentTimeMillis(),
receivedTimeMillis = System.currentTimeMillis(),
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
attachments = PointerAttachment.forPointers(Optional.of(attachments))
)
SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
ThreadUtil.sleep(1)
}
@@ -91,10 +88,10 @@ class ConversationItemPreviewer {
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = System.currentTimeMillis(),
receivedTimeMillis = System.currentTimeMillis(),
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
attachments = PointerAttachment.forPointers(Optional.of(attachments))
)
val insert = SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
val insert = SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
SignalDatabase.attachments.getAttachmentsForMessage(insert.messageId).forEachIndexed { index, attachment ->
// if (index != 1) {
@@ -112,29 +109,16 @@ class ConversationItemPreviewer {
attachment()
}
val message = OutgoingMediaMessage(
other,
body,
PointerAttachment.forPointers(Optional.of(attachments)),
System.currentTimeMillis(),
-1,
0,
false,
ThreadDatabase.DistributionTypes.DEFAULT,
StoryType.NONE,
null,
false,
null,
emptyList(),
emptyList(),
emptyList(),
emptySet(),
emptySet(),
null
val message = OutgoingMessage(
recipient = other,
body = body,
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
timestamp = System.currentTimeMillis(),
isSecure = true
)
val insert = SignalDatabase.mms.insertMessageOutbox(
OutgoingSecureMediaMessage(message),
val insert = SignalDatabase.messages.insertMessageOutbox(
message,
SignalDatabase.threads.getOrCreateThreadIdFor(other),
false,
null

View File

@@ -7,7 +7,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.IdentityDatabase
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
@@ -32,7 +32,7 @@ class SafetyNumberChangeDialogPreviewer {
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("Super really long name like omg", "But seriously it's long like really really long"))
harness.setVerified(other, IdentityDatabase.VerifiedStatus.VERIFIED)
harness.setVerified(other, IdentityTable.VerifiedStatus.VERIFIED)
harness.changeIdentityKey(other)
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
@@ -52,7 +52,7 @@ class SafetyNumberChangeDialogPreviewer {
othersRecipients.forEach { other ->
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("My", "Name"))
harness.setVerified(other, IdentityDatabase.VerifiedStatus.DEFAULT)
harness.setVerified(other, IdentityTable.VerifiedStatus.DEFAULT)
harness.changeIdentityKey(other)
SignalDatabase.distributionLists.addMemberToList(DistributionListId.MY_STORY, DistributionListPrivacyMode.ONLY_WITH, other.id)
@@ -64,7 +64,7 @@ class SafetyNumberChangeDialogPreviewer {
SafetyNumberBottomSheet
.forIdentityRecordsAndDestinations(
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
destinations = listOf(ContactSearchKey.RecipientSearchKey.Story(myStoryRecipientId))
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
)
.show(conversationActivity.supportFragmentManager)
}

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
import android.net.Uri
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
@@ -15,7 +16,7 @@ import org.thoughtcrime.securesms.util.MediaUtil
import java.util.Optional
@RunWith(AndroidJUnit4::class)
class AttachmentDatabaseTest {
class AttachmentTableTest {
@Before
fun setUp() {
@@ -34,12 +35,13 @@ class AttachmentDatabaseTest {
assertEquals(attachment2.fileName, attachment.fileName)
}
@FlakyTest
@Test
fun givenABlobAndDifferentTransformQuality_whenIInsert2AttachmentsForPreUpload_thenIExpectDifferentFileInfos() {
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
val highQualityProperties = createHighQualityTransformProperties()
val highQualityImage = createAttachment(1, blob, highQualityProperties)
val lowQualityImage = createAttachment(1, blob, AttachmentDatabase.TransformProperties.empty())
val lowQualityImage = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(lowQualityImage)
@@ -55,12 +57,13 @@ class AttachmentDatabaseTest {
false
)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentDatabase.DATA)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentDatabase.DATA)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
assertNotEquals(attachment1Info, attachment2Info)
}
@FlakyTest
@Test
fun givenIdenticalAttachmentsInsertedForPreUpload_whenIUpdateAttachmentDataAndSpecifyOnlyModifyThisAttachment_thenIExpectDifferentFileInfos() {
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
@@ -81,13 +84,13 @@ class AttachmentDatabaseTest {
true
)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentDatabase.DATA)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentDatabase.DATA)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
assertNotEquals(attachment1Info, attachment2Info)
}
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentDatabase.TransformProperties): UriAttachment {
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties): UriAttachment {
return UriAttachmentBuilder.build(
id,
uri = uri,
@@ -96,8 +99,8 @@ class AttachmentDatabaseTest {
)
}
private fun createHighQualityTransformProperties(): AttachmentDatabase.TransformProperties {
return AttachmentDatabase.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH)
private fun createHighQualityTransformProperties(): AttachmentTable.TransformProperties {
return AttachmentTable.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH)
}
private fun createMediaStream(byteArray: ByteArray): MediaStream {

View File

@@ -0,0 +1,750 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.ringrtc.CallId
import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.testing.SignalActivityRule
@RunWith(AndroidJUnit4::class)
class CallTableTest {
@get:Rule
val harness = SignalActivityRule()
@Test
fun givenACall_whenISetTimestamp_thenIExpectUpdatedTimestamp() {
val callId = 1L
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
SignalDatabase.calls.setTimestamp(callId, harness.others[0], -1L)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(-1L, call?.timestamp)
val messageRecord = SignalDatabase.messages.getMessageRecord(call!!.messageId!!)
assertEquals(-1L, messageRecord.dateReceived)
assertEquals(-1L, messageRecord.dateSent)
}
@Test
fun givenPreExistingEvent_whenIDeleteGroupCall_thenIMarkDeletedAndSetTimestamp() {
val callId = 1L
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
SignalDatabase.calls.deleteGroupCall(call!!)
val deletedCall = SignalDatabase.calls.getCallById(callId, harness.others[0])
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
assertEquals(CallTable.Event.DELETE, deletedCall?.event)
assertNotEquals(0L, oldestDeletionTimestamp)
assertNull(deletedCall!!.messageId)
}
@Test
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
val callId = 1L
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
callId,
harness.others[0],
CallTable.Direction.OUTGOING,
System.currentTimeMillis()
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
assertEquals(CallTable.Event.DELETE, call?.event)
assertNotEquals(oldestDeletionTimestamp, 0)
assertNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenIInsertAcceptedOutgoingGroupCall_thenIExpectLocalRingerAndOutgoingRing() {
val callId = 1L
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.OUTGOING,
System.currentTimeMillis()
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
assertEquals(harness.self.id, call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenIInsertAcceptedIncomingGroupCall_thenIExpectJoined() {
val callId = 1L
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
System.currentTimeMillis()
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.JOINED, call?.event)
assertNull(call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenARingingCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = harness.others[0],
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.RINGING, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenAMissedCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = harness.others[0],
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.EXPIRED_REQUEST
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenADeclinedCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = harness.others[0],
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.DECLINED, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenAGenericGroupCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = System.currentTimeMillis(),
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertEquals(CallTable.Event.JOINED, acceptedCall?.event)
}
@Test
fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = System.currentTimeMillis(),
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
}
@Test
fun givenAPriorCallEventWithNewerTimestamp_whenIReceiveAGroupCallUpdateMessage_thenIExpectAnUpdatedTimestamp() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.getCallById(callId, harness.others[0]).let {
assertNotNull(it)
assertEquals(now, it?.timestamp)
}
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = 1L,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
assertEquals(1L, call?.timestamp)
}
@Test
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
val callId = 1L
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
callId = callId,
recipientId = harness.others[0],
direction = CallTable.Direction.INCOMING,
timestamp = System.currentTimeMillis()
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = harness.others[0],
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.DELETE, call?.event)
}
@Test
fun givenAGenericCallEvent_whenRingRequested_thenISetRingerAndMoveToRingingState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.RINGING, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
}
@Test
fun givenAJoinedCallEvent_whenRingRequested_thenISetRingerAndMoveToRingingState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
}
@Test
fun givenAGenericCallEvent_whenRingExpired_thenISetRingerAndMoveToMissedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.EXPIRED_REQUEST
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
}
@Test
fun givenARingingCallEvent_whenRingExpired_thenISetRingerAndMoveToMissedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.EXPIRED_REQUEST
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
}
@Test
fun givenAJoinedCallEvent_whenRingIsCancelledBecauseUserIsBusyLocally_thenIMoveToAcceptedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_LOCALLY
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
}
@Test
fun givenAJoinedCallEvent_whenRingIsCancelledBecauseUserIsBusyOnAnotherDevice_thenIMoveToAcceptedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.INCOMING,
now
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
}
@Test
fun givenARingingCallEvent_whenRingCancelledBecauseUserIsBusyLocally_thenIMoveToMissedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_LOCALLY
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
}
@Test
fun givenARingingCallEvent_whenRingCancelledBecauseUserIsBusyOnAnotherDevice_thenIMoveToMissedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
}
@Test
fun givenACallEvent_whenRingIsAcceptedOnAnotherDevice_thenIMoveToAcceptedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
val now = System.currentTimeMillis()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = harness.others[0],
sender = harness.others[1],
timestamp = now,
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
}
@Test
fun givenARingingCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.DECLINED, call?.event)
}
@Test
fun givenAMissedCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.EXPIRED_REQUEST
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.DECLINED, call?.event)
}
@Test
fun givenAnOutgoingRingCallEvent_whenRingDeclinedOnAnotherDevice_thenIDoNotChangeState() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertAcceptedGroupCall(
callId,
harness.others[0],
CallTable.Direction.OUTGOING,
System.currentTimeMillis()
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
}
@Test
fun givenNoPriorEvent_whenRingRequested_thenICreateAnEventInTheRingingStateAndSetRinger() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.RINGING, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingExpired_thenICreateAnEventInTheMissedStateAndSetRinger() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.EXPIRED_REQUEST
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingCancelledByRinger_thenICreateAnEventInTheMissedStateAndSetRinger() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.CANCELLED_BY_RINGER
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertEquals(harness.others[1], call?.ringerRecipient)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyLocally_thenICreateAnEventInTheMissedState() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_LOCALLY
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyOnAnotherDevice_thenICreateAnEventInTheMissedState() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingAcceptedOnAnotherDevice_thenICreateAnEventInTheAcceptedState() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.ACCEPTED, call?.event)
assertNotNull(call?.messageId)
}
@Test
fun givenNoPriorEvent_whenRingDeclinedOnAnotherDevice_thenICreateAnEventInTheDeclinedState() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
callId,
harness.others[0],
harness.others[1],
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, harness.others[0])
assertNotNull(call)
assertEquals(CallTable.Event.DECLINED, call?.event)
assertNotNull(call?.messageId)
}
}

View File

@@ -0,0 +1,539 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertTrue
import net.zetetic.database.sqlcipher.SQLiteDatabase
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ForeignKeyConstraint
import org.signal.core.util.Index
import org.signal.core.util.getForeignKeys
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.testing.SignalActivityRule
/**
* A test that guarantees that a freshly-created database looks the same as one that went through the upgrade path.
*/
@RunWith(AndroidJUnit4::class)
class DatabaseConsistencyTest {
@get:Rule
val harness = SignalActivityRule()
@Test
fun testUpgradeConsistency() {
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
val testHelper = InMemoryTestHelper(ApplicationDependencies.getApplication()).also {
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
}
val upgradedStatements = testHelper.readableDatabase.getAllCreateStatements()
if (currentVersionStatements != upgradedStatements) {
var message = "\n"
val currentByName = currentVersionStatements.associateBy { it.name }
val upgradedByName = upgradedStatements.associateBy { it.name }
if (currentByName.keys != upgradedByName.keys) {
val exclusiveToCurrent = currentByName.keys - upgradedByName.keys
val exclusiveToUpgrade = upgradedByName.keys - currentByName.keys
message += "SQL entities exclusive to the newly-created database: $exclusiveToCurrent\n"
message += "SQL entities exclusive to the upgraded database: $exclusiveToUpgrade\n\n"
} else {
for (currentEntry in currentByName) {
val upgradedValue: Statement = upgradedByName[currentEntry.key]!!
if (upgradedValue.sql != currentEntry.value.sql) {
message += "Statement differed:\n"
message += "newly-created:\n"
message += "${currentEntry.value.sql}\n\n"
message += "upgraded:\n"
message += "${upgradedValue.sql}\n\n"
}
}
}
assertTrue(message, false)
}
}
@Test
fun testForeignKeyIndexCoverage() {
/** We may deem certain indexes non-critical if deletion frequency is low or table size is small. */
val ignoredColumns: List<Pair<String, String>> = listOf(
StorySendTable.TABLE_NAME to StorySendTable.DISTRIBUTION_ID
)
val foreignKeys: List<ForeignKeyConstraint> = SignalDatabase.rawDatabase.getForeignKeys()
val indexesByFirstColumn: List<Index> = SignalDatabase.rawDatabase.getIndexes()
val notFound: List<Pair<String, String>> = foreignKeys
.filterNot { ignoredColumns.contains(it.table to it.column) }
.filterNot { foreignKey ->
indexesByFirstColumn.hasPrimaryIndexFor(foreignKey.table, foreignKey.column)
}
.map { it.table to it.column }
assertTrue("Missing indexes to cover: $notFound", notFound.isEmpty())
}
private fun List<Index>.hasPrimaryIndexFor(table: String, column: String): Boolean {
return this.any { index -> index.table == table && index.columns[0] == column }
}
private data class Statement(
val name: String,
val sql: String
)
private fun SQLiteDatabase.getAllCreateStatements(): List<Statement> {
return this.rawQuery("SELECT name, sql FROM sqlite_schema WHERE sql NOT NULL AND name != 'sqlite_sequence'")
.readToList { cursor ->
Statement(
name = cursor.requireNonNullString("name"),
sql = cursor.requireNonNullString("sql").normalizeSql()
)
}
.filterNot { it.name.startsWith("sqlite_stat") }
.sortedBy { it.name }
}
private fun String.normalizeSql(): String {
return this
.split("\n")
.map { it.trim() }
.joinToString(separator = " ")
.replace(Regex.fromLiteral(" ,"), ",")
.replace(Regex("\\s+"), " ")
.replace(Regex.fromLiteral("( "), "(")
.replace(Regex.fromLiteral(" )"), ")")
.replace(Regex("CREATE TABLE \"([a-zA-Z_]+)\""), "CREATE TABLE $1") // for some reason SQLite will wrap table names in quotes for upgraded tables. This unwraps them.
}
private class InMemoryTestHelper(private val application: Application) : SQLiteOpenHelper(application, null, null, 1) {
override fun onCreate(db: SQLiteDatabase) {
for (statement in SNAPSHOT_V181) {
db.execSQL(statement.sql)
}
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
SignalDatabaseMigrations.migrate(application, db, 181, SignalDatabaseMigrations.DATABASE_VERSION)
}
/**
* This is the list of statements that existed at version 181. Never change this.
*/
private val SNAPSHOT_V181 = listOf(
Statement(
name = "message",
sql = "CREATE TABLE message (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n date_server INTEGER DEFAULT -1,\n thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n recipient_device_id INTEGER,\n type INTEGER NOT NULL,\n body TEXT,\n read INTEGER DEFAULT 0,\n ct_l TEXT,\n exp INTEGER,\n m_type INTEGER,\n m_size INTEGER,\n st INTEGER,\n tr_id TEXT,\n subscription_id INTEGER DEFAULT -1, \n receipt_timestamp INTEGER DEFAULT -1, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n viewed_receipt_count INTEGER DEFAULT 0,\n mismatched_identities TEXT DEFAULT NULL,\n network_failures TEXT DEFAULT NULL,\n expires_in INTEGER DEFAULT 0,\n expire_started INTEGER DEFAULT 0,\n notified INTEGER DEFAULT 0,\n quote_id INTEGER DEFAULT 0,\n quote_author INTEGER DEFAULT 0,\n quote_body TEXT DEFAULT NULL,\n quote_missing INTEGER DEFAULT 0,\n quote_mentions BLOB DEFAULT NULL,\n quote_type INTEGER DEFAULT 0,\n shared_contacts TEXT DEFAULT NULL,\n unidentified INTEGER DEFAULT 0,\n link_previews TEXT DEFAULT NULL,\n view_once INTEGER DEFAULT 0,\n reactions_unread INTEGER DEFAULT 0,\n reactions_last_seen INTEGER DEFAULT -1,\n remote_deleted INTEGER DEFAULT 0,\n mentions_self INTEGER DEFAULT 0,\n notified_timestamp INTEGER DEFAULT 0,\n server_guid TEXT DEFAULT NULL,\n message_ranges BLOB DEFAULT NULL,\n story_type INTEGER DEFAULT 0,\n parent_story_id INTEGER DEFAULT 0,\n export_state BLOB DEFAULT NULL,\n exported INTEGER DEFAULT 0,\n scheduled_date INTEGER DEFAULT -1\n )"
),
Statement(
name = "part",
sql = "CREATE TABLE part (_id INTEGER PRIMARY KEY, mid INTEGER, seq INTEGER DEFAULT 0, ct TEXT, name TEXT, chset INTEGER, cd TEXT, fn TEXT, cid TEXT, cl TEXT, ctt_s INTEGER, ctt_t TEXT, encrypted INTEGER, pending_push INTEGER, _data TEXT, data_size INTEGER, file_name TEXT, unique_id INTEGER NOT NULL, digest BLOB, fast_preflight_id TEXT, voice_note INTEGER DEFAULT 0, borderless INTEGER DEFAULT 0, video_gif INTEGER DEFAULT 0, data_random BLOB, quote INTEGER DEFAULT 0, width INTEGER DEFAULT 0, height INTEGER DEFAULT 0, caption TEXT DEFAULT NULL, sticker_pack_id TEXT DEFAULT NULL, sticker_pack_key DEFAULT NULL, sticker_id INTEGER DEFAULT -1, sticker_emoji STRING DEFAULT NULL, data_hash TEXT DEFAULT NULL, blur_hash TEXT DEFAULT NULL, transform_properties TEXT DEFAULT NULL, transfer_file TEXT DEFAULT NULL, display_order INTEGER DEFAULT 0, upload_timestamp INTEGER DEFAULT 0, cdn_number INTEGER DEFAULT 0)"
),
Statement(
name = "thread",
sql = "CREATE TABLE thread (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n date INTEGER DEFAULT 0, \n meaningful_messages INTEGER DEFAULT 0,\n recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE,\n read INTEGER DEFAULT 1, \n type INTEGER DEFAULT 0, \n error INTEGER DEFAULT 0, \n snippet TEXT, \n snippet_type INTEGER DEFAULT 0, \n snippet_uri TEXT DEFAULT NULL, \n snippet_content_type TEXT DEFAULT NULL, \n snippet_extras TEXT DEFAULT NULL, \n unread_count INTEGER DEFAULT 0, \n archived INTEGER DEFAULT 0, \n status INTEGER DEFAULT 0, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n expires_in INTEGER DEFAULT 0, \n last_seen INTEGER DEFAULT 0, \n has_sent INTEGER DEFAULT 0, \n last_scrolled INTEGER DEFAULT 0, \n pinned INTEGER DEFAULT 0, \n unread_self_mention_count INTEGER DEFAULT 0\n)"
),
Statement(
name = "identities",
sql = "CREATE TABLE identities (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address INTEGER UNIQUE, \n identity_key TEXT, \n first_use INTEGER DEFAULT 0, \n timestamp INTEGER DEFAULT 0, \n verified INTEGER DEFAULT 0, \n nonblocking_approval INTEGER DEFAULT 0\n )"
),
Statement(
name = "drafts",
sql = "CREATE TABLE drafts (\n _id INTEGER PRIMARY KEY, \n thread_id INTEGER, \n type TEXT, \n value TEXT\n )"
),
Statement(
name = "push",
sql = "CREATE TABLE push (_id INTEGER PRIMARY KEY, type INTEGER, source TEXT, source_uuid TEXT, device_id INTEGER, body TEXT, content TEXT, timestamp INTEGER, server_timestamp INTEGER DEFAULT 0, server_delivered_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL)"
),
Statement(
name = "groups",
sql = "CREATE TABLE groups (\n _id INTEGER PRIMARY KEY, \n group_id TEXT, \n recipient_id INTEGER,\n title TEXT,\n avatar_id INTEGER, \n avatar_key BLOB,\n avatar_content_type TEXT, \n avatar_relay TEXT,\n timestamp INTEGER,\n active INTEGER DEFAULT 1,\n avatar_digest BLOB, \n mms INTEGER DEFAULT 0, \n master_key BLOB, \n revision BLOB, \n decrypted_group BLOB, \n expected_v2_id TEXT DEFAULT NULL, \n former_v1_members TEXT DEFAULT NULL, \n distribution_id TEXT DEFAULT NULL, \n display_as_story INTEGER DEFAULT 0, \n auth_service_id TEXT DEFAULT NULL, \n last_force_update_timestamp INTEGER DEFAULT 0\n )"
),
Statement(
name = "group_membership",
sql = "CREATE TABLE group_membership ( _id INTEGER PRIMARY KEY, group_id TEXT NOT NULL, recipient_id INTEGER NOT NULL, UNIQUE(group_id, recipient_id) )"
),
Statement(
name = "recipient",
sql = "CREATE TABLE recipient (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n uuid TEXT UNIQUE DEFAULT NULL,\n username TEXT UNIQUE DEFAULT NULL,\n phone TEXT UNIQUE DEFAULT NULL,\n email TEXT UNIQUE DEFAULT NULL,\n group_id TEXT UNIQUE DEFAULT NULL,\n group_type INTEGER DEFAULT 0,\n blocked INTEGER DEFAULT 0,\n message_ringtone TEXT DEFAULT NULL, \n message_vibrate INTEGER DEFAULT 0, \n call_ringtone TEXT DEFAULT NULL, \n call_vibrate INTEGER DEFAULT 0, \n notification_channel TEXT DEFAULT NULL, \n mute_until INTEGER DEFAULT 0, \n color TEXT DEFAULT NULL, \n seen_invite_reminder INTEGER DEFAULT 0,\n default_subscription_id INTEGER DEFAULT -1,\n message_expiration_time INTEGER DEFAULT 0,\n registered INTEGER DEFAULT 0,\n system_given_name TEXT DEFAULT NULL, \n system_family_name TEXT DEFAULT NULL, \n system_display_name TEXT DEFAULT NULL, \n system_photo_uri TEXT DEFAULT NULL, \n system_phone_label TEXT DEFAULT NULL, \n system_phone_type INTEGER DEFAULT -1, \n system_contact_uri TEXT DEFAULT NULL, \n system_info_pending INTEGER DEFAULT 0, \n profile_key TEXT DEFAULT NULL, \n profile_key_credential TEXT DEFAULT NULL, \n signal_profile_name TEXT DEFAULT NULL, \n profile_family_name TEXT DEFAULT NULL, \n profile_joined_name TEXT DEFAULT NULL, \n signal_profile_avatar TEXT DEFAULT NULL, \n profile_sharing INTEGER DEFAULT 0, \n last_profile_fetch INTEGER DEFAULT 0, \n unidentified_access_mode INTEGER DEFAULT 0, \n force_sms_selection INTEGER DEFAULT 0, \n storage_service_key TEXT UNIQUE DEFAULT NULL, \n mention_setting INTEGER DEFAULT 0, \n storage_proto TEXT DEFAULT NULL,\n capabilities INTEGER DEFAULT 0,\n last_session_reset BLOB DEFAULT NULL,\n wallpaper BLOB DEFAULT NULL,\n wallpaper_file TEXT DEFAULT NULL,\n about TEXT DEFAULT NULL,\n about_emoji TEXT DEFAULT NULL,\n extras BLOB DEFAULT NULL,\n groups_in_common INTEGER DEFAULT 0,\n chat_colors BLOB DEFAULT NULL,\n custom_chat_colors_id INTEGER DEFAULT 0,\n badges BLOB DEFAULT NULL,\n pni TEXT DEFAULT NULL,\n distribution_list_id INTEGER DEFAULT NULL,\n needs_pni_signature INTEGER DEFAULT 0,\n unregistered_timestamp INTEGER DEFAULT 0,\n hidden INTEGER DEFAULT 0,\n reporting_token BLOB DEFAULT NULL,\n system_nickname TEXT DEFAULT NULL\n)"
),
Statement(
name = "group_receipts",
sql = "CREATE TABLE group_receipts (\n _id INTEGER PRIMARY KEY, \n mms_id INTEGER, \n address INTEGER, \n status INTEGER, \n timestamp INTEGER, \n unidentified INTEGER DEFAULT 0\n )"
),
Statement(
name = "one_time_prekeys",
sql = "CREATE TABLE one_time_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL, \n private_key TEXT NOT NULL,\n UNIQUE(account_id, key_id)\n )"
),
Statement(
name = "signed_prekeys",
sql = "CREATE TABLE signed_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL,\n private_key TEXT NOT NULL,\n signature TEXT NOT NULL, \n timestamp INTEGER DEFAULT 0,\n UNIQUE(account_id, key_id)\n )"
),
Statement(
name = "sessions",
sql = "CREATE TABLE sessions (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n account_id TEXT NOT NULL,\n address TEXT NOT NULL,\n device INTEGER NOT NULL,\n record BLOB NOT NULL,\n UNIQUE(account_id, address, device)\n )"
),
Statement(
name = "sender_keys",
sql = "CREATE TABLE sender_keys (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n distribution_id TEXT NOT NULL,\n record BLOB NOT NULL, \n created_at INTEGER NOT NULL, \n UNIQUE(address,device, distribution_id) ON CONFLICT REPLACE\n )"
),
Statement(
name = "sender_key_shared",
sql = "CREATE TABLE sender_key_shared (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n distribution_id TEXT NOT NULL, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n timestamp INTEGER DEFAULT 0, \n UNIQUE(distribution_id,address, device) ON CONFLICT REPLACE\n )"
),
Statement(
name = "pending_retry_receipts",
sql = "CREATE TABLE pending_retry_receipts(_id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, device INTEGER NOT NULL, sent_timestamp INTEGER NOT NULL, received_timestamp TEXT NOT NULL, thread_id INTEGER NOT NULL, UNIQUE(author,sent_timestamp) ON CONFLICT REPLACE)"
),
Statement(
name = "sticker",
sql = "CREATE TABLE sticker (_id INTEGER PRIMARY KEY AUTOINCREMENT, pack_id TEXT NOT NULL, pack_key TEXT NOT NULL, pack_title TEXT NOT NULL, pack_author TEXT NOT NULL, sticker_id INTEGER, cover INTEGER, pack_order INTEGER, emoji TEXT NOT NULL, content_type TEXT DEFAULT NULL, last_used INTEGER, installed INTEGER,file_path TEXT NOT NULL, file_length INTEGER, file_random BLOB, UNIQUE(pack_id, sticker_id, cover) ON CONFLICT IGNORE)"
),
Statement(
name = "storage_key",
sql = "CREATE TABLE storage_key (_id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER, key TEXT UNIQUE)"
),
Statement(
name = "mention",
sql = "CREATE TABLE mention(_id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, message_id INTEGER, recipient_id INTEGER, range_start INTEGER, range_length INTEGER)"
),
Statement(
name = "payments",
sql = "CREATE TABLE payments(_id INTEGER PRIMARY KEY, uuid TEXT DEFAULT NULL, recipient INTEGER DEFAULT 0, recipient_address TEXT DEFAULT NULL, timestamp INTEGER, note TEXT DEFAULT NULL, direction INTEGER, state INTEGER, failure_reason INTEGER, amount BLOB NOT NULL, fee BLOB NOT NULL, transaction_record BLOB DEFAULT NULL, receipt BLOB DEFAULT NULL, payment_metadata BLOB DEFAULT NULL, receipt_public_key TEXT DEFAULT NULL, block_index INTEGER DEFAULT 0, block_timestamp INTEGER DEFAULT 0, seen INTEGER, UNIQUE(uuid) ON CONFLICT ABORT)"
),
Statement(
name = "chat_colors",
sql = "CREATE TABLE chat_colors (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n chat_colors BLOB\n)"
),
Statement(
name = "emoji_search",
sql = "CREATE TABLE emoji_search (\n _id INTEGER PRIMARY KEY,\n label TEXT NOT NULL,\n emoji TEXT NOT NULL,\n rank INTEGER DEFAULT 2147483647 \n )"
),
Statement(
name = "avatar_picker",
sql = "CREATE TABLE avatar_picker (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n last_used INTEGER DEFAULT 0,\n group_id TEXT DEFAULT NULL,\n avatar BLOB NOT NULL\n)"
),
Statement(
name = "group_call_ring",
sql = "CREATE TABLE group_call_ring (\n _id INTEGER PRIMARY KEY,\n ring_id INTEGER UNIQUE,\n date_received INTEGER,\n ring_state INTEGER\n)"
),
Statement(
name = "reaction",
sql = "CREATE TABLE reaction (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n emoji TEXT NOT NULL,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n UNIQUE(message_id, author_id) ON CONFLICT REPLACE\n)"
),
Statement(
name = "donation_receipt",
sql = "CREATE TABLE donation_receipt (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n receipt_type TEXT NOT NULL,\n receipt_date INTEGER NOT NULL,\n amount TEXT NOT NULL,\n currency TEXT NOT NULL,\n subscription_level INTEGER NOT NULL\n)"
),
Statement(
name = "story_sends",
sql = "CREATE TABLE story_sends (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n allows_replies INTEGER NOT NULL,\n distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE\n)"
),
Statement(
name = "cds",
sql = "CREATE TABLE cds (\n _id INTEGER PRIMARY KEY,\n e164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE,\n last_seen_at INTEGER DEFAULT 0\n )"
),
Statement(
name = "remote_megaphone",
sql = "CREATE TABLE remote_megaphone (\n _id INTEGER PRIMARY KEY,\n uuid TEXT UNIQUE NOT NULL,\n priority INTEGER NOT NULL,\n countries TEXT,\n minimum_version INTEGER NOT NULL,\n dont_show_before INTEGER NOT NULL,\n dont_show_after INTEGER NOT NULL,\n show_for_days INTEGER NOT NULL,\n conditional_id TEXT,\n primary_action_id TEXT,\n secondary_action_id TEXT,\n image_url TEXT,\n image_uri TEXT DEFAULT NULL,\n title TEXT NOT NULL,\n body TEXT NOT NULL,\n primary_action_text TEXT,\n secondary_action_text TEXT,\n shown_at INTEGER DEFAULT 0,\n finished_at INTEGER DEFAULT 0,\n primary_action_data TEXT DEFAULT NULL,\n secondary_action_data TEXT DEFAULT NULL,\n snoozed_at INTEGER DEFAULT 0,\n seen_count INTEGER DEFAULT 0\n)"
),
Statement(
name = "pending_pni_signature_message",
sql = "CREATE TABLE pending_pni_signature_message (\n _id INTEGER PRIMARY KEY,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n device_id INTEGER NOT NULL\n )"
),
Statement(
name = "call",
sql = "CREATE TABLE call (\n _id INTEGER PRIMARY KEY,\n call_id INTEGER NOT NULL UNIQUE,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n peer INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n type INTEGER NOT NULL,\n direction INTEGER NOT NULL,\n event INTEGER NOT NULL\n)"
),
Statement(
name = "message_fts",
sql = "CREATE VIRTUAL TABLE message_fts USING fts5(body, thread_id UNINDEXED, content=message, content_rowid=_id)"
),
Statement(
name = "remapped_recipients",
sql = "CREATE TABLE remapped_recipients (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )"
),
Statement(
name = "remapped_threads",
sql = "CREATE TABLE remapped_threads (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )"
),
Statement(
name = "msl_payload",
sql = "CREATE TABLE msl_payload (\n _id INTEGER PRIMARY KEY,\n date_sent INTEGER NOT NULL,\n content BLOB NOT NULL,\n content_hint INTEGER NOT NULL,\n urgent INTEGER NOT NULL DEFAULT 1\n )"
),
Statement(
name = "msl_recipient",
sql = "CREATE TABLE msl_recipient (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL, \n device INTEGER NOT NULL\n )"
),
Statement(
name = "msl_message",
sql = "CREATE TABLE msl_message (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n message_id INTEGER NOT NULL\n )"
),
Statement(
name = "notification_profile",
sql = "CREATE TABLE notification_profile (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n emoji TEXT NOT NULL,\n color TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n allow_all_calls INTEGER NOT NULL DEFAULT 0,\n allow_all_mentions INTEGER NOT NULL DEFAULT 0\n)"
),
Statement(
name = "notification_profile_schedule",
sql = "CREATE TABLE notification_profile_schedule (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n enabled INTEGER NOT NULL DEFAULT 0,\n start INTEGER NOT NULL,\n end INTEGER NOT NULL,\n days_enabled TEXT NOT NULL\n)"
),
Statement(
name = "notification_profile_allowed_members",
sql = "CREATE TABLE notification_profile_allowed_members (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL,\n UNIQUE(notification_profile_id, recipient_id) ON CONFLICT REPLACE\n)"
),
Statement(
name = "distribution_list",
sql = "CREATE TABLE distribution_list (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL,\n distribution_id TEXT UNIQUE NOT NULL,\n recipient_id INTEGER UNIQUE REFERENCES recipient (_id),\n allows_replies INTEGER DEFAULT 1,\n deletion_timestamp INTEGER DEFAULT 0,\n is_unknown INTEGER DEFAULT 0,\n privacy_mode INTEGER DEFAULT 0\n )"
),
Statement(
name = "distribution_list_member",
sql = "CREATE TABLE distribution_list_member (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id),\n privacy_mode INTEGER DEFAULT 0\n )"
),
Statement(
name = "recipient_group_type_index",
sql = "CREATE INDEX recipient_group_type_index ON recipient (group_type)"
),
Statement(
name = "recipient_pni_index",
sql = "CREATE UNIQUE INDEX recipient_pni_index ON recipient (pni)"
),
Statement(
name = "recipient_service_id_profile_key",
sql = "CREATE INDEX recipient_service_id_profile_key ON recipient (uuid, profile_key) WHERE uuid NOT NULL AND profile_key NOT NULL"
),
Statement(
name = "mms_read_and_notified_and_thread_id_index",
sql = "CREATE INDEX mms_read_and_notified_and_thread_id_index ON message (read, notified, thread_id)"
),
Statement(
name = "mms_type_index",
sql = "CREATE INDEX mms_type_index ON message (type)"
),
Statement(
name = "mms_date_sent_index",
sql = "CREATE INDEX mms_date_sent_index ON message (date_sent, recipient_id, thread_id)"
),
Statement(
name = "mms_date_server_index",
sql = "CREATE INDEX mms_date_server_index ON message (date_server)"
),
Statement(
name = "mms_thread_date_index",
sql = "CREATE INDEX mms_thread_date_index ON message (thread_id, date_received)"
),
Statement(
name = "mms_reactions_unread_index",
sql = "CREATE INDEX mms_reactions_unread_index ON message (reactions_unread)"
),
Statement(
name = "mms_story_type_index",
sql = "CREATE INDEX mms_story_type_index ON message (story_type)"
),
Statement(
name = "mms_parent_story_id_index",
sql = "CREATE INDEX mms_parent_story_id_index ON message (parent_story_id)"
),
Statement(
name = "mms_thread_story_parent_story_scheduled_date_index",
sql = "CREATE INDEX mms_thread_story_parent_story_scheduled_date_index ON message (thread_id, date_received, story_type, parent_story_id, scheduled_date)"
),
Statement(
name = "message_quote_id_quote_author_scheduled_date_index",
sql = "CREATE INDEX message_quote_id_quote_author_scheduled_date_index ON message (quote_id, quote_author, scheduled_date)"
),
Statement(
name = "mms_exported_index",
sql = "CREATE INDEX mms_exported_index ON message (exported)"
),
Statement(
name = "mms_id_type_payment_transactions_index",
sql = "CREATE INDEX mms_id_type_payment_transactions_index ON message (_id,type) WHERE type & 12884901888 != 0"
),
Statement(
name = "part_mms_id_index",
sql = "CREATE INDEX part_mms_id_index ON part (mid)"
),
Statement(
name = "pending_push_index",
sql = "CREATE INDEX pending_push_index ON part (pending_push)"
),
Statement(
name = "part_sticker_pack_id_index",
sql = "CREATE INDEX part_sticker_pack_id_index ON part (sticker_pack_id)"
),
Statement(
name = "part_data_hash_index",
sql = "CREATE INDEX part_data_hash_index ON part (data_hash)"
),
Statement(
name = "part_data_index",
sql = "CREATE INDEX part_data_index ON part (_data)"
),
Statement(
name = "thread_recipient_id_index",
sql = "CREATE INDEX thread_recipient_id_index ON thread (recipient_id)"
),
Statement(
name = "archived_count_index",
sql = "CREATE INDEX archived_count_index ON thread (archived, meaningful_messages)"
),
Statement(
name = "thread_pinned_index",
sql = "CREATE INDEX thread_pinned_index ON thread (pinned)"
),
Statement(
name = "thread_read",
sql = "CREATE INDEX thread_read ON thread (read)"
),
Statement(
name = "draft_thread_index",
sql = "CREATE INDEX draft_thread_index ON drafts (thread_id)"
),
Statement(
name = "group_id_index",
sql = "CREATE UNIQUE INDEX group_id_index ON groups (group_id)"
),
Statement(
name = "group_recipient_id_index",
sql = "CREATE UNIQUE INDEX group_recipient_id_index ON groups (recipient_id)"
),
Statement(
name = "expected_v2_id_index",
sql = "CREATE UNIQUE INDEX expected_v2_id_index ON groups (expected_v2_id)"
),
Statement(
name = "group_distribution_id_index",
sql = "CREATE UNIQUE INDEX group_distribution_id_index ON groups(distribution_id)"
),
Statement(
name = "group_receipt_mms_id_index",
sql = "CREATE INDEX group_receipt_mms_id_index ON group_receipts (mms_id)"
),
Statement(
name = "sticker_pack_id_index",
sql = "CREATE INDEX sticker_pack_id_index ON sticker (pack_id)"
),
Statement(
name = "sticker_sticker_id_index",
sql = "CREATE INDEX sticker_sticker_id_index ON sticker (sticker_id)"
),
Statement(
name = "storage_key_type_index",
sql = "CREATE INDEX storage_key_type_index ON storage_key (type)"
),
Statement(
name = "mention_message_id_index",
sql = "CREATE INDEX mention_message_id_index ON mention (message_id)"
),
Statement(
name = "mention_recipient_id_thread_id_index",
sql = "CREATE INDEX mention_recipient_id_thread_id_index ON mention (recipient_id, thread_id)"
),
Statement(
name = "timestamp_direction_index",
sql = "CREATE INDEX timestamp_direction_index ON payments (timestamp, direction)"
),
Statement(
name = "timestamp_index",
sql = "CREATE INDEX timestamp_index ON payments (timestamp)"
),
Statement(
name = "receipt_public_key_index",
sql = "CREATE UNIQUE INDEX receipt_public_key_index ON payments (receipt_public_key)"
),
Statement(
name = "msl_payload_date_sent_index",
sql = "CREATE INDEX msl_payload_date_sent_index ON msl_payload (date_sent)"
),
Statement(
name = "msl_recipient_recipient_index",
sql = "CREATE INDEX msl_recipient_recipient_index ON msl_recipient (recipient_id, device, payload_id)"
),
Statement(
name = "msl_recipient_payload_index",
sql = "CREATE INDEX msl_recipient_payload_index ON msl_recipient (payload_id)"
),
Statement(
name = "msl_message_message_index",
sql = "CREATE INDEX msl_message_message_index ON msl_message (message_id, payload_id)"
),
Statement(
name = "date_received_index",
sql = "CREATE INDEX date_received_index on group_call_ring (date_received)"
),
Statement(
name = "notification_profile_schedule_profile_index",
sql = "CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id)"
),
Statement(
name = "notification_profile_allowed_members_profile_index",
sql = "CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id)"
),
Statement(
name = "donation_receipt_type_index",
sql = "CREATE INDEX donation_receipt_type_index ON donation_receipt (receipt_type)"
),
Statement(
name = "donation_receipt_date_index",
sql = "CREATE INDEX donation_receipt_date_index ON donation_receipt (receipt_date)"
),
Statement(
name = "story_sends_recipient_id_sent_timestamp_allows_replies_index",
sql = "CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)"
),
Statement(
name = "story_sends_message_id_distribution_id_index",
sql = "CREATE INDEX story_sends_message_id_distribution_id_index ON story_sends (message_id, distribution_id)"
),
Statement(
name = "distribution_list_member_list_id_recipient_id_privacy_mode_index",
sql = "CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)"
),
Statement(
name = "pending_pni_recipient_sent_device_index",
sql = "CREATE UNIQUE INDEX pending_pni_recipient_sent_device_index ON pending_pni_signature_message (recipient_id, sent_timestamp, device_id)"
),
Statement(
name = "call_call_id_index",
sql = "CREATE INDEX call_call_id_index ON call (call_id)"
),
Statement(
name = "call_message_id_index",
sql = "CREATE INDEX call_message_id_index ON call (message_id)"
),
Statement(
name = "message_ai",
sql = "CREATE TRIGGER message_ai AFTER INSERT ON message BEGIN\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END"
),
Statement(
name = "message_ad",
sql = "CREATE TRIGGER message_ad AFTER DELETE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n END"
),
Statement(
name = "message_au",
sql = "CREATE TRIGGER message_au AFTER UPDATE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END"
),
Statement(
name = "msl_message_delete",
sql = "CREATE TRIGGER msl_message_delete AFTER DELETE ON message \n BEGIN \n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id);\n END"
),
Statement(
name = "msl_attachment_delete",
sql = "CREATE TRIGGER msl_attachment_delete AFTER DELETE ON part\n BEGIN\n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old.mid);\n END"
)
)
}
}

View File

@@ -10,9 +10,9 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import java.util.UUID
class DistributionListDatabaseTest {
class DistributionListTablesTest {
private lateinit var distributionDatabase: DistributionListDatabase
private lateinit var distributionDatabase: DistributionListTables
@Before
fun setup() {

View File

@@ -0,0 +1,341 @@
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
import org.junit.Test
import org.signal.core.util.delete
import org.signal.core.util.readToList
import org.signal.core.util.requireLong
import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import java.security.SecureRandom
import kotlin.random.Random
class GroupTableTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var groupTable: GroupTable
@Before
fun setUp() {
groupTable = SignalDatabase.groups
groupTable.writableDatabase.delete(GroupTable.TABLE_NAME).run()
groupTable.writableDatabase.delete(GroupTable.MembershipTable.TABLE_NAME).run()
}
@Test
fun whenICreateGroupV2_thenIExpectMemberRowsPopulated() {
val groupId = insertPushGroup()
//language=sql
val members: List<RecipientId> = groupTable.writableDatabase.query(
"""
SELECT ${GroupTable.MembershipTable.RECIPIENT_ID}
FROM ${GroupTable.MembershipTable.TABLE_NAME}
WHERE ${GroupTable.MembershipTable.GROUP_ID} = "${groupId.serialize()}"
""".trimIndent()
).readToList {
RecipientId.from(it.requireLong(GroupTable.RECIPIENT_ID))
}
assertEquals(2, members.size)
}
@Test
fun givenAGroupV2_whenIGetGroupsContainingMember_thenIExpectGroup() {
val groupId = insertPushGroup()
insertThread(groupId)
val groups = groupTable.getGroupsContainingMember(harness.others[0], false)
assertEquals(1, groups.size)
assertEquals(groupId, groups[0].id)
}
@Test
fun givenAnMmsGroup_whenIGetMembers_thenIExpectAllMembers() {
val groupId = insertMmsGroup()
val groups = groupTable.getGroupMemberIds(groupId, GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF)
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()
insertMmsGroup(members = listOf(harness.others[1]))
val groups = groupTable.getGroups()
assertEquals(2, groups.cursor?.count)
}
@Test
fun givenAGroup_whenIGetGroup_thenIExpectGroup() {
val v2Group = insertPushGroup()
insertThread(v2Group)
val groupRecord = groupTable.getGroup(v2Group).get()
assertEquals(setOf(harness.self.id, harness.others[0]), groupRecord.members.toSet())
}
@Test
fun givenAGroupAndARemap_whenIGetGroup_thenIExpectRemap() {
val v2Group = insertPushGroup()
insertThread(v2Group)
groupTable.writableDatabase.withinTransaction {
RemappedRecords.getInstance().addRecipient(harness.others[0], harness.others[1])
}
val groupRecord = groupTable.getGroup(v2Group).get()
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet())
}
@Test
fun givenAGroup_whenIRemapRecipientsThatHaveAConflict_thenIExpectDeletion() {
val v2Group = insertPushGroupWithSelfAndOthers(
listOf(
harness.others[0],
harness.others[1]
)
)
insertThread(v2Group)
groupTable.remapRecipient(harness.others[0], harness.others[1])
val groupRecord = groupTable.getGroup(v2Group).get()
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet())
}
@Test
fun givenAGroup_whenIRemapRecipients_thenIExpectRemap() {
val v2Group = insertPushGroup()
insertThread(v2Group)
val newId = harness.others[1]
groupTable.remapRecipient(harness.others[0], newId)
val groupRecord = groupTable.getGroup(v2Group).get()
assertEquals(setOf(harness.self.id, newId), groupRecord.members.toSet())
}
@Test
fun givenAGroupAndMember_whenIIsCurrentMember_thenIExpectTrue() {
val v2Group = insertPushGroup()
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
assertTrue(actual)
}
@Test
fun givenAGroupAndMember_whenIRemove_thenIExpectNotAMember() {
val v2Group = insertPushGroup()
groupTable.remove(v2Group, harness.others[0])
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
assertFalse(actual)
}
@Test
fun givenAGroupAndNonMember_whenIIsCurrentMember_thenIExpectFalse() {
val v2Group = insertPushGroup()
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[1])
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())
val g2 = insertPushGroup(listOf())
val gr1 = groupTable.getGroup(g1)
val gr2 = groupTable.getGroup(g2)
assertEquals(g1, gr1.get().id)
assertEquals(g2, gr2.get().id)
}
@Test
fun givenASharedActiveGroupWithoutAThread_whenISearchForRecipientsWithGroupsInCommon_thenIExpectThatGroup() {
val groupInCommon = insertPushGroup()
val expected = Recipient.resolved(harness.others[0])
SignalDatabase.recipients.setProfileSharing(expected.id, false)
SignalDatabase.recipients.queryGroupMemberContacts("Buddy")!!.use {
assertTrue(it.moveToFirst())
assertEquals(1, it.count)
assertEquals(expected.id.toLong(), it.requireLong(RecipientTable.ID))
}
val groups = groupTable.getPushGroupsContainingMember(expected.id)
assertEquals(1, groups.size)
assertEquals(groups[0].id, groupInCommon)
}
private fun insertThread(groupId: GroupId): Long {
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
}
private fun insertMmsGroup(members: List<RecipientId> = listOf(harness.self.id, harness.others[0])): GroupId {
val id = GroupId.createMms(SecureRandom())
groupTable.create(
id,
null,
members.apply {
println("Creating a group with ${members.size} members")
}
)
return id
}
private fun insertPushGroup(
members: List<DecryptedMember> = listOf(
DecryptedMember.newBuilder()
.setUuid(harness.self.requireServiceId().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build(),
DecryptedMember.newBuilder()
.setUuid(Recipient.resolved(harness.others[0]).requireServiceId().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build()
)
): GroupId {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.newBuilder()
.addAllMembers(members)
.setRevision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState)!!
}
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val selfMember: DecryptedMember = DecryptedMember.newBuilder()
.setUuid(harness.self.requireServiceId().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build()
val otherMembers: List<DecryptedMember> = others.map { id ->
DecryptedMember.newBuilder()
.setUuid(Recipient.resolved(id).requireServiceId().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build()
}
val decryptedGroupState = DecryptedGroup.newBuilder()
.addAllMembers(listOf(selfMember) + otherMembers)
.setRevision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState)!!
}
}

View File

@@ -17,8 +17,8 @@ import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class MmsDatabaseTest_gifts {
private lateinit var mms: MmsDatabase
class MessageTableTest_gifts {
private lateinit var mms: MessageTable
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@@ -27,7 +27,7 @@ class MmsDatabaseTest_gifts {
@Before
fun setUp() {
mms = SignalDatabase.mms
mms = SignalDatabase.messages
mms.deleteAllThreads()

View File

@@ -4,8 +4,7 @@ import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import java.util.Optional
@@ -21,36 +20,28 @@ object MmsHelper {
subscriptionId: Int = -1,
expiresIn: Long = 0,
viewOnce: Boolean = false,
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
threadId: Long = 1,
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient, distributionType),
storyType: StoryType = StoryType.NONE,
parentStoryId: ParentStoryId? = null,
isStoryReaction: Boolean = false,
giftBadge: GiftBadge? = null,
secure: Boolean = true
): Long {
val message = OutgoingMediaMessage(
recipient,
body,
emptyList(),
sentTimeMillis,
subscriptionId,
expiresIn,
viewOnce,
distributionType,
storyType,
parentStoryId,
isStoryReaction,
null,
emptyList(),
emptyList(),
emptyList(),
emptySet(),
emptySet(),
giftBadge
).let {
if (secure) OutgoingSecureMediaMessage(it) else it
}
val message = OutgoingMessage(
recipient = recipient,
body = body,
timestamp = sentTimeMillis,
subscriptionId = subscriptionId,
expiresIn = expiresIn,
viewOnce = viewOnce,
distributionType = distributionType,
storyType = storyType,
parentStoryId = parentStoryId,
isStoryReaction = isStoryReaction,
giftBadge = giftBadge,
isSecure = secure
)
return insert(
message = message,
@@ -59,16 +50,16 @@ object MmsHelper {
}
fun insert(
message: OutgoingMediaMessage,
message: OutgoingMessage,
threadId: Long
): Long {
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
return SignalDatabase.messages.insertMessageOutbox(message, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
}
fun insert(
message: IncomingMediaMessage,
threadId: Long
): Optional<MessageDatabase.InsertResult> {
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId)
): Optional<MessageTable.InsertResult> {
return SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, threadId)
}
}

View File

@@ -24,9 +24,9 @@ import java.util.concurrent.TimeUnit
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class MmsDatabaseTest_stories {
class MmsTableTest_stories {
private lateinit var mms: MmsDatabase
private lateinit var mms: MessageTable
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@@ -37,7 +37,7 @@ class MmsDatabaseTest_stories {
@Before
fun setUp() {
mms = SignalDatabase.mms
mms = SignalDatabase.messages
mms.deleteAllThreads()
@@ -106,14 +106,14 @@ class MmsDatabaseTest_stories {
-1L
).get().messageId
val messageBeforeMark = SignalDatabase.mms.getMessageRecord(messageId)
val messageBeforeMark = SignalDatabase.messages.getMessageRecord(messageId)
assertFalse(messageBeforeMark.incomingStoryViewedAtTimestamp > 0)
// WHEN
SignalDatabase.mms.setIncomingMessageViewed(messageId)
SignalDatabase.messages.setIncomingMessageViewed(messageId)
// THEN
val messageAfterMark = SignalDatabase.mms.getMessageRecord(messageId)
val messageAfterMark = SignalDatabase.messages.getMessageRecord(messageId)
assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0)
}
@@ -128,7 +128,7 @@ class MmsDatabaseTest_stories {
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
).get().messageId
@@ -136,12 +136,12 @@ class MmsDatabaseTest_stories {
val randomizedOrderedIds = messageIds.shuffled()
randomizedOrderedIds.forEach {
SignalDatabase.mms.setIncomingMessageViewed(it)
SignalDatabase.messages.setIncomingMessageViewed(it)
Thread.sleep(5)
}
// WHEN
val result = SignalDatabase.mms.getOrderedStoryRecipientsAndIds(false)
val result = SignalDatabase.messages.getOrderedStoryRecipientsAndIds(false)
val resultOrderedIds = result.map { it.messageId }
// THEN
@@ -160,7 +160,7 @@ class MmsDatabaseTest_stories {
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
).get().messageId
@@ -174,7 +174,7 @@ class MmsDatabaseTest_stories {
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
).get().messageId
@@ -183,7 +183,7 @@ class MmsDatabaseTest_stories {
val interspersedIds: List<Long> = (0 until 10).map {
Thread.sleep(5)
if (it % 2 == 0) {
SignalDatabase.mms.setIncomingMessageViewed(viewedIds[it / 2])
SignalDatabase.messages.setIncomingMessageViewed(viewedIds[it / 2])
viewedIds[it / 2]
} else {
MmsHelper.insert(
@@ -195,7 +195,7 @@ class MmsDatabaseTest_stories {
}
}
val result = SignalDatabase.mms.getOrderedStoryRecipientsAndIds(false)
val result = SignalDatabase.messages.getOrderedStoryRecipientsAndIds(false)
val resultOrderedIds = result.map { it.messageId }
assertEquals(unviewedIds.reversed() + interspersedIds.reversed(), resultOrderedIds)
@@ -219,7 +219,7 @@ class MmsDatabaseTest_stories {
sentTimeMillis = 200,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
)
@@ -237,8 +237,7 @@ class MmsDatabaseTest_stories {
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
storyType = StoryType.STORY_WITH_REPLIES
)
// WHEN
@@ -296,8 +295,7 @@ class MmsDatabaseTest_stories {
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
storyType = StoryType.STORY_WITH_REPLIES
)
MmsHelper.insert(
@@ -355,7 +353,7 @@ class MmsDatabaseTest_stories {
)
// WHEN
val oldestTimestamp = SignalDatabase.mms.getOldestStorySendTimestamp(false)
val oldestTimestamp = SignalDatabase.messages.getOldestStorySendTimestamp(false)
// THEN
assertNull(oldestTimestamp)
@@ -374,7 +372,7 @@ class MmsDatabaseTest_stories {
)
// WHEN
val oldestTimestamp = SignalDatabase.mms.getOldestStorySendTimestamp(true)
val oldestTimestamp = SignalDatabase.messages.getOldestStorySendTimestamp(true)
// THEN
assertEquals(expected, oldestTimestamp)

View File

@@ -1,624 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.Assert
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.Test
import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.state.SessionRecord
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_getAndPossiblyMerge {
private lateinit var recipientDatabase: RecipientDatabase
private lateinit var identityDatabase: IdentityDatabase
private lateinit var groupReceiptDatabase: GroupReceiptDatabase
private lateinit var groupDatabase: GroupDatabase
private lateinit var threadDatabase: ThreadDatabase
private lateinit var smsDatabase: MessageDatabase
private lateinit var mmsDatabase: MessageDatabase
private lateinit var sessionDatabase: SessionDatabase
private lateinit var mentionDatabase: MentionDatabase
private lateinit var reactionDatabase: ReactionDatabase
private lateinit var notificationProfileDatabase: NotificationProfileDatabase
private lateinit var distributionListDatabase: DistributionListDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
recipientDatabase = SignalDatabase.recipients
identityDatabase = SignalDatabase.identities
groupReceiptDatabase = SignalDatabase.groupReceipts
groupDatabase = SignalDatabase.groups
threadDatabase = SignalDatabase.threads
smsDatabase = SignalDatabase.sms
mmsDatabase = SignalDatabase.mms
sessionDatabase = SignalDatabase.sessions
mentionDatabase = SignalDatabase.mentions
reactionDatabase = SignalDatabase.reactions
notificationProfileDatabase = SignalDatabase.notificationProfiles
distributionListDatabase = SignalDatabase.distributionLists
ensureDbEmpty()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
// ==============================================================
// If both the ACI and E164 map to no one
// ==============================================================
/** If all you have is an ACI, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertFalse(recipient.hasE164())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasServiceId())
}
/** With high trust, you can associate an ACI-e164 pair. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertEquals(E164_A, recipient.requireE164())
}
// ==============================================================
// If the ACI maps to an existing user, but the E164 doesn't
// ==============================================================
/** You can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** Basically the change number case. Update the existing user. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
// ==============================================================
// If the E164 maps to an existing user, but the ACI doesn't
// ==============================================================
/** You can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** We never change the ACI of an existing row. New ACI = new person. Take the e164 from the current holder. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
recipientDatabase.setPni(existingId, PNI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A)
recipientDatabase.setPni(retrievedId, PNI_A)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertFalse(existingRecipient.hasE164())
}
/** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertEquals(E164_A, existingRecipient.requireE164())
}
// ==============================================================
// If both the ACI and E164 map to an existing user
// ==============================================================
/** If your ACI and e164 match, youre good. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** Merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge() {
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A)
val mergedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
assertEquals(existingAciId, mergedId)
val retrievedRecipient = Recipient.resolved(mergedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(mergedId, existingE164Recipient.id)
// TODO [greyson] Change number
}
/** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge], but with a number change. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_changedNumber() {
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
// TODO [greyson] Change number
}
/** No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertFalse(existingRecipient2.hasE164())
// TODO [greyson] Change number
}
/**
* Another case that results in a merge. Nothing strictly new here, but this case is called out because its a merge but *also* an E164 change,
* which clients may need to know for UX purposes.
*/
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
val recipientWithId2 = Recipient.resolved(existingId2)
assertEquals(retrievedId, recipientWithId2.id)
}
/** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_B.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
assertEquals(existingId2, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val recipientWithId1 = Recipient.resolved(existingId1)
assertEquals(ACI_B, recipientWithId1.requireServiceId())
assertEquals(E164_A, recipientWithId1.requireE164())
}
/** This is a case where normally we'd update the E164 of a user, but here the changeSelf flag is disabled, so we shouldn't. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_changeSelfFalse() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, changeSelf = false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** This is a case where we're changing our own number, and it's allowed because changeSelf = true. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_changeSelfTrue() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, changeSelf = true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
/** Verifying a case where a change number job is expected to be enqueued. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_changedNumber() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
// TODO [greyson] Change number
}
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Test
fun getAndPossiblyMerge_merge_general() {
// Setup
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_B)
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
val smsId3: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
val mmsId1: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
val mmsId2: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
val mmsId3: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
val threadIdAci: Long = threadDatabase.getThreadIdFor(recipientIdAci)!!
val threadIdE164: Long = threadDatabase.getThreadIdFor(recipientIdE164)!!
assertNotEquals(threadIdAci, threadIdE164)
mentionDatabase.insert(threadIdAci, mmsId1, listOf(Mention(recipientIdE164, 0, 1)))
mentionDatabase.insert(threadIdE164, mmsId2, listOf(Mention(recipientIdAci, 0, 1)))
groupReceiptDatabase.insert(listOf(recipientIdAci, recipientIdE164), mmsId1, 0, 3)
val identityKeyAci: IdentityKey = identityKey(1)
val identityKeyE164: IdentityKey = identityKey(2)
identityDatabase.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
identityDatabase.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
sessionDatabase.store(localAci, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
val profile1: NotificationProfile = notificationProfile(name = "Test")
val profile2: NotificationProfile = notificationProfile(name = "Test2")
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdAci)
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB)
val distributionListId: DistributionListId = distributionListDatabase.createList("testlist", listOf(recipientIdE164, recipientIdAciB))!!
// Merge
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
assertEquals(recipientIdAci, retrievedId)
// Recipient validation
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(recipientIdE164)
assertEquals(retrievedId, existingE164Recipient.id)
// Thread validation
assertEquals(threadIdAci, retrievedThreadId)
Assert.assertNull(threadDatabase.getThreadIdFor(recipientIdE164))
Assert.assertNull(threadDatabase.getThreadRecord(threadIdE164))
// SMS validation
val sms1: MessageRecord = smsDatabase.getMessageRecord(smsId1)!!
val sms2: MessageRecord = smsDatabase.getMessageRecord(smsId2)!!
val sms3: MessageRecord = smsDatabase.getMessageRecord(smsId3)!!
assertEquals(retrievedId, sms1.recipient.id)
assertEquals(retrievedId, sms2.recipient.id)
assertEquals(retrievedId, sms3.recipient.id)
assertEquals(retrievedThreadId, sms1.threadId)
assertEquals(retrievedThreadId, sms2.threadId)
assertEquals(retrievedThreadId, sms3.threadId)
// MMS validation
val mms1: MessageRecord = mmsDatabase.getMessageRecord(mmsId1)!!
val mms2: MessageRecord = mmsDatabase.getMessageRecord(mmsId2)!!
val mms3: MessageRecord = mmsDatabase.getMessageRecord(mmsId3)!!
assertEquals(retrievedId, mms1.recipient.id)
assertEquals(retrievedId, mms2.recipient.id)
assertEquals(retrievedId, mms3.recipient.id)
assertEquals(retrievedThreadId, mms1.threadId)
assertEquals(retrievedThreadId, mms2.threadId)
assertEquals(retrievedThreadId, mms3.threadId)
// Mention validation
val mention1: MentionModel = getMention(mmsId1)
assertEquals(retrievedId, mention1.recipientId)
assertEquals(retrievedThreadId, mention1.threadId)
val mention2: MentionModel = getMention(mmsId2)
assertEquals(retrievedId, mention2.recipientId)
assertEquals(retrievedThreadId, mention2.threadId)
// Group receipt validation
val groupReceipts: List<GroupReceiptDatabase.GroupReceiptInfo> = groupReceiptDatabase.getGroupReceiptInfo(mmsId1)
assertEquals(retrievedId, groupReceipts[0].recipientId)
assertEquals(retrievedId, groupReceipts[1].recipientId)
// Identity validation
assertEquals(identityKeyAci, identityDatabase.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
Assert.assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
// Session validation
Assert.assertNotNull(sessionDatabase.load(localAci, SignalProtocolAddress(ACI_A.toString(), 1)))
// Reaction validation
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
val reactionsMms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(mmsId1, true))
assertEquals(1, reactionsSms.size)
assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0])
assertEquals(1, reactionsMms.size)
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
// Notification Profile validation
val updatedProfile1: NotificationProfile = notificationProfileDatabase.getProfile(profile1.id)!!
val updatedProfile2: NotificationProfile = notificationProfileDatabase.getProfile(profile2.id)!!
MatcherAssert.assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
MatcherAssert.assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
// Distribution List validation
val updatedList: DistributionListRecord = distributionListDatabase.getList(distributionListId)!!
MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
}
// ==============================================================
// Misc
// ==============================================================
@Test
fun createByE164SanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
// WHEN I retrieve one by E164
val possible: Optional<RecipientId> = recipientDatabase.getByE164(E164_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.e164.isPresent)
assertEquals(E164_A, recipient.e164.get())
}
@Test
fun createByUuidSanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
// WHEN I retrieve one by UUID
val possible: Optional<RecipientId> = recipientDatabase.getByServiceId(ACI_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.serviceId.isPresent)
assertEquals(ACI_A, recipient.serviceId.get())
}
@Test(expected = IllegalArgumentException::class)
fun getAndPossiblyMerge_noArgs_invalid() {
recipientDatabase.getAndPossiblyMerge(null, null, true)
}
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0))
}
}
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
}
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty(), false, false)
}
private fun identityKey(value: Byte): IdentityKey {
val bytes = ByteArray(33)
bytes[0] = 0x05
bytes[1] = value
return IdentityKey(bytes)
}
private fun notificationProfile(name: String): NotificationProfile {
return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
}
private fun groupMasterKey(value: Byte): GroupMasterKey {
val bytes = ByteArray(32)
bytes[0] = value
return GroupMasterKey(bytes)
}
private fun decryptedGroup(members: Collection<UUID>): DecryptedGroup {
return DecryptedGroup.newBuilder()
.addAllMembers(members.map { DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(it)).build() })
.build()
}
private fun getMention(messageId: Long): MentionModel {
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor ->
cursor.moveToFirst()
return MentionModel(
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)),
threadId = CursorUtil.requireLong(cursor, MentionDatabase.THREAD_ID)
)
}
}
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
data class MentionModel(
val recipientId: RecipientId,
val threadId: Long
)
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}

View File

@@ -1,482 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.Optional
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_processPnpTuple {
private lateinit var recipientDatabase: RecipientDatabase
private lateinit var smsDatabase: SmsDatabase
private lateinit var threadDatabase: ThreadDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
smsDatabase = SignalDatabase.sms
threadDatabase = SignalDatabase.threads
ensureDbEmpty()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
@Test
fun noMatch_e164Only() {
test {
process(E164_A, null, null)
expect(E164_A, null, null)
}
}
@Test
fun noMatch_e164AndPni() {
test {
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
}
}
@Test
fun noMatch_aciOnly() {
test {
process(null, null, ACI_A)
expect(null, null, ACI_A)
}
}
@Test(expected = IllegalStateException::class)
fun noMatch_noData() {
test {
process(null, null, null)
}
}
@Test
fun noMatch_allFields() {
test {
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun fullMatch() {
test {
given(E164_A, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyE164Matches() {
test {
given(E164_A, null, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyE164Matches_differentAci() {
test {
given(E164_A, null, ACI_B)
process(E164_A, PNI_A, ACI_A)
expect(null, null, ACI_B)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun e164AndPniMatches() {
test {
given(E164_A, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun e164AndAciMatches() {
test {
given(E164_A, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyPniMatches() {
test {
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun pniAndAciMatches() {
test {
given(null, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyAciMatches() {
test {
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
test {
given(E164_A, PNI_B, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
}
}
@Test
fun e164AndPniMatches_noExistingSession() {
test {
given(E164_A, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyPniMatches_noExistingSession() {
test {
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyPniMatches_noExistingPniSession_changeNumber() {
// This test, I could go either way. We decide to change the E164 on the existing row rather than create a new one.
// But it's an "unstable E164->PNI mapping" case, which we don't expect, so as long as there's a user-visible impact that should be fine.
test {
given(E164_B, PNI_A, null, createThread = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
}
@Test
fun pniAndAciMatches_changeNumber() {
// This test, I could go either way. We decide to change the E164 on the existing row rather than create a new one.
// But it's an "unstable E164->PNI mapping" case, which we don't expect, so as long as there's a user-visible impact that should be fine.
test {
given(E164_B, PNI_A, ACI_A, createThread = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
}
@Test
fun onlyAciMatches_changeNumber() {
test {
given(E164_B, null, ACI_A, createThread = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
}
@Test
fun merge_e164Only_pniOnly_aciOnly() {
test {
given(E164_A, null, null)
given(null, PNI_A, null)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun merge_e164Only_pniOnly_noAciProvided() {
test {
given(E164_A, null, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expectDeleted()
}
}
@Test
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
test {
given(E164_A, null, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectDeleted()
}
}
@Test
fun merge_e164Only_pniAndE164_noAciProvided() {
test {
given(E164_A, null, null)
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expect(E164_B, null, null)
}
}
@Test
fun merge_e164AndPni_pniOnly_noAciProvided() {
test {
given(E164_A, PNI_B, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expectDeleted()
}
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
test {
given(E164_A, PNI_B, null)
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expect(E164_B, null, null)
}
}
@Test
fun merge_e164AndPni_aciOnly() {
test {
given(E164_A, PNI_A, null)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
test {
given(E164_B, PNI_A, null)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_B, null, null)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
test {
given(E164_A, PNI_A, null)
given(E164_B, PNI_B, ACI_A, createThread = true)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
}
@Test
fun merge_e164AndPni_e164Aci_changeNumber() {
test {
given(E164_A, PNI_A, null)
given(E164_B, null, ACI_A, createThread = true)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientDatabase.TABLE_NAME,
null,
contentValuesOf(
RecipientDatabase.PHONE to e164,
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
RecipientDatabase.PNI_COLUMN to pni?.toString(),
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
)
)
return RecipientId.from(id)
}
private fun require(id: RecipientId): IdRecord {
return get(id)!!
}
private fun get(id: RecipientId): IdRecord? {
SignalDatabase.rawDatabase
.select(RecipientDatabase.ID, RecipientDatabase.PHONE, RecipientDatabase.SERVICE_ID, RecipientDatabase.PNI_COLUMN)
.from(RecipientDatabase.TABLE_NAME)
.where("${RecipientDatabase.ID} = ?", id)
.run()
.use { cursor ->
return if (cursor.moveToFirst()) {
IdRecord(
id = RecipientId.from(cursor.requireLong(RecipientDatabase.ID)),
e164 = cursor.requireString(RecipientDatabase.PHONE),
sid = ServiceId.parseOrNull(cursor.requireString(RecipientDatabase.SERVICE_ID)),
pni = PNI.parseOrNull(cursor.requireString(RecipientDatabase.PNI_COLUMN))
)
} else {
null
}
}
}
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0))
}
}
/**
* Baby DSL for making tests readable.
*/
private fun test(init: TestCase.() -> Unit): TestCase {
val test = TestCase()
test.init()
return test
}
private inner class TestCase {
private val generatedIds: LinkedHashSet<RecipientId> = LinkedHashSet()
private var expectCount = 0
private lateinit var outputRecipientId: RecipientId
fun given(e164: String?, pni: PNI?, aci: ACI?, createThread: Boolean = false) {
val id = insert(e164, pni, aci)
generatedIds += id
if (createThread) {
// Create a thread and throw a dummy message in it so it doesn't get automatically deleted
threadDatabase.getOrCreateThreadIdFor(Recipient.resolved(id))
smsDatabase.insertMessageInbox(IncomingEncryptedMessage(IncomingTextMessage(id, 1, 0, 0, 0, "", Optional.empty(), 0, false, ""), ""))
}
}
fun process(e164: String?, pni: PNI?, aci: ACI?) {
SignalDatabase.rawDatabase.beginTransaction()
try {
outputRecipientId = recipientDatabase.processPnpTuple(e164, pni, aci, pniVerified = false).finalId
generatedIds += outputRecipientId
SignalDatabase.rawDatabase.setTransactionSuccessful()
} finally {
SignalDatabase.rawDatabase.endTransaction()
}
}
fun expect(e164: String?, pni: PNI?, aci: ACI?) {
expect(generatedIds.elementAt(expectCount++), e164, pni, aci)
}
fun expect(id: RecipientId, e164: String?, pni: PNI?, aci: ACI?) {
val record: IdRecord = require(id)
assertEquals(e164, record.e164)
assertEquals(pni, record.pni)
assertEquals(aci ?: pni, record.sid)
}
fun expectDeleted() {
expectDeleted(generatedIds.elementAt(expectCount++))
}
fun expectDeleted(id: RecipientId) {
assertNull(get(id))
}
fun expectChangeNumberEvent() {
assertEquals(1, smsDatabase.getChangeNumberMessageCount(outputRecipientId))
}
}
private data class IdRecord(
val id: RecipientId,
val e164: String?,
val sid: ServiceId?,
val pni: PNI?,
)
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}

View File

@@ -1,842 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.lang.AssertionError
import java.lang.IllegalStateException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_processPnpTupleToChangeSet {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
private lateinit var db: RecipientDatabase
@Before
fun setup() {
db = SignalDatabase.recipients
}
@Test
fun noMatch_e164Only() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, null, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, null, null)
),
changeSet
)
}
@Test
fun noMatch_e164AndPni() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, null)
),
changeSet
)
}
@Test
fun noMatch_aciOnly() {
val changeSet = db.processPnpTupleToChangeSet(null, null, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(null, null, ACI_A)
),
changeSet
)
}
@Test(expected = IllegalStateException::class)
fun noMatch_noData() {
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
}
@Test
fun noMatch_allFields() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, ACI_A)
),
changeSet
)
}
@Test
fun fullMatch() {
val result = applyAndAssert(
Input(E164_A, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id)
),
result.changeSet
)
}
@Test
fun onlyE164Matches() {
val result = applyAndAssert(
Input(E164_A, null, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null, pniSession = true),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun e164AndAciMatches() {
val result = applyAndAssert(
Input(E164_A, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(null, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(null, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches() {
val result = applyAndAssert(
Input(null, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches() {
val result = applyAndAssert(
Input(null, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
Input(null, null, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.thirdId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.Merge(
primaryId = result.thirdId,
secondaryId = result.firstId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.SetAci(
recipientId = result.firstId,
aci = ACI_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniAndE164_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(E164_B, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(
recipientId = result.firstId,
pni = PNI_A
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(E164_B, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_sessionsExist() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null, pniSession = true),
Input(E164_B, PNI_A, null, pniSession = true),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.secondId),
PnpOperation.SessionSwitchoverInsert(result.firstId)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(null, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
val result = applyAndAssert(
listOf(
Input(E164_B, PNI_A, null),
Input(null, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.SetPni(
recipientId = result.secondId,
pni = PNI_A,
),
PnpOperation.SetE164(
recipientId = result.secondId,
e164 = E164_A,
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_B, PNI_A, null),
Input(E164_C, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.SetPni(
recipientId = result.secondId,
pni = PNI_A,
),
PnpOperation.SetE164(
recipientId = result.secondId,
e164 = E164_A,
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_C,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, PNI_B, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164Aci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientDatabase.TABLE_NAME,
null,
contentValuesOf(
RecipientDatabase.PHONE to e164,
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
RecipientDatabase.PNI_COLUMN to pni?.toString(),
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
)
)
return RecipientId.from(id)
}
private fun insertMockSessionFor(account: ServiceId, address: ServiceId) {
SignalDatabase.rawDatabase.insert(
SessionDatabase.TABLE_NAME, null,
contentValuesOf(
SessionDatabase.ACCOUNT_ID to account.toString(),
SessionDatabase.ADDRESS to address.toString(),
SessionDatabase.DEVICE to 1,
SessionDatabase.RECORD to Util.getSecretBytes(32)
)
)
}
data class Input(val e164: String?, val pni: PNI?, val aci: ACI?, val pniSession: Boolean = false, val aciSession: Boolean = false)
data class Update(val e164: String?, val pni: PNI?, val aci: ACI?, val pniVerified: Boolean = false)
data class Output(val e164: String?, val pni: PNI?, val aci: ACI?)
data class PnpMatchResult(val ids: List<RecipientId>, val changeSet: PnpChangeSet) {
val id
get() = if (ids.size == 1) {
ids[0]
} else {
throw IllegalStateException("There are multiple IDs, but you assumed 1!")
}
val firstId
get() = ids[0]
val secondId
get() = ids[1]
val thirdId
get() = ids[2]
}
private fun applyAndAssert(input: Input, update: Update, output: Output): PnpMatchResult {
return applyAndAssert(listOf(input), update, output)
}
/**
* Helper method that will call insert your recipients, call [RecipientDatabase.processPnpTupleToChangeSet] with your params,
* and then verify your output matches what you expect.
*
* It results the inserted ID's and changeset for additional verification.
*
* But basically this is here to make the tests more readable. It gives you a clear list of:
* - input
* - update
* - output
*
* that you can spot check easily.
*
* Important: The output will only include records that contain fields from the input. That means
* for:
*
* Input: E164_B, PNI_A, null
* Update: E164_A, PNI_A, null
*
* You will get:
* Output: E164_A, PNI_A, null
*
* Even though there was an update that will also result in the row (E164_B, null, null)
*/
private fun applyAndAssert(input: List<Input>, update: Update, output: Output): PnpMatchResult {
val ids = input.map { insert(it.e164, it.pni, it.aci) }
input
.filter { it.pniSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.pni!!) }
input
.filter { it.aciSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.aci!!) }
val byE164 = update.e164?.let { db.getByE164(it).orElse(null) }
val byPniSid = update.pni?.let { db.getByServiceId(it).orElse(null) }
val byAciSid = update.aci?.let { db.getByServiceId(it).orElse(null) }
val data = PnpDataSet(
e164 = update.e164,
pni = update.pni,
aci = update.aci,
byE164 = byE164,
byPniSid = byPniSid,
byPniOnly = update.pni?.let { db.getByPni(it).orElse(null) },
byAciSid = byAciSid,
e164Record = byE164?.let { db.getRecord(it) },
pniSidRecord = byPniSid?.let { db.getRecord(it) },
aciSidRecord = byAciSid?.let { db.getRecord(it) }
)
val changeSet = db.processPnpTupleToChangeSet(update.e164, update.pni, update.aci, pniVerified = update.pniVerified)
val finalData = data.perform(changeSet.operations)
val finalRecords = setOfNotNull(finalData.e164Record, finalData.pniSidRecord, finalData.aciSidRecord)
assertEquals("There's still multiple records in the resulting record set! $finalRecords", 1, finalRecords.size)
finalRecords.firstOrNull { record -> record.e164 == output.e164 && record.pni == output.pni && record.serviceId == (output.aci ?: output.pni) }
?: throw AssertionError("Expected output was not found in the result set! Expected: $output")
return PnpMatchResult(
ids = ids,
changeSet = changeSet
)
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
const val E164_C = "+14441234567"
}
}

View File

@@ -11,9 +11,14 @@ import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest {
class RecipientTableTest {
@get:Rule
val harness = SignalActivityRule()
@@ -38,7 +43,7 @@ class RecipientDatabaseTest {
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(false)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
@@ -79,7 +84,7 @@ class RecipientDatabaseTest {
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(false)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
@@ -109,7 +114,7 @@ class RecipientDatabaseTest {
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(false)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
@@ -150,7 +155,7 @@ class RecipientDatabaseTest {
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(false)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
@@ -159,4 +164,47 @@ class RecipientDatabaseTest {
assertNotEquals(0, results.size)
assertFalse(blockedRecipient in results)
}
@Test
fun givenARecipientWithPniAndAci_whenIMarkItUnregistered_thenIExpectItToBeSplit() {
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
SignalDatabase.recipients.markUnregistered(mainId)
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
assertEquals(mainId, byAci)
assertEquals(byE164, byPni)
assertNotEquals(byAci, byE164)
}
@Test
fun givenARecipientWithPniAndAci_whenISplitItForStorageSync_thenIExpectItToBeSplit() {
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
val mainRecord = SignalDatabase.recipients.getRecord(mainId)
SignalDatabase.recipients.splitForStorageSync(mainRecord.storageId!!)
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
assertEquals(mainId, byAci)
assertEquals(byE164, byPni)
assertNotEquals(byAci, byE164)
}
companion object {
val ACI_A = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
val PNI_A = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
const val E164_A = "+12222222222"
}
}

View File

@@ -180,4 +180,45 @@ class SQLiteDatabaseTest {
assertTrue(hasRun1.get())
assertTrue(hasRun2.get())
}
@Test
fun runPostSuccessfulTransaction_runsAfterMainTransactionInNestedTransaction() {
val hasRun1 = AtomicBoolean(false)
val hasRun2 = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction {
assertFalse(hasRun1.get())
assertFalse(hasRun2.get())
hasRun1.set(true)
}
assertFalse(hasRun1.get())
assertFalse(hasRun2.get())
db.beginTransaction()
db.runPostSuccessfulTransaction {
assertTrue(hasRun1.get())
assertFalse(hasRun2.get())
hasRun2.set(true)
}
db.setTransactionSuccessful()
assertFalse(hasRun1.get())
assertFalse(hasRun2.get())
db.endTransaction()
db.setTransactionSuccessful()
assertFalse(hasRun1.get())
assertFalse(hasRun2.get())
db.endTransaction()
assertTrue(hasRun1.get())
assertTrue(hasRun2.get())
}
}

View File

@@ -31,8 +31,8 @@ import java.util.UUID
@RunWith(AndroidJUnit4::class)
class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
private lateinit var recipients: RecipientDatabase
private lateinit var sms: SmsDatabase
private lateinit var recipients: RecipientTable
private lateinit var sms: MessageTable
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@@ -45,7 +45,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
@Before
fun setUp() {
recipients = SignalDatabase.recipients
sms = SignalDatabase.sms
sms = SignalDatabase.messages
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
@@ -163,7 +163,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
*/
@Test
fun previousJoinRequestCollapse() {
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
@@ -197,7 +197,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
fun previousJoinThenTextCollapse() {
val secondLatestMessage = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get()
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
@@ -231,7 +231,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
*/
@Test
fun previousCollapseAndJoinRequestDoubleCollapse() {
val secondLatestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
val secondLatestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
@@ -243,7 +243,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
)
).get()
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {

View File

@@ -10,6 +10,7 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -22,7 +23,7 @@ import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class StorySendsDatabaseTest {
class StorySendTableTest {
private val distributionId1 = DistributionId.from(UUID.randomUUID())
private val distributionId2 = DistributionId.from(UUID.randomUUID())
@@ -45,7 +46,7 @@ class StorySendsDatabaseTest {
private var messageId2: Long = 0
private var messageId3: Long = 0
private lateinit var storySends: StorySendsDatabase
private lateinit var storySends: StorySendTable
@Before
fun setup() {
@@ -64,17 +65,17 @@ class StorySendsDatabaseTest {
messageId1 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
storyType = StoryType.STORY_WITHOUT_REPLIES
)
messageId2 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITH_REPLIES,
storyType = StoryType.STORY_WITH_REPLIES
)
messageId3 = MmsHelper.insert(
recipient = distributionListRecipient3,
storyType = StoryType.STORY_WITHOUT_REPLIES,
storyType = StoryType.STORY_WITHOUT_REPLIES
)
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
@@ -183,7 +184,7 @@ class StorySendsDatabaseTest {
@Test
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
SignalDatabase.messages.markAsRemoteDelete(messageId1)
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
@@ -280,19 +281,20 @@ class StorySendsDatabaseTest {
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNonNullResult() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
SignalDatabase.messages.markAsRemoteDelete(messageId1)
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)!!
assertNotNull(manifest)
}
/*
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetRecipientIdsForManifestUpdate_thenIExpectOnlyRecipientsWithStory2() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId1, recipients11to20, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
SignalDatabase.messages.markAsRemoteDelete(messageId1)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
@@ -304,7 +306,7 @@ class StorySendsDatabaseTest {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
SignalDatabase.messages.markAsRemoteDelete(messageId1)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
@@ -317,14 +319,14 @@ class StorySendsDatabaseTest {
fun givenTwoStoriesAndTheOneThatAllowedRepliesIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectAllowRepliesToBeTrue() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId2)
SignalDatabase.messages.markAsRemoteDelete(messageId2)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
assertTrue(results.entries.all { it.allowedToReply })
}
*/
@Test
fun givenEmptyManifest_whenIApplyRemoteManifest_thenNothingChanges() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
@@ -354,8 +356,8 @@ class StorySendsDatabaseTest {
assertEquals(expected, result)
}
@Test
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeMarkedRemoteDeleted() {
@Test(expected = NoSuchMessageException::class)
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeDeleted() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
@@ -375,7 +377,8 @@ class StorySendsDatabaseTest {
storySends.applySentStoryManifest(remote, 200)
assertTrue(SignalDatabase.mms.getMessageRecord(messageId5).isRemoteDelete)
SignalDatabase.messages.getMessageRecord(messageId5)
fail("Expected messageId5 to no longer exist.")
}
@Test
@@ -399,7 +402,7 @@ class StorySendsDatabaseTest {
storySends.applySentStoryManifest(remote, 200)
assertFalse(SignalDatabase.mms.getMessageRecord(messageId4).isRemoteDelete)
assertFalse(SignalDatabase.messages.getMessageRecord(messageId4).isRemoteDelete)
}
@Test

View File

@@ -6,13 +6,14 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@Suppress("ClassName")
class ThreadDatabaseTest_pinned {
class ThreadTableTest_pinned {
@Rule
@JvmField
@@ -33,7 +34,7 @@ class ThreadDatabaseTest_pinned {
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.mms.deleteMessage(messageId)
SignalDatabase.messages.deleteMessage(messageId)
// THEN
val pinned = SignalDatabase.threads.getPinnedThreadIds()
@@ -48,10 +49,10 @@ class ThreadDatabaseTest_pinned {
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.mms.deleteMessage(messageId)
SignalDatabase.messages.deleteMessage(messageId)
// THEN
val unarchivedCount = SignalDatabase.threads.getUnarchivedConversationListCount()
val unarchivedCount = SignalDatabase.threads.getUnarchivedConversationListCount(ConversationFilter.OFF)
assertEquals(1, unarchivedCount)
}
@@ -63,12 +64,12 @@ class ThreadDatabaseTest_pinned {
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.mms.deleteMessage(messageId)
SignalDatabase.messages.deleteMessage(messageId)
// THEN
SignalDatabase.threads.getUnarchivedConversationList(true, 0, 1).use {
SignalDatabase.threads.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 1).use {
it.moveToFirst()
assertEquals(threadId, CursorUtil.requireLong(it, ThreadDatabase.ID))
assertEquals(threadId, CursorUtil.requireLong(it, ThreadTable.ID))
}
}
}

View File

@@ -15,7 +15,7 @@ import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class ThreadDatabaseTest_recents {
class ThreadTableTest_recents {
@Rule
@JvmField
@@ -40,7 +40,7 @@ class ThreadDatabaseTest_recents {
val results: MutableList<RecipientId> = SignalDatabase.threads.getRecentConversationList(10, false, false, false, false, false, false).use { cursor ->
val ids = mutableListOf<RecipientId>()
while (cursor.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(cursor, ThreadDatabase.RECIPIENT_ID)))
ids.add(RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)))
}
ids

View File

@@ -11,7 +11,7 @@ object UriAttachmentBuilder {
id: Long,
uri: Uri = Uri.parse("content://$id"),
contentType: String,
transferState: Int = AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
transferState: Int = AttachmentTable.TRANSFER_PROGRESS_PENDING,
size: Long = 0L,
fileName: String = "file$id",
voiceNote: Boolean = false,
@@ -22,7 +22,7 @@ object UriAttachmentBuilder {
stickerLocator: StickerLocator? = null,
blurHash: BlurHash? = null,
audioHash: AudioHash? = null,
transformProperties: AttachmentDatabase.TransformProperties? = null
transformProperties: AttachmentTable.TransformProperties? = null
): UriAttachment {
return UriAttachment(
uri,

View File

@@ -10,7 +10,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.SqlUtil
import org.thoughtcrime.securesms.database.DistributionListDatabase
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
@@ -72,9 +72,9 @@ class MyStoryMigrationTest {
private fun setMyStoryDistributionId(serializedId: String) {
SignalDatabase.rawDatabase.update(
DistributionListDatabase.LIST_TABLE_NAME,
DistributionListTables.LIST_TABLE_NAME,
contentValuesOf(
DistributionListDatabase.DISTRIBUTION_ID to serializedId
DistributionListTables.DISTRIBUTION_ID to serializedId
),
"_id = ?",
SqlUtil.buildArgs(DistributionListId.MY_STORY)
@@ -83,7 +83,7 @@ class MyStoryMigrationTest {
private fun deleteMyStory() {
SignalDatabase.rawDatabase.delete(
DistributionListDatabase.LIST_TABLE_NAME,
DistributionListTables.LIST_TABLE_NAME,
"_id = ?",
SqlUtil.buildArgs(DistributionListId.MY_STORY)
)
@@ -91,9 +91,9 @@ class MyStoryMigrationTest {
private fun assertValidMyStoryExists() {
SignalDatabase.rawDatabase.query(
DistributionListDatabase.LIST_TABLE_NAME,
DistributionListTables.LIST_TABLE_NAME,
SqlUtil.COUNT,
"_id = ? AND ${DistributionListDatabase.DISTRIBUTION_ID} = ?",
"_id = ? AND ${DistributionListTables.DISTRIBUTION_ID} = ?",
SqlUtil.buildArgs(DistributionListId.MY_STORY, DistributionId.MY_STORY.toString()),
null,
null,

View File

@@ -2,26 +2,33 @@ package org.thoughtcrime.securesms.dependencies
import android.app.Application
import okhttp3.ConnectionSpec
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import okio.ByteString
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.KbsEnclave
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
import org.thoughtcrime.securesms.push.SignalServiceTrustStore
import org.thoughtcrime.securesms.recipients.LiveRecipientCache
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.thoughtcrime.securesms.util.Base64
import org.whispersystems.signalservice.api.KeyBackupService
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl
@@ -41,18 +48,26 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
private val uncensoredConfiguration: SignalServiceConfiguration
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
private val keyBackupService: KeyBackupService
private val recipientCache: LiveRecipientCache
init {
runSync {
webServer = MockWebServer()
baseUrl = webServer.url("").toString()
addMockWebRequestHandlers(
Get("/v1/websocket/?login=") {
MockResponse().success().withWebSocketUpgrade(mockIdentifiedWebSocket)
},
Get("/v1/websocket", { !it.path.contains("login") }) {
MockResponse().success().withWebSocketUpgrade(object : WebSocketListener() {})
}
)
}
webServer.setDispatcher(object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
val handler = handlers.firstOrNull {
request.method == it.verb && request.path.startsWith("/${it.path}")
}
val handler = handlers.firstOrNull { it.requestPredicate(request) }
return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500)
}
})
@@ -64,14 +79,15 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT))
),
arrayOf(SignalContactDiscoveryUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
arrayOf(SignalKeyBackupServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
emptyArray(),
emptyList(),
Optional.of(SignalServiceNetworkAccess.DNS),
Optional.empty(),
Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS)
Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS)
)
serviceNetworkAccessMock = mock {
@@ -81,6 +97,8 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
}
keyBackupService = mock()
recipientCache = LiveRecipientCache(application) { r -> r.run() }
}
override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess {
@@ -91,18 +109,55 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
return keyBackupService
}
override fun provideRecipientCache(): LiveRecipientCache {
return recipientCache
}
class MockWebSocket : WebSocketListener() {
private val TAG = "MockWebSocket"
var webSocket: WebSocket? = null
private set
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.i(TAG, "onOpen(${webSocket.hashCode()})")
this.webSocket = webSocket
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.i(TAG, "onClosing(${webSocket.hashCode()}): $code, $reason")
this.webSocket = null
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.i(TAG, "onClosed(${webSocket.hashCode()}): $code, $reason")
this.webSocket = null
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.w(TAG, "onFailure(${webSocket.hashCode()})", t)
this.webSocket = null
}
}
companion object {
lateinit var webServer: MockWebServer
private set
lateinit var baseUrl: String
private set
val mockIdentifiedWebSocket = MockWebSocket()
private val handlers: MutableList<Verb> = mutableListOf()
fun addMockWebRequestHandlers(vararg verbs: Verb) {
handlers.addAll(verbs)
}
fun injectWebSocketMessage(value: ByteString) {
mockIdentifiedWebSocket.webSocket!!.send(value)
}
fun clearHandlers() {
handlers.clear()
}

View File

@@ -69,7 +69,7 @@ class PreKeysSyncJobTest {
Put("/v2/keys/signed?identity=pni") { r ->
pniSignedPreKey = r.parsedRequestBody()
MockResponse().success()
},
}
)
// WHEN
@@ -107,7 +107,7 @@ class PreKeysSyncJobTest {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(100)) },
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(100)) }
)
// WHEN
@@ -134,7 +134,7 @@ class PreKeysSyncJobTest {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
Put("/v2/keys/signed?identity=pni") { MockResponse().success() },
Put("/v2/keys/signed?identity=pni") { MockResponse().success() }
)
// WHEN
@@ -173,7 +173,7 @@ class PreKeysSyncJobTest {
Put("/v2/keys/?identity=pni") { r ->
pniPreKeyStateRequest = r.parsedRequestBody()
MockResponse().success()
},
}
)
// WHEN

View File

@@ -0,0 +1,175 @@
package org.thoughtcrime.securesms.jobs
import androidx.test.ext.junit.runners.AndroidJUnit4
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.libsignal.usernames.Username
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.Put
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.failure
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
import org.whispersystems.util.Base64UrlSafe
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class RefreshOwnProfileJob__checkUsernameIsInSyncTest {
@get:Rule
val harness = SignalActivityRule()
@After
fun tearDown() {
InstrumentationApplicationDependencyProvider.clearHandlers()
SignalStore.phoneNumberPrivacy().clearUsernameOutOfSync()
}
@Test
fun givenNoLocalUsername_whenICheckUsernameIsInSync_thenIExpectNoFailures() {
// WHEN
RefreshOwnProfileJob.checkUsernameIsInSync()
}
@Test
fun givenLocalUsernameDoesNotMatchServerUsername_whenICheckUsernameIsInSync_thenIExpectRetry() {
// GIVEN
var didReserve = false
var didConfirm = false
val username = "hello.32"
val serverUsername = "hello.3232"
SignalDatabase.recipients.setUsername(harness.self.id, username)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/accounts/whoami") { r ->
MockResponse().success(
WhoAmIResponse().apply {
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(serverUsername))
}
)
},
Put("/v1/accounts/username_hash/reserve") { r ->
didReserve = true
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
},
Put("/v1/accounts/username_hash/confirm") { r ->
didConfirm = true
MockResponse().success()
}
)
// WHEN
RefreshOwnProfileJob.checkUsernameIsInSync()
// THEN
assertTrue(didReserve)
assertTrue(didConfirm)
assertFalse(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
}
@Test
fun givenLocalAndNoServer_whenICheckUsernameIsInSync_thenIExpectRetry() {
// GIVEN
var didReserve = false
var didConfirm = false
val username = "hello.32"
SignalDatabase.recipients.setUsername(harness.self.id, username)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/accounts/whoami") { r ->
MockResponse().success(WhoAmIResponse())
},
Put("/v1/accounts/username_hash/reserve") { r ->
didReserve = true
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
},
Put("/v1/accounts/username_hash/confirm") { r ->
didConfirm = true
MockResponse().success()
}
)
// WHEN
RefreshOwnProfileJob.checkUsernameIsInSync()
// THEN
assertTrue(didReserve)
assertTrue(didConfirm)
assertFalse(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
}
@Test
fun givenLocalAndServerMatch_whenICheckUsernameIsInSync_thenIExpectNoRetry() {
// GIVEN
var didReserve = false
var didConfirm = false
val username = "hello.32"
SignalDatabase.recipients.setUsername(harness.self.id, username)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/accounts/whoami") { r ->
MockResponse().success(
WhoAmIResponse().apply {
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))
}
)
},
Put("/v1/accounts/username_hash/reserve") { r ->
didReserve = true
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
},
Put("/v1/accounts/username_hash/confirm") { r ->
didConfirm = true
MockResponse().success()
}
)
// WHEN
RefreshOwnProfileJob.checkUsernameIsInSync()
// THEN
assertFalse(didReserve)
assertFalse(didConfirm)
assertFalse(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
}
@Test
fun givenMismatchAndReservationFails_whenICheckUsernameIsInSync_thenIExpectNoConfirm() {
// GIVEN
var didReserve = false
var didConfirm = false
val username = "hello.32"
SignalDatabase.recipients.setUsername(harness.self.id, username)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/accounts/whoami") { r ->
MockResponse().success(
WhoAmIResponse().apply {
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash("${username}23"))
}
)
},
Put("/v1/accounts/username_hash/reserve") { r ->
didReserve = true
MockResponse().failure(418)
},
Put("/v1/accounts/username_hash/confirm") { r ->
didConfirm = true
MockResponse().success()
}
)
// WHEN
RefreshOwnProfileJob.checkUsernameIsInSync()
// THEN
assertTrue(didReserve)
assertFalse(didConfirm)
assertTrue(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
}
}

View File

@@ -5,9 +5,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.signal.core.util.Hex;
import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.signal.libsignal.svr2.PinHash;
import org.whispersystems.signalservice.api.kbs.KbsData;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.kbs.PinHashUtil;
import java.io.IOException;
@@ -24,16 +25,16 @@ public final class PinHashing_hashPin_Test {
byte[] backupId = Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"));
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
PinHash hashedPin = PinHashUtil.hashPin(pin, new byte[]{});
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
assertArrayEquals(Hex.fromStringCondensed("ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8"), kbsData.getKbsAccessKey());
assertArrayEquals(Hex.fromStringCondensed("3f33ce58eb25b40436592a30eae2a8fabab1899095f4e2fba6e2d0dc43b4a2d9cac5a3931748522393951e0e54dec769"), kbsData.getCipherText());
assertEquals(masterKey, kbsData.getMasterKey());
String localPinHash = PinHashing.localPinHash(pin);
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
String localPinHash = PinHashUtil.localPinHash(pin);
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
}
@Test
@@ -42,16 +43,16 @@ public final class PinHashing_hashPin_Test {
byte[] backupId = Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f");
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("88a787415a2ecd79da0d1016a82a27c5c695c9a19b88b0aa1d35683280aa9a67"));
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
PinHash hashedPin = PinHashUtil.hashPin(pin, new byte[]{});
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
assertArrayEquals(Hex.fromStringCondensed("301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b"), kbsData.getKbsAccessKey());
assertArrayEquals(Hex.fromStringCondensed("9d9b05402ea39c17ff1c9298c8a0e86784a352aa02a74943bf8bcf07ec0f4b574a5b786ad0182c8d308d9eb06538b8c9"), kbsData.getCipherText());
assertEquals(masterKey, kbsData.getMasterKey());
String localPinHash = PinHashing.localPinHash(pin);
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
String localPinHash = PinHashUtil.localPinHash(pin);
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
}
@Test
@@ -60,18 +61,18 @@ public final class PinHashing_hashPin_Test {
byte[] backupId = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8");
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("9571f3fde1e58588ba49bcf82be1b301ca3859a6f59076f79a8f47181ef952bf"));
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
PinHash hashedPin = PinHashUtil.hashPin(pin, new byte[]{});
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
assertArrayEquals(Hex.fromStringCondensed("ab645acdccc1652a48a34b2ac6926340ff35c03034013f68760f20013f028dd8"), kbsData.getKbsAccessKey());
assertArrayEquals(Hex.fromStringCondensed("11c0ba1834db15e47c172f6c987c64bd4cfc69c6047dd67a022afeec0165a10943f204d5b8f37b3cb7bab21c6dfc39c8"), kbsData.getCipherText());
assertEquals(masterKey, kbsData.getMasterKey());
assertEquals("577939bccb2b6638c39222d5a97998a867c5e154e30b82cc120f2dd07a3de987", kbsData.getMasterKey().deriveRegistrationLock());
String localPinHash = PinHashing.localPinHash(pin);
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
String localPinHash = PinHashUtil.localPinHash(pin);
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
}
@Test
@@ -80,18 +81,17 @@ public final class PinHashing_hashPin_Test {
byte[] backupId = Hex.fromStringCondensed("717dc111a98423a57196512606822fca646c653facd037c10728f14ba0be2ab3");
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("0432d735b32f66d0e3a70d4f9cc821a8529521a4937d26b987715d8eff4e4c54"));
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
PinHash hashedPin = PinHashUtil.hashPin(pin, new byte[]{});
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
assertArrayEquals(Hex.fromStringCondensed("d2fedabd0d4c17a371491c9722578843a26be3b4923e28d452ab2fc5491e794b"), kbsData.getKbsAccessKey());
assertArrayEquals(Hex.fromStringCondensed("877ef871ef1fc668401c717ef21aa12e8523579fb1ff4474b76f28c2293537c80cc7569996c9e0229bea7f378e3a824e"), kbsData.getCipherText());
assertEquals(masterKey, kbsData.getMasterKey());
assertEquals("23a75cb1df1a87df45cc2ed167c2bdc85ab1220b847c88761b0005cac907fce5", kbsData.getMasterKey().deriveRegistrationLock());
String localPinHash = PinHashing.localPinHash(pin);
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
String localPinHash = PinHashUtil.localPinHash(pin);
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
}
}

View File

@@ -0,0 +1,250 @@
package org.thoughtcrime.securesms.messages
import android.database.Cursor
import android.util.Base64
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.core.util.ThreadUtil
import org.signal.core.util.readToList
import org.signal.core.util.select
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.toBodyRangeList
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.MessageTableTestUtils
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage
import kotlin.time.Duration.Companion.seconds
@RunWith(AndroidJUnit4::class)
class EditMessageSyncProcessorTest {
companion object {
private val IGNORE_MESSAGE_COLUMNS = listOf(
MessageTable.DATE_RECEIVED,
MessageTable.NOTIFIED_TIMESTAMP,
MessageTable.REACTIONS_LAST_SEEN,
MessageTable.NOTIFIED
)
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
AttachmentTable.UNIQUE_ID,
AttachmentTable.TRANSFER_FILE
)
}
@get:Rule
val harness = SignalActivityRule()
private lateinit var processorV2: MessageContentProcessorV2
private lateinit var testResult: TestResults
private var envelopeTimestamp: Long = 0
@Before
fun setup() {
processorV2 = MessageContentProcessorV2(harness.context)
envelopeTimestamp = System.currentTimeMillis()
testResult = TestResults()
}
@Test
fun textMessage() {
var originalTimestamp = envelopeTimestamp + 200
for (i in 1..10) {
originalTimestamp += 400
val toRecipient = Recipient.resolved(harness.others[0])
val content = MessageContentFuzzer.fuzzTextMessage()
val metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, toRecipient.id)
val syncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
SignalServiceProtos.SyncMessage.newBuilder().setSent(
SignalServiceProtos.SyncMessage.Sent.newBuilder()
.setDestinationUuid(metadata.destinationServiceId.toString())
.setTimestamp(originalTimestamp)
.setExpirationStartTimestamp(originalTimestamp)
.setMessage(content.dataMessage)
)
).build()
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer)
val syncTextMessage = TestMessage(
envelope = MessageContentFuzzer.envelope(originalTimestamp),
content = syncContent,
metadata = metadata,
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(originalTimestamp)
)
val editTimestamp = originalTimestamp + 200
val editedContent = MessageContentFuzzer.fuzzTextMessage()
val editSyncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
SignalServiceProtos.SyncMessage.newBuilder().setSent(
SignalServiceProtos.SyncMessage.Sent.newBuilder()
.setDestinationUuid(metadata.destinationServiceId.toString())
.setTimestamp(editTimestamp)
.setExpirationStartTimestamp(editTimestamp)
.setEditMessage(
EditMessage.newBuilder()
.setDataMessage(editedContent.dataMessage)
.setTargetSentTimestamp(originalTimestamp)
)
)
).build()
val syncEditMessage = TestMessage(
envelope = MessageContentFuzzer.envelope(editTimestamp),
content = editSyncContent,
metadata = metadata,
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(editTimestamp)
)
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer / 1000)
val originalTextMessage = OutgoingMessage(
threadRecipient = toRecipient,
sentTimeMillis = originalTimestamp,
body = content.dataMessage.body,
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
isUrgent = true,
isSecure = true,
bodyRanges = content.dataMessage.bodyRangesList.toBodyRangeList()
)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient)
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(originalMessageId, true)
if (content.dataMessage.expireTimer > 0) {
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
}
val editMessage = OutgoingMessage(
threadRecipient = toRecipient,
sentTimeMillis = editTimestamp,
body = editedContent.dataMessage.body,
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
isUrgent = true,
isSecure = true,
bodyRanges = editedContent.dataMessage.bodyRangesList.toBodyRangeList(),
messageToEdit = originalMessageId
)
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(editMessageId, true)
if (content.dataMessage.expireTimer > 0) {
SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp)
}
testResult.collectLocal()
testResult.assert()
}
}
private inner class TestResults {
private lateinit var localMessages: List<List<Pair<String, String?>>>
private lateinit var localAttachments: List<List<Pair<String, String?>>>
private lateinit var syncMessages: List<List<Pair<String, String?>>>
private lateinit var syncAttachments: List<List<Pair<String, String?>>>
fun collectLocal() {
harness.inMemoryLogger.clear()
localMessages = dumpMessages()
localAttachments = dumpAttachments()
cleanup()
}
fun runSync(messages: List<TestMessage>) {
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
if (content.hasSyncMessage()) {
processorV2.process(
envelope,
content,
metadata,
serverDeliveredTimestamp,
false
)
ThreadUtil.sleep(1)
}
}
harness.inMemoryLogger.clear()
syncMessages = dumpMessages()
syncAttachments = dumpAttachments()
cleanup()
}
fun cleanup() {
SignalDatabase.rawDatabase.withinTransaction { db ->
SignalDatabase.threads.deleteAllConversations()
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${MessageTable.TABLE_NAME}'")
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${ThreadTable.TABLE_NAME}'")
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${AttachmentTable.TABLE_NAME}'")
}
}
fun assert() {
syncMessages.zip(localMessages)
.forEach { (v2, v1) ->
v2.assertIs(v1)
}
syncAttachments.zip(localAttachments)
.forEach { (v2, v1) ->
v2.assertIs(v1)
}
}
private fun dumpMessages(): List<List<Pair<String, String?>>> {
return dumpTable(MessageTable.TABLE_NAME)
.map { row ->
val newRow = row.toMutableList()
newRow.removeIf { IGNORE_MESSAGE_COLUMNS.contains(it.first) }
newRow
}
}
private fun dumpAttachments(): List<List<Pair<String, String?>>> {
return dumpTable(AttachmentTable.TABLE_NAME)
.map { row ->
val newRow = row.toMutableList()
newRow.removeIf { IGNORE_ATTACHMENT_COLUMNS.contains(it.first) }
newRow
}
}
private fun dumpTable(table: String): List<List<Pair<String, String?>>> {
return SignalDatabase.rawDatabase
.select()
.from(table)
.run()
.readToList { cursor ->
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
val index = cursor.getColumnIndex(column)
var data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
}
column to data
}
map
}
}
}
}

View File

@@ -30,7 +30,7 @@ abstract class MessageContentProcessorTest {
protected fun createNormalContentTestSubject(): MessageContentProcessor {
val context = ApplicationProvider.getApplicationContext<Application>()
return MessageContentProcessor.forNormalContent(context)
return MessageContentProcessor.create(context)
}
/**

View File

@@ -0,0 +1,313 @@
package org.thoughtcrime.securesms.messages
import android.database.Cursor
import android.util.Base64
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.core.util.ThreadUtil
import org.signal.core.util.readToList
import org.signal.core.util.select
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.Entry
import org.thoughtcrime.securesms.testing.InMemoryLogger
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.MessageTableTestUtils
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer
import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
import java.util.Optional
@RunWith(AndroidJUnit4::class)
class MessageContentProcessorTestV2 {
companion object {
private val TAGS = listOf(MessageContentProcessor.TAG, MessageContentProcessorV2.TAG, AttachmentTable.TAG)
private val GENERALIZE_TAG = mapOf(
MessageContentProcessor.TAG to "MCP",
MessageContentProcessorV2.TAG to "MCP",
AttachmentTable.TAG to AttachmentTable.TAG
)
private val IGNORE_MESSAGE_COLUMNS = listOf(
MessageTable.DATE_RECEIVED,
MessageTable.NOTIFIED_TIMESTAMP,
MessageTable.REACTIONS_LAST_SEEN,
MessageTable.NOTIFIED
)
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
AttachmentTable.UNIQUE_ID,
AttachmentTable.TRANSFER_FILE
)
}
@get:Rule
val harness = SignalActivityRule()
private lateinit var processorV1: MessageContentProcessor
private lateinit var processorV2: MessageContentProcessorV2
private lateinit var testResult: TestResults
private var envelopeTimestamp: Long = 0
@Before
fun setup() {
processorV1 = MessageContentProcessor(harness.context)
processorV2 = MessageContentProcessorV2(harness.context)
envelopeTimestamp = System.currentTimeMillis()
testResult = TestResults()
}
@Test
fun textMessage() {
var start = envelopeTimestamp
val messages: List<TestMessage> = (0 until 100).map {
start += 200
TestMessage(
envelope = MessageContentFuzzer.envelope(start),
content = MessageContentFuzzer.fuzzTextMessage(),
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
)
}
testResult.runV2(messages)
testResult.runV1(messages)
testResult.assert()
}
@Test
fun mediaMessage() {
var start = envelopeTimestamp
val textMessages: List<TestMessage> = (0 until 10).map {
start += 200
TestMessage(
envelope = MessageContentFuzzer.envelope(start),
content = MessageContentFuzzer.fuzzTextMessage(),
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
)
}
val firstBatchMediaMessages: List<TestMessage> = (0 until 10).map {
start += 200
TestMessage(
envelope = MessageContentFuzzer.envelope(start),
content = MessageContentFuzzer.fuzzMediaMessageWithBody(textMessages),
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
)
}
val secondBatchNoContentMediaMessages: List<TestMessage> = (0 until 10).map {
start += 200
TestMessage(
envelope = MessageContentFuzzer.envelope(start),
content = MessageContentFuzzer.fuzzMediaMessageNoContent(textMessages + firstBatchMediaMessages),
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
)
}
val thirdBatchNoTextMediaMessagesMessages: List<TestMessage> = (0 until 10).map {
start += 200
TestMessage(
envelope = MessageContentFuzzer.envelope(start),
content = MessageContentFuzzer.fuzzMediaMessageNoText(textMessages + firstBatchMediaMessages),
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
)
}
testResult.runV2(textMessages + firstBatchMediaMessages + secondBatchNoContentMediaMessages + thirdBatchNoTextMediaMessagesMessages)
testResult.runV1(textMessages + firstBatchMediaMessages + secondBatchNoContentMediaMessages + thirdBatchNoTextMediaMessagesMessages)
testResult.assert()
}
private inner class TestResults {
private lateinit var v1Logs: List<Entry>
private lateinit var v1Messages: List<List<Pair<String, String?>>>
private lateinit var v1Attachments: List<List<Pair<String, String?>>>
private lateinit var v2Logs: List<Entry>
private lateinit var v2Messages: List<List<Pair<String, String?>>>
private lateinit var v2Attachments: List<List<Pair<String, String?>>>
fun runV1(messages: List<TestMessage>) {
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
if (content.hasDataMessage()) {
processorV1.process(
MessageContentProcessor.MessageState.DECRYPTED_OK,
toSignalServiceContent(envelope, content, metadata, serverDeliveredTimestamp),
null,
envelope.timestamp,
-1
)
ThreadUtil.sleep(1)
}
}
v1Logs = harness.inMemoryLogger.logs()
harness.inMemoryLogger.clear()
v1Messages = dumpMessages()
v1Attachments = dumpAttachments()
}
fun runV2(messages: List<TestMessage>) {
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
if (content.hasDataMessage()) {
processorV2.process(
envelope,
content,
metadata,
serverDeliveredTimestamp,
false
)
ThreadUtil.sleep(1)
}
}
v2Logs = harness.inMemoryLogger.logs()
harness.inMemoryLogger.clear()
v2Messages = dumpMessages()
v2Attachments = dumpAttachments()
cleanup()
}
fun cleanup() {
SignalDatabase.rawDatabase.withinTransaction { db ->
SignalDatabase.threads.deleteAllConversations()
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${MessageTable.TABLE_NAME}'")
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${ThreadTable.TABLE_NAME}'")
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${AttachmentTable.TABLE_NAME}'")
}
}
fun assert() {
v2Logs.zip(v1Logs)
.forEach { (v2, v1) ->
GENERALIZE_TAG[v2.tag]!!.assertIs(GENERALIZE_TAG[v1.tag]!!)
if (v2.tag != AttachmentTable.TAG) {
if (v2.message?.startsWith("[") == true && v1.message?.startsWith("[") == false) {
v2.message!!.substring(v2.message!!.indexOf(']') + 2).assertIs(v1.message)
} else {
v2.message.assertIs(v1.message)
}
} else {
if (v2.message?.startsWith("Inserted attachment at ID: AttachmentId::") == true) {
v2.message!!
.substring(0, v2.message!!.indexOf(','))
.assertIs(
v1.message!!
.substring(0, v1.message!!.indexOf(','))
)
} else {
v2.message.assertIs(v1.message)
}
}
v2.throwable.assertIs(v1.throwable)
}
v2Messages.zip(v1Messages)
.forEach { (v2, v1) ->
v2.assertIs(v1)
}
v2Attachments.zip(v1Attachments)
.forEach { (v2, v1) ->
v2.assertIs(v1)
}
}
private fun InMemoryLogger.logs(): List<Entry> {
return entries()
.filter { TAGS.contains(it.tag) }
}
private fun dumpMessages(): List<List<Pair<String, String?>>> {
return dumpTable(MessageTable.TABLE_NAME)
.map { row ->
val newRow = row.toMutableList()
newRow.removeIf { IGNORE_MESSAGE_COLUMNS.contains(it.first) }
newRow
}
}
private fun dumpAttachments(): List<List<Pair<String, String?>>> {
return dumpTable(AttachmentTable.TABLE_NAME)
.map { row ->
val newRow = row.toMutableList()
newRow.removeIf { IGNORE_ATTACHMENT_COLUMNS.contains(it.first) }
newRow
}
}
private fun dumpTable(table: String): List<List<Pair<String, String?>>> {
return SignalDatabase.rawDatabase
.select()
.from(table)
.run()
.readToList { cursor ->
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
val index = cursor.getColumnIndex(column)
var data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
}
column to data
}
map
}
}
}
private fun toSignalServiceContent(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long): SignalServiceContent {
val localAddress = SignalServiceAddress(metadata.destinationServiceId, Optional.ofNullable(SignalStore.account().e164))
val signalServiceMetadata = SignalServiceMetadata(
SignalServiceAddress(metadata.sourceServiceId, Optional.ofNullable(metadata.sourceE164)),
metadata.sourceDeviceId,
envelope.timestamp,
envelope.serverTimestamp,
serverDeliveredTimestamp,
metadata.sealedSender,
envelope.serverGuid,
Optional.ofNullable(metadata.groupId),
metadata.destinationServiceId.toString()
)
val contentProto = SignalServiceContentProto.newBuilder()
.setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress))
.setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(signalServiceMetadata))
.setContent(content)
.build()
return SignalServiceContent.createFromProto(contentProto)!!
}
}

View File

@@ -0,0 +1,84 @@
package org.thoughtcrime.securesms.messages
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.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.MessageTableTestUtils
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class MessageContentProcessorV2__recipientStatusTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var processorV2: MessageContentProcessorV2
private var envelopeTimestamp: Long = 0
@Before
fun setup() {
processorV2 = MessageContentProcessorV2(harness.context)
envelopeTimestamp = System.currentTimeMillis()
}
/**
* Process sync group sent text transcript with partial send and then process second sync with recipient update
* flag set to true with the rest of the send completed.
*/
@Test
fun syncGroupSentTextMessageWithRecipientUpdateFollowup() {
val (groupId, masterKey, groupRecipientId) = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), harness.others[0].asMember(), harness.others[1].asMember())
val groupContextV2 = GroupContextV2.newBuilder().setRevision(0).setMasterKey(masterKey.serialize().toProtoByteString()).build()
val initialTextMessage = DataMessage.newBuilder().buildWith {
body = MessageContentFuzzer.string()
groupV2 = groupContextV2
timestamp = envelopeTimestamp
}
processorV2.process(
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
)
val threadId = SignalDatabase.threads.getThreadIdFor(groupRecipientId)!!
val firstSyncMessages = MessageTableTestUtils.getMessages(threadId)
val firstMessageId = firstSyncMessages[0].id
val firstReceiptInfo = SignalDatabase.groupReceipts.getGroupReceiptInfo(firstMessageId)
processorV2.process(
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0], harness.others[1]), recipientUpdate = true),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
)
val secondSyncMessages = MessageTableTestUtils.getMessages(threadId)
val secondReceiptInfo = SignalDatabase.groupReceipts.getGroupReceiptInfo(firstMessageId)
firstSyncMessages.size assertIs 1
firstSyncMessages[0].body assertIs initialTextMessage.body
firstReceiptInfo.first { it.recipientId == harness.others[0] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED
firstReceiptInfo.first { it.recipientId == harness.others[1] }.status assertIs GroupReceiptTable.STATUS_UNKNOWN
secondSyncMessages.size assertIs 1
secondSyncMessages[0].body assertIs initialTextMessage.body
secondReceiptInfo.first { it.recipientId == harness.others[0] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED
secondReceiptInfo.first { it.recipientId == harness.others[1] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED
}
}

View File

@@ -9,7 +9,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.database.MessageDatabase
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MmsHelper
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
@@ -30,12 +30,12 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
@Before
fun setUp() {
SignalDatabase.mms.deleteAllThreads()
SignalDatabase.messages.deleteAllThreads()
}
@After
fun tearDown() {
SignalDatabase.mms.deleteAllThreads()
SignalDatabase.messages.deleteAllThreads()
}
@Test
@@ -72,16 +72,16 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
runTestWithContent(contentProto = storyContent)
val replyId = SignalDatabase.mmsSms.getConversation(senderThreadId, 0, 1).use {
val replyId = SignalDatabase.messages.getConversation(senderThreadId, 0, 1).use {
it.moveToFirst()
it.requireLong(MessageDatabase.ID)
it.requireLong(MessageTable.ID)
}
val replyRecord = SignalDatabase.mms.getMessageRecord(replyId) as MediaMmsMessageRecord
val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord
assertEquals(ParentStoryId.DirectReply(storyMessageId).serialize(), replyRecord.parentStoryId!!.serialize())
assertEquals(expectedBody, replyRecord.body)
SignalDatabase.mms.deleteAllThreads()
SignalDatabase.messages.deleteAllThreads()
}
@Test
@@ -111,7 +111,7 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
decryptedGroupState
)
val groupRecipient = Recipient.externalGroupExact(group)
val groupRecipient = Recipient.externalGroupExact(group!!)
val threadForGroup = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
val insertResult = MmsHelper.insert(
@@ -137,19 +137,19 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
runTestWithContent(storyContent)
val replyId = SignalDatabase.mms.getStoryReplies(insertResult.get().messageId).use { cursor ->
val replyId = SignalDatabase.messages.getStoryReplies(insertResult.get().messageId).use { cursor ->
assertEquals(1, cursor.count)
cursor.moveToFirst()
cursor.requireLong(MessageDatabase.ID)
cursor.requireLong(MessageTable.ID)
}
val replyRecord = SignalDatabase.mms.getMessageRecord(replyId) as MediaMmsMessageRecord
val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord
assertEquals(ParentStoryId.GroupReply(insertResult.get().messageId).serialize(), replyRecord.parentStoryId?.serialize())
assertEquals(threadForGroup, replyRecord.threadId)
assertEquals(expectedBody, replyRecord.body)
SignalDatabase.mms.deleteGroupStoryReplies(insertResult.get().messageId)
SignalDatabase.mms.deleteAllThreads()
SignalDatabase.messages.deleteGroupStoryReplies(insertResult.get().messageId)
SignalDatabase.messages.deleteAllThreads()
}
/**
@@ -176,6 +176,6 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
private fun runTestWithContent(contentProto: SignalServiceContentProto) {
val content = SignalServiceContent.createFromProto(contentProto)
val testSubject = createNormalContentTestSubject()
testSubject.doProcess(content = content)
testSubject.doProcess(content = content!!)
}
}

View File

@@ -20,11 +20,11 @@ class MessageContentProcessor__handleTextMessageTest : MessageContentProcessorTe
val content = SignalServiceContent.createFromProto(contentProto)
// WHEN
testSubject.doProcess(content = content)
testSubject.doProcess(content = content!!)
// THEN
val record = SignalDatabase.sms.getMessageRecord(1)
val threadSize = SignalDatabase.mmsSms.getConversationCount(record.threadId)
val record = SignalDatabase.messages.getMessageRecord(1)
val threadSize = SignalDatabase.messages.getMessageCountForThread(record.threadId)
assertEquals(1, threadSize)
assertTrue(record.isSecure)

View File

@@ -0,0 +1,209 @@
package org.thoughtcrime.securesms.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import okio.ByteString
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.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.AliceClient
import org.thoughtcrime.securesms.testing.BobClient
import org.thoughtcrime.securesms.testing.Entry
import org.thoughtcrime.securesms.testing.FakeClientHelpers
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.awaitFor
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
import org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketMessage
import org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketRequestMessage
import java.util.regex.Pattern
import kotlin.random.Random
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import android.util.Log as AndroidLog
/**
* Sends N messages from Bob to Alice to track performance of Alice's processing of messages.
*/
// @Ignore("Ignore test in normal testing as it's a performance test with no assertions")
@RunWith(AndroidJUnit4::class)
class MessageProcessingPerformanceTest {
companion object {
private val TAG = Log.tag(MessageProcessingPerformanceTest::class.java)
private val TIMING_TAG = "TIMING_$TAG".substring(0..23)
private val DECRYPTION_TIME_PATTERN = Pattern.compile("^Decrypted (?<count>\\d+) envelopes in (?<duration>\\d+) ms.*$")
}
@get:Rule
val harness = SignalActivityRule()
private val trustRoot: ECKeyPair = Curve.generateKeyPair()
@Before
fun setup() {
mockkStatic(UnidentifiedAccessUtil::class)
every { UnidentifiedAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
mockkObject(MessageContentProcessorV2)
every { MessageContentProcessorV2.create(harness.application) } returns TimingMessageContentProcessorV2(harness.application)
}
@After
fun after() {
unmockkStatic(UnidentifiedAccessUtil::class)
unmockkStatic(MessageContentProcessorV2::class)
}
@Test
fun testPerformance() {
val aliceClient = AliceClient(
serviceId = harness.self.requireServiceId(),
e164 = harness.self.requireE164(),
trustRoot = trustRoot
)
val bob = Recipient.resolved(harness.others[0])
val bobClient = BobClient(
serviceId = bob.requireServiceId(),
e164 = bob.requireE164(),
identityKeyPair = harness.othersKeys[0],
trustRoot = trustRoot,
profileKey = ProfileKey(bob.profileKey)
)
// Send the initial messages to get past the prekey phase
establishSession(aliceClient, bobClient, bob)
// Have Bob generate N messages that will be received by Alice
val messageCount = 100
val envelopes = generateInboundEnvelopes(bobClient, messageCount)
val firstTimestamp = envelopes.first().timestamp
val lastTimestamp = envelopes.last().timestamp
// Inject the envelopes into the websocket
Thread {
for (envelope in envelopes) {
Log.i(TIMING_TAG, "Retrieved envelope! ${envelope.timestamp}")
InstrumentationApplicationDependencyProvider.injectWebSocketMessage(envelope.toWebSocketPayload())
}
InstrumentationApplicationDependencyProvider.injectWebSocketMessage(webSocketTombstone())
}.start()
// Wait until they've all been fully decrypted + processed
harness
.inMemoryLogger
.getLockForUntil(TimingMessageContentProcessorV2.endTagPredicate(lastTimestamp))
.awaitFor(1.minutes)
harness.inMemoryLogger.flush()
// Process logs for timing data
val entries = harness.inMemoryLogger.entries()
// Calculate decryption average
val totalDecryptDuration: Long = entries
.mapNotNull { entry -> entry.message?.let { DECRYPTION_TIME_PATTERN.matcher(it) } }
.filter { it.matches() }
.drop(1) // Ignore the first message, which represents the prekey exchange
.sumOf { it.group("duration")!!.toLong() }
AndroidLog.w(TAG, "Decryption: Average runtime: ${totalDecryptDuration.toFloat() / messageCount.toFloat()}ms")
// Calculate MessageContentProcessor
val takeLast: List<Entry> = entries.filter { it.tag == TimingMessageContentProcessorV2.TAG }.drop(2)
val iterator = takeLast.iterator()
var processCount = 0L
var processDuration = 0L
while (iterator.hasNext()) {
val start = iterator.next()
val end = iterator.next()
processCount++
processDuration += end.timestamp - start.timestamp
}
AndroidLog.w(TAG, "MessageContentProcessor.process: Average runtime: ${processDuration.toFloat() / processCount.toFloat()}ms")
// Calculate messages per second from "retrieving" first message post session initialization to processing last message
val start = entries.first { it.message == "Retrieved envelope! $firstTimestamp" }
val end = entries.first { it.message == TimingMessageContentProcessorV2.endTag(lastTimestamp) }
val duration = (end.timestamp - start.timestamp).toFloat() / 1000f
val messagePerSecond = messageCount.toFloat() / duration
AndroidLog.w(TAG, "Processing $messageCount messages took ${duration}s or ${messagePerSecond}m/s")
}
private fun establishSession(aliceClient: AliceClient, bobClient: BobClient, bob: Recipient) {
// Send message from Bob to Alice (self)
val firstPreKeyMessageTimestamp = System.currentTimeMillis()
val encryptedEnvelope = bobClient.encrypt(firstPreKeyMessageTimestamp)
val aliceProcessFirstMessageLatch = harness
.inMemoryLogger
.getLockForUntil(TimingMessageContentProcessorV2.endTagPredicate(firstPreKeyMessageTimestamp))
Thread { aliceClient.process(encryptedEnvelope, System.currentTimeMillis()) }.start()
aliceProcessFirstMessageLatch.awaitFor(15.seconds)
// Send message from Alice to Bob
val aliceNow = System.currentTimeMillis()
bobClient.decrypt(aliceClient.encrypt(aliceNow, bob), aliceNow)
}
private fun generateInboundEnvelopes(bobClient: BobClient, count: Int): List<Envelope> {
val envelopes = ArrayList<Envelope>(count)
var now = System.currentTimeMillis()
for (i in 0..count) {
envelopes += bobClient.encrypt(now)
now += 3
}
return envelopes
}
private fun webSocketTombstone(): ByteString {
return WebSocketMessage
.newBuilder()
.setRequest(
WebSocketRequestMessage.newBuilder()
.setVerb("PUT")
.setPath("/api/v1/queue/empty")
)
.build()
.toByteArray()
.toByteString()
}
private fun Envelope.toWebSocketPayload(): ByteString {
return WebSocketMessage
.newBuilder()
.setType(WebSocketMessage.Type.REQUEST)
.setRequest(
WebSocketRequestMessage.newBuilder()
.setVerb("PUT")
.setPath("/api/v1/message")
.setId(Random(System.currentTimeMillis()).nextLong())
.addHeaders("X-Signal-Timestamp: ${this.timestamp}")
.setBody(this.toByteString())
)
.build()
.toByteArray()
.toByteString()
}
}

View File

@@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.messages
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
data class TestMessage(
val envelope: SignalServiceProtos.Envelope,
val content: SignalServiceProtos.Content,
val metadata: EnvelopeMetadata,
val serverDeliveredTimestamp: Long
)

View File

@@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.messages
import android.content.Context
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.testing.LogPredicate
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
class TimingMessageContentProcessorV2(context: Context) : MessageContentProcessorV2(context) {
companion object {
val TAG = Log.tag(TimingMessageContentProcessorV2::class.java)
fun endTagPredicate(timestamp: Long): LogPredicate = { entry ->
entry.tag == TAG && entry.message == endTag(timestamp)
}
private fun startTag(timestamp: Long) = "$timestamp start"
fun endTag(timestamp: Long) = "$timestamp end"
}
override fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean) {
Log.d(TAG, startTag(envelope.timestamp))
super.process(envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent)
Log.d(TAG, endTag(envelope.timestamp))
}
}

View File

@@ -73,6 +73,7 @@ class UsernameEditFragmentTest {
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Ignore("Flakey espresso test.")
@Test
fun testUsernameCreationOutsideOfRegistration() {
val scenario = createScenario()
@@ -99,7 +100,7 @@ class UsernameEditFragmentTest {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/accounts/username/reserved") {
MockResponse().success(ReserveUsernameResponse(username, "reservationToken"))
MockResponse().success(ReserveUsernameResponse(username))
},
Put("/v1/accounts/username/confirm") {
MockResponse().success()

View File

@@ -27,13 +27,13 @@ class SafetyNumberBottomSheetRepositoryTest {
@Test
fun givenIOnlyHave1to1Destinations_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
val recipients = harness.others
val destinations = harness.others.map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
val destinations = harness.others.map { ContactSearchKey.RecipientSearchKey(it, false) }
val result = subjectUnderTest.getBuckets(recipients, destinations).test()
testScheduler.triggerActions()
result.assertValueAt(1) { map ->
result.assertValueAt(0) { map ->
assertMatch(map, mapOf(SafetyNumberBucket.ContactsBucket to harness.others))
}
}
@@ -42,7 +42,7 @@ class SafetyNumberBottomSheetRepositoryTest {
fun givenIOnlyHaveASingle1to1Destination_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
// GIVEN
val recipients = harness.others
val destination = harness.others.take(1).map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
val destination = harness.others.take(1).map { ContactSearchKey.RecipientSearchKey(it, false) }
// WHEN
val result = subjectUnderTest.getBuckets(recipients, destination).test(1)
@@ -59,7 +59,7 @@ class SafetyNumberBottomSheetRepositoryTest {
// GIVEN
val distributionListMembers = harness.others.take(5)
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
// WHEN
val result = subjectUnderTest.getBuckets(harness.others, listOf(destinationKey)).test(1)
@@ -82,7 +82,7 @@ class SafetyNumberBottomSheetRepositoryTest {
val distributionListMembers = harness.others.take(5)
val toRemove = distributionListMembers.last()
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
testScheduler.triggerActions()
@@ -108,7 +108,7 @@ class SafetyNumberBottomSheetRepositoryTest {
// GIVEN
val distributionListMembers = harness.others.take(5)
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
testScheduler.triggerActions()

View File

@@ -0,0 +1,127 @@
package org.thoughtcrime.securesms.storage
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.update
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.storage.SignalContactRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class ContactRecordProcessorTest {
@Before
fun setup() {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
}
@Test
fun process_splitContact_normalSplit() {
// GIVEN
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
setStorageId(originalId, STORAGE_ID_A)
val remote1 = buildRecord(STORAGE_ID_B) {
setServiceId(ACI_A.toString())
setUnregisteredAtTimestamp(100)
}
val remote2 = buildRecord(STORAGE_ID_C) {
setServiceId(PNI_A.toString())
setServicePni(PNI_A.toString())
setServiceE164(E164_A)
}
// WHEN
val subject = ContactRecordProcessor()
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
// THEN
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
assertEquals(originalId, byAci)
assertEquals(byE164, byPni)
assertNotEquals(byAci, byE164)
}
@Test
fun process_splitContact_doNotSplitIfAciRecordIsRegistered() {
// GIVEN
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
setStorageId(originalId, STORAGE_ID_A)
val remote1 = buildRecord(STORAGE_ID_B) {
setServiceId(ACI_A.toString())
setUnregisteredAtTimestamp(0)
}
val remote2 = buildRecord(STORAGE_ID_C) {
setServiceId(PNI_A.toString())
setServicePni(PNI_A.toString())
setServiceE164(E164_A)
}
// WHEN
val subject = ContactRecordProcessor()
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
// THEN
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
assertEquals(originalId, byAci)
assertEquals(byE164, byPni)
assertEquals(byAci, byE164)
}
private fun buildRecord(id: StorageId, applyParams: ContactRecord.Builder.() -> ContactRecord.Builder): SignalContactRecord {
return SignalContactRecord(id, ContactRecord.getDefaultInstance().toBuilder().applyParams().build())
}
private fun setStorageId(recipientId: RecipientId, storageId: StorageId) {
SignalDatabase.rawDatabase
.update(RecipientTable.TABLE_NAME)
.values(RecipientTable.STORAGE_SERVICE_ID to Base64.encodeBytes(storageId.raw))
.where("${RecipientTable.ID} = ?", recipientId)
.run()
}
companion object {
val ACI_A = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("bbbb0000-0b60-4a68-9cd9-ed2f8453f9ed"))
val ACI_SELF = ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
val PNI_A = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("bbbb1111-cd55-40bf-adda-c35a85375533"))
val PNI_SELF = PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
const val E164_A = "+12222222222"
const val E164_B = "+13333333333"
const val E164_SELF = "+10000000000"
val STORAGE_ID_A: StorageId = StorageId.forContact(byteArrayOf(1, 2, 3, 4))
val STORAGE_ID_B: StorageId = StorageId.forContact(byteArrayOf(5, 6, 7, 8))
val STORAGE_ID_C: StorageId = StorageId.forContact(byteArrayOf(9, 10, 11, 12))
}
}

View File

@@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.testing
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.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
/**
* Welcome to Alice's Client.
*
* Alice represent the Android instrumentation test user. Unlike [BobClient] much less is needed here
* as it can make use of the standard Signal Android App infrastructure.
*/
class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECKeyPair) {
companion object {
val TAG = Log.tag(AliceClient::class.java)
}
private val aliceSenderCertificate = FakeClientHelpers.createCertificateFor(
trustRoot = trustRoot,
uuid = serviceId.uuid(),
e164 = e164,
deviceId = 1,
identityKey = SignalStore.account().aciIdentityKey.publicKey.publicKey,
expires = 31337
)
fun process(envelope: Envelope, serverDeliveredTimestamp: Long) {
val start = System.currentTimeMillis()
val bufferedStore = BufferedProtocolStore.create()
ApplicationDependencies.getIncomingMessageObserver()
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
?.mapNotNull { it.run() }
?.forEach { ApplicationDependencies.getJobManager().add(it) }
bufferedStore.flushToDisk()
val end = System.currentTimeMillis()
Log.d(TAG, "${end - start}")
}
fun encrypt(now: Long, destination: Recipient): Envelope {
return ApplicationDependencies.getSignalServiceMessageSender().getEncryptedMessage(
SignalServiceAddress(destination.requireServiceId(), destination.requireE164()),
FakeClientHelpers.getTargetUnidentifiedAccess(ProfileKeyUtil.getSelfProfileKey(), ProfileKey(destination.profileKey), aliceSenderCertificate),
1,
FakeClientHelpers.encryptedTextMessage(now),
false
).toEnvelope(now, destination.requireServiceId())
}
}

View File

@@ -0,0 +1,173 @@
package org.thoughtcrime.securesms.testing
import org.signal.core.util.readToSingleInt
import org.signal.core.util.select
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.SessionBuilder
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord
import org.signal.libsignal.protocol.state.IdentityKeyStore
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
import org.signal.libsignal.protocol.state.PreKeyBundle
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.state.SessionRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.database.OneTimePreKeyTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignedPreKeyTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
import org.whispersystems.signalservice.api.SignalSessionLock
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import java.util.Optional
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
/**
* Welcome to Bob's Client.
*
* Bob is a "fake" client that can start a session with the Android instrumentation test user (Alice).
*
* Bob can create a new session using a prekey bundle created from Alice's prekeys, send a message, decrypt
* a return message from Alice, and that'll start a standard Signal session with normal keys/ratcheting.
*/
class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair: IdentityKeyPair, val trustRoot: ECKeyPair, val profileKey: ProfileKey) {
private val serviceAddress = SignalServiceAddress(serviceId, e164)
private val registrationId = KeyHelper.generateRegistrationId(false)
private val aciStore = BobSignalServiceAccountDataStore(registrationId, identityKeyPair)
private val senderCertificate = FakeClientHelpers.createCertificateFor(trustRoot, serviceId.uuid(), e164, 1, identityKeyPair.publicKey.publicKey, 31337)
private val sessionLock = object : SignalSessionLock {
private val lock = ReentrantLock()
override fun acquire(): SignalSessionLock.Lock {
lock.lock()
return SignalSessionLock.Lock { lock.unlock() }
}
}
/** Inspired by SignalServiceMessageSender#getEncryptedMessage */
fun encrypt(now: Long): SignalServiceProtos.Envelope {
val envelopeContent = FakeClientHelpers.encryptedTextMessage(now)
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null)
if (!aciStore.containsSession(getAliceProtocolAddress())) {
val sessionBuilder = SignalSessionBuilder(sessionLock, SessionBuilder(aciStore, getAliceProtocolAddress()))
sessionBuilder.process(getAlicePreKeyBundle())
}
return cipher.encrypt(getAliceProtocolAddress(), getAliceUnidentifiedAccess(), envelopeContent)
.toEnvelope(envelopeContent.content.get().dataMessage.timestamp, getAliceServiceId())
}
fun decrypt(envelope: SignalServiceProtos.Envelope, serverDeliveredTimestamp: Long) {
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator())
cipher.decrypt(envelope, serverDeliveredTimestamp)
}
private fun getAliceServiceId(): ServiceId {
return SignalStore.account().requireAci()
}
private fun getAlicePreKeyBundle(): PreKeyBundle {
val selfPreKeyId = SignalDatabase.rawDatabase
.select(OneTimePreKeyTable.KEY_ID)
.from(OneTimePreKeyTable.TABLE_NAME)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ?", getAliceServiceId().toString())
.run()
.readToSingleInt(-1)
val selfPreKeyRecord = SignalDatabase.oneTimePreKeys.get(getAliceServiceId(), selfPreKeyId)!!
val selfSignedPreKeyId = SignalDatabase.rawDatabase
.select(SignedPreKeyTable.KEY_ID)
.from(SignedPreKeyTable.TABLE_NAME)
.where("${SignedPreKeyTable.ACCOUNT_ID} = ?", getAliceServiceId().toString())
.run()
.readToSingleInt(-1)
val selfSignedPreKeyRecord = SignalDatabase.signedPreKeys.get(getAliceServiceId(), selfSignedPreKeyId)!!
return PreKeyBundle(
SignalStore.account().registrationId,
1,
selfPreKeyId,
selfPreKeyRecord.keyPair.publicKey,
selfSignedPreKeyId,
selfSignedPreKeyRecord.keyPair.publicKey,
selfSignedPreKeyRecord.signature,
getAlicePublicKey()
)
}
private fun getAliceProtocolAddress(): SignalProtocolAddress {
return SignalProtocolAddress(SignalStore.account().requireAci().toString(), 1)
}
private fun getAlicePublicKey(): IdentityKey {
return SignalStore.account().aciIdentityKey.publicKey
}
private fun getAliceProfileKey(): ProfileKey {
return ProfileKeyUtil.getSelfProfileKey()
}
private fun getAliceUnidentifiedAccess(): Optional<UnidentifiedAccess> {
return FakeClientHelpers.getTargetUnidentifiedAccess(profileKey, getAliceProfileKey(), senderCertificate)
}
private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore {
private var aliceSessionRecord: SessionRecord? = null
override fun getIdentityKeyPair(): IdentityKeyPair = identityKeyPair
override fun getLocalRegistrationId(): Int = registrationId
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 getSubDeviceSessions(name: String?): List<Int> = emptyList()
override fun containsSession(address: SignalProtocolAddress?): Boolean = aliceSessionRecord != null
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account().aciIdentityKey.publicKey
override fun loadPreKey(preKeyId: Int): PreKeyRecord = throw UnsupportedOperationException()
override fun storePreKey(preKeyId: Int, record: PreKeyRecord?) = throw UnsupportedOperationException()
override fun containsPreKey(preKeyId: Int): Boolean = throw UnsupportedOperationException()
override fun removePreKey(preKeyId: Int) = throw UnsupportedOperationException()
override fun loadExistingSessions(addresses: MutableList<SignalProtocolAddress>?): MutableList<SessionRecord> = throw UnsupportedOperationException()
override fun deleteSession(address: SignalProtocolAddress?) = throw UnsupportedOperationException()
override fun deleteAllSessions(name: String?) = throw UnsupportedOperationException()
override fun loadSignedPreKey(signedPreKeyId: Int): SignedPreKeyRecord = throw UnsupportedOperationException()
override fun loadSignedPreKeys(): MutableList<SignedPreKeyRecord> = throw UnsupportedOperationException()
override fun storeSignedPreKey(signedPreKeyId: Int, record: SignedPreKeyRecord?) = throw UnsupportedOperationException()
override fun containsSignedPreKey(signedPreKeyId: Int): Boolean = throw UnsupportedOperationException()
override fun removeSignedPreKey(signedPreKeyId: Int) = throw UnsupportedOperationException()
override fun loadKyberPreKey(kyberPreKeyId: Int): KyberPreKeyRecord = throw UnsupportedOperationException()
override fun loadKyberPreKeys(): MutableList<KyberPreKeyRecord> = throw UnsupportedOperationException()
override fun storeKyberPreKey(kyberPreKeyId: Int, record: KyberPreKeyRecord?) = throw UnsupportedOperationException()
override fun containsKyberPreKey(kyberPreKeyId: Int): Boolean = throw UnsupportedOperationException()
override fun markKyberPreKeyUsed(kyberPreKeyId: Int) = throw UnsupportedOperationException()
override fun storeSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?, record: SenderKeyRecord?) = throw UnsupportedOperationException()
override fun loadSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?): SenderKeyRecord = throw UnsupportedOperationException()
override fun archiveSession(address: SignalProtocolAddress?) = throw UnsupportedOperationException()
override fun getAllAddressesWithActiveSessions(addressNames: MutableList<String>?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
override fun getSenderKeySharedWith(distributionId: DistributionId?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
override fun markSenderKeySharedWith(distributionId: DistributionId?, addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
override fun clearSenderKeySharedWith(addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
}
}

View File

@@ -0,0 +1,79 @@
package org.thoughtcrime.securesms.testing
import org.signal.libsignal.internal.Native
import org.signal.libsignal.internal.NativeHandleGuard
import org.signal.libsignal.metadata.certificate.CertificateValidator
import org.signal.libsignal.metadata.certificate.SenderCertificate
import org.signal.libsignal.metadata.certificate.ServerCertificate
import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
import org.whispersystems.util.Base64
import java.util.Optional
import java.util.UUID
object FakeClientHelpers {
val noOpCertificateValidator = object : CertificateValidator(null) {
override fun validate(certificate: SenderCertificate, validationTime: Long) = Unit
}
fun createCertificateFor(trustRoot: ECKeyPair, uuid: UUID, e164: String, deviceId: Int, identityKey: ECPublicKey, expires: Long): SenderCertificate {
val serverKey: ECKeyPair = Curve.generateKeyPair()
NativeHandleGuard(serverKey.publicKey).use { serverPublicGuard ->
NativeHandleGuard(trustRoot.privateKey).use { trustRootPrivateGuard ->
val serverCertificate = ServerCertificate(Native.ServerCertificate_New(1, serverPublicGuard.nativeHandle(), trustRootPrivateGuard.nativeHandle()))
NativeHandleGuard(identityKey).use { identityGuard ->
NativeHandleGuard(serverCertificate).use { serverCertificateGuard ->
NativeHandleGuard(serverKey.privateKey).use { serverPrivateGuard ->
return SenderCertificate(Native.SenderCertificate_New(uuid.toString(), e164, deviceId, identityGuard.nativeHandle(), expires, serverCertificateGuard.nativeHandle(), serverPrivateGuard.nativeHandle()))
}
}
}
}
}
}
fun getTargetUnidentifiedAccess(myProfileKey: ProfileKey, theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): Optional<UnidentifiedAccess> {
val selfUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(myProfileKey)
val themUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
return UnidentifiedAccessPair(UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate.serialized, false), UnidentifiedAccess(themUnidentifiedAccessKey, senderCertificate.serialized, false)).targetUnidentifiedAccess
}
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {
val content = SignalServiceProtos.Content.newBuilder().apply {
setDataMessage(
SignalServiceProtos.DataMessage.newBuilder().apply {
body = message
timestamp = now
}
)
}
return EnvelopeContent.encrypted(content.build(), ContentHint.RESENDABLE, Optional.empty())
}
fun OutgoingPushMessage.toEnvelope(timestamp: Long, destination: ServiceId): Envelope {
return Envelope.newBuilder()
.setType(Envelope.Type.valueOf(this.type))
.setSourceDevice(1)
.setTimestamp(timestamp)
.setServerTimestamp(timestamp + 1)
.setDestinationUuid(destination.toString())
.setServerGuid(UUID.randomUUID().toString())
.setContent(Base64.decode(this.content).toProtoByteString())
.setUrgent(true)
.setStory(false)
.build()
}
}

View File

@@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.testing
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId
import kotlin.random.Random
/**
* Helper methods for creating groups for message processing tests et al.
*/
object GroupTestingUtils {
fun member(serviceId: ServiceId, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
return DecryptedMember.newBuilder()
.setUuid(serviceId.toByteString())
.setJoinedAtRevision(revision)
.setRole(role)
.build()
}
fun insertGroup(revision: Int = 0, vararg members: DecryptedMember): TestGroupInfo {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.newBuilder()
.addAllMembers(members.toList())
.setRevision(revision)
.setTitle(MessageContentFuzzer.string())
.build()
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!!
val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId)
SignalDatabase.recipients.setProfileSharing(groupRecipientId, true)
return TestGroupInfo(groupId, groupMasterKey, groupRecipientId)
}
fun RecipientId.asMember(): DecryptedMember {
return Recipient.resolved(this).asMember()
}
fun Recipient.asMember(): DecryptedMember {
return member(serviceId = requireServiceId())
}
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId)
}

View File

@@ -0,0 +1,97 @@
package org.thoughtcrime.securesms.testing
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import java.util.concurrent.CountDownLatch
typealias LogPredicate = (Entry) -> Boolean
/**
* Logging implementation that holds logs in memory as they are added to be retrieve at a later time by a test.
* Can also be used for multithreaded synchronization and waiting until certain logs are emitted before continuing
* a test.
*/
class InMemoryLogger : Log.Logger() {
private val executor = SignalExecutors.newCachedSingleThreadExecutor("inmemory-logger", ThreadUtil.PRIORITY_BACKGROUND_THREAD)
private val predicates = mutableListOf<LogPredicate>()
private val logEntries = mutableListOf<Entry>()
override fun v(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Verbose(tag, message, t, System.currentTimeMillis()))
override fun d(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Debug(tag, message, t, System.currentTimeMillis()))
override fun i(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Info(tag, message, t, System.currentTimeMillis()))
override fun w(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Warn(tag, message, t, System.currentTimeMillis()))
override fun e(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) = add(Error(tag, message, t, System.currentTimeMillis()))
override fun flush() {
val latch = CountDownLatch(1)
executor.execute { latch.countDown() }
latch.await()
}
fun clear() {
val latch = CountDownLatch(1)
executor.execute {
predicates.clear()
logEntries.clear()
latch.countDown()
}
latch.await()
}
private fun add(entry: Entry) {
executor.execute {
logEntries += entry
val iterator = predicates.iterator()
while (iterator.hasNext()) {
val predicate = iterator.next()
if (predicate(entry)) {
iterator.remove()
}
}
}
}
/** Blocks until a snapshot of all log entries can be taken in a thread-safe way. */
fun entries(): List<Entry> {
val latch = CountDownLatch(1)
var entries: List<Entry> = emptyList()
executor.execute {
entries = logEntries.toList()
latch.countDown()
}
latch.await()
return entries
}
/** Returns a countdown latch that'll fire at a future point when an [Entry] is received that matches the predicate. */
fun getLockForUntil(predicate: LogPredicate): CountDownLatch {
val latch = CountDownLatch(1)
executor.execute {
predicates += { entry ->
if (predicate(entry)) {
latch.countDown()
true
} else {
false
}
}
}
return latch
}
}
sealed interface Entry {
val tag: String
val message: String?
val throwable: Throwable?
val timestamp: Long
}
data class Verbose(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry
data class Debug(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry
data class Info(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry
data class Warn(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry
data class Error(override val tag: String, override val message: String?, override val throwable: Throwable?, override val timestamp: Long) : Entry

View File

@@ -0,0 +1,259 @@
package org.thoughtcrime.securesms.testing
import com.google.protobuf.ByteString
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.thoughtcrime.securesms.messages.TestMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
import java.util.UUID
import kotlin.random.Random
import kotlin.random.nextInt
import kotlin.time.Duration.Companion.days
/**
* Random but deterministic fuzzer for create various message content protos.
*/
object MessageContentFuzzer {
private val mediaTypes = listOf("image/png", "image/jpeg", "image/heic", "image/heif", "image/avif", "image/webp", "image/gif", "audio/aac", "audio/*", "video/mp4", "video/*", "text/x-vcard", "text/x-signal-plain", "application/x-signal-view-once", "*/*", "application/octet-stream")
private val emojis = listOf("😂", "❤️", "🔥", "😍", "👀", "🤔", "🙏", "👍", "🤷", "🥺")
private val random = Random(1)
/**
* Create an [Envelope].
*/
fun envelope(timestamp: Long): Envelope {
return Envelope.newBuilder()
.setTimestamp(timestamp)
.setServerTimestamp(timestamp + 5)
.setServerGuidBytes(UuidUtil.toByteString(UUID.randomUUID()))
.build()
}
/**
* Create metadata to match an [Envelope].
*/
fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata {
return EnvelopeMetadata(
sourceServiceId = Recipient.resolved(source).requireServiceId(),
sourceE164 = null,
sourceDeviceId = 1,
sealedSender = true,
groupId = groupId?.decodedId,
destinationServiceId = Recipient.resolved(destination).requireServiceId()
)
}
/**
* Create a random text message that will contain a body but may also contain
* - An expire timer value
* - Bold style body ranges
*/
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
return Content.newBuilder()
.setDataMessage(
DataMessage.newBuilder().buildWith {
body = string()
if (random.nextBoolean()) {
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
}
if (random.nextBoolean()) {
addBodyRanges(
SignalServiceProtos.BodyRange.newBuilder().buildWith {
start = 0
length = 1
style = SignalServiceProtos.BodyRange.Style.BOLD
}
)
}
if (groupContextV2 != null) {
groupV2 = groupContextV2
}
}
)
.build()
}
/**
* Create a sync sent text message for the given [DataMessage].
*/
fun syncSentTextMessage(
textMessage: DataMessage,
deliveredTo: List<RecipientId>,
recipientUpdate: Boolean = false
): Content {
return Content
.newBuilder()
.setSyncMessage(
SyncMessage.newBuilder().buildWith {
sent = SyncMessage.Sent.newBuilder().buildWith {
timestamp = textMessage.timestamp
message = textMessage
isRecipientUpdate = recipientUpdate
addAllUnidentifiedStatus(
deliveredTo.map {
SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder().buildWith {
destinationUuid = Recipient.resolved(it).requireServiceId().toString()
unidentified = true
}
}
)
}
}
).build()
}
/**
* Create a random media message that may be:
* - A text body
* - A text body with a quote that references an existing message
* - A text body with a quote that references a non existing message
* - A message with 0-2 attachment pointers and may contain a text body
*/
fun fuzzMediaMessageWithBody(quoteAble: List<TestMessage> = emptyList()): Content {
return Content.newBuilder()
.setDataMessage(
DataMessage.newBuilder().buildWith {
if (random.nextBoolean()) {
body = string()
}
if (random.nextBoolean() && quoteAble.isNotEmpty()) {
body = string()
val quoted = quoteAble.random(random)
quote = DataMessage.Quote.newBuilder().buildWith {
id = quoted.envelope.timestamp
authorUuid = quoted.metadata.sourceServiceId.toString()
text = quoted.content.dataMessage.body
addAllAttachments(quoted.content.dataMessage.attachmentsList)
addAllBodyRanges(quoted.content.dataMessage.bodyRangesList)
type = DataMessage.Quote.Type.NORMAL
}
}
if (random.nextFloat() < 0.1 && quoteAble.isNotEmpty()) {
val quoted = quoteAble.random(random)
quote = DataMessage.Quote.newBuilder().buildWith {
id = random.nextLong(quoted.envelope.timestamp - 1000000, quoted.envelope.timestamp)
authorUuid = quoted.metadata.sourceServiceId.toString()
text = quoted.content.dataMessage.body
}
}
if (random.nextFloat() < 0.25) {
val total = random.nextInt(1, 2)
(0..total).forEach { _ -> addAttachments(attachmentPointer()) }
}
}
)
.build()
}
/**
* Creates a random media message that contains no traditional media content. It may be:
* - A reaction to a prior message
*/
fun fuzzMediaMessageNoContent(previousMessages: List<TestMessage> = emptyList()): Content {
return Content.newBuilder()
.setDataMessage(
DataMessage.newBuilder().buildWith {
if (random.nextFloat() < 0.25) {
val reactTo = previousMessages.random(random)
reaction = DataMessage.Reaction.newBuilder().buildWith {
emoji = emojis.random(random)
remove = false
targetAuthorUuid = reactTo.metadata.sourceServiceId.toString()
targetSentTimestamp = reactTo.envelope.timestamp
}
}
}
).build()
}
/**
* Create a random media message that can never contain a text body. It may be:
* - A sticker
*/
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
return Content.newBuilder()
.setDataMessage(
DataMessage.newBuilder().buildWith {
if (random.nextFloat() < 0.9) {
sticker = DataMessage.Sticker.newBuilder().buildWith {
packId = byteString(length = 24)
packKey = byteString(length = 128)
stickerId = random.nextInt()
data = attachmentPointer()
emoji = emojis.random(random)
}
}
}
).build()
}
/**
* Generate a random [String].
*/
fun string(length: Int = 10, allowNullString: Boolean = false): String {
var string = ""
if (allowNullString && random.nextBoolean()) {
return string
}
for (i in 0 until length) {
string += random.nextInt(65..90).toChar()
}
return string
}
/**
* Generate a random [ByteString].
*/
fun byteString(length: Int = 512): ByteString {
return random.nextBytes(length).toProtoByteString()
}
/**
* Generate a random [AttachmentPointer].
*/
fun attachmentPointer(): AttachmentPointer {
return AttachmentPointer.newBuilder().run {
cdnKey = string()
contentType = mediaTypes.random(random)
key = byteString()
size = random.nextInt(1024 * 1024 * 50)
thumbnail = byteString()
digest = byteString()
fileName = string()
flags = 0
width = random.nextInt(until = 1024)
height = random.nextInt(until = 1024)
caption = string(allowNullString = true)
blurHash = string()
uploadTimestamp = random.nextLong()
cdnNumber = 1
build()
}
}
/**
* Creates a server delivered timestamp that is always later than the envelope and server "received" timestamp.
*/
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
return envelopeTimestamp + 10
}
}

View File

@@ -11,6 +11,7 @@ import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.protocol.util.Medium
import org.signal.libsignal.svr2.PinHash
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -19,7 +20,6 @@ import org.thoughtcrime.securesms.pin.TokenData
import org.thoughtcrime.securesms.test.BuildConfig
import org.whispersystems.signalservice.api.KbsPinData
import org.whispersystems.signalservice.api.KeyBackupService
import org.whispersystems.signalservice.api.kbs.HashedPin
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
import org.whispersystems.signalservice.api.push.ServiceId
@@ -32,6 +32,7 @@ import org.whispersystems.signalservice.internal.push.PreKeyEntity
import org.whispersystems.signalservice.internal.push.PreKeyResponse
import org.whispersystems.signalservice.internal.push.PreKeyResponseItem
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson
import org.whispersystems.signalservice.internal.push.SenderCertificate
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
@@ -56,6 +57,16 @@ object MockProvider {
)
}
val sessionMetadataJson = RegistrationSessionMetadataJson(
id = "asdfasdfasdfasdf",
nextCall = null,
nextSms = null,
nextVerificationAttempt = null,
allowedToRequestCode = true,
requestedInformation = emptyList(),
verified = true
)
fun createVerifyAccountResponse(aci: ServiceId, newPni: ServiceId): VerifyAccountResponse {
return VerifyAccountResponse().apply {
uuid = aci.toString()
@@ -81,12 +92,12 @@ object MockProvider {
}
kbsRepository.stub {
on { getToken(any() as String) } doReturn Single.just(ServiceResponse.forResult(tokenData, 200, ""))
on { getToken(any() as? String) } doReturn Single.just(ServiceResponse.forResult(tokenData, 200, ""))
}
val session: KeyBackupService.RestoreSession = object : KeyBackupService.RestoreSession {
override fun hashSalt(): ByteArray = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8")
override fun restorePin(hashedPin: HashedPin?): KbsPinData = KbsPinData(MasterKey.createNew(SecureRandom()), null)
override fun restorePin(hashedPin: PinHash?): KbsPinData = KbsPinData(MasterKey.createNew(SecureRandom()), null)
}
val kbsService = ApplicationDependencies.getKeyBackupService(BuildConfig.KBS_ENCLAVE)

View File

@@ -7,15 +7,20 @@ import org.thoughtcrime.securesms.util.JsonUtils
import java.util.concurrent.TimeUnit
typealias ResponseFactory = (request: RecordedRequest) -> MockResponse
typealias RequestPredicate = (request: RecordedRequest) -> Boolean
/**
* Represent an HTTP verb for mocking web requests.
*/
sealed class Verb(val verb: String, val path: String, val responseFactory: ResponseFactory)
sealed class Verb(val requestPredicate: RequestPredicate, val responseFactory: ResponseFactory)
class Get(path: String, responseFactory: ResponseFactory) : Verb("GET", path, responseFactory)
class Get(path: String, predicate: RequestPredicate, responseFactory: ResponseFactory) : Verb(defaultRequestPredicate("GET", path, predicate), responseFactory) {
constructor(path: String, responseFactory: ResponseFactory) : this(path, { true }, responseFactory)
}
class Put(path: String, responseFactory: ResponseFactory) : Verb("PUT", path, responseFactory)
class Put(path: String, responseFactory: ResponseFactory) : Verb(defaultRequestPredicate("PUT", path), responseFactory)
class Post(path: String, responseFactory: ResponseFactory) : Verb(defaultRequestPredicate("POST", path), responseFactory)
fun MockResponse.success(response: Any? = null): MockResponse {
return setResponseCode(200).apply {
@@ -46,3 +51,7 @@ inline fun <reified T> RecordedRequest.parsedRequestBody(): T {
val bodyString = String(body.readByteArray())
return JsonUtils.fromJson(bodyString, T::class.java)
}
private fun defaultRequestPredicate(verb: String, path: String, predicate: RequestPredicate = { true }): RequestPredicate = { request ->
request.method == verb && request.path.startsWith("/$path") && predicate(request)
}

View File

@@ -13,7 +13,7 @@ class RxTestSchedulerRule(
val ioTestScheduler: TestScheduler = defaultTestScheduler,
val computationTestScheduler: TestScheduler = defaultTestScheduler,
val singleTestScheduler: TestScheduler = defaultTestScheduler,
val newThreadTestScheduler: TestScheduler = defaultTestScheduler,
val newThreadTestScheduler: TestScheduler = defaultTestScheduler
) : ExternalResource() {
override fun before() {

View File

@@ -11,22 +11,24 @@ import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import org.junit.rules.ExternalResource
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.thoughtcrime.securesms.SignalInstrumentationApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.RegistrationRepository
import org.thoughtcrime.securesms.registration.RegistrationUtil
import org.thoughtcrime.securesms.registration.VerifyResponse
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ACI
@@ -53,18 +55,23 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
private set
lateinit var others: List<RecipientId>
private set
lateinit var othersKeys: List<IdentityKeyPair>
val inMemoryLogger: InMemoryLogger
get() = (application as SignalInstrumentationApplicationContext).inMemoryLogger
override fun before() {
context = InstrumentationRegistry.getInstrumentation().targetContext
self = setupSelf()
others = setupOthers()
val setupOthers = setupOthers()
others = setupOthers.first
othersKeys = setupOthers.second
InstrumentationApplicationDependencyProvider.clearHandlers()
}
private fun setupSelf(): Recipient {
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
val masterSecret = MasterSecretUtil.generateMasterSecret(application, MasterSecretUtil.UNENCRYPTED_PASSPHRASE)
MasterSecretUtil.generateAsymmetricMasterSecret(application, masterSecret)
@@ -74,7 +81,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
val registrationRepository = RegistrationRepository(application)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(Put("/v2/keys") { MockResponse().success() })
val response: ServiceResponse<VerifyAccountResponse> = registrationRepository.registerAccountWithoutRegistrationLock(
val response: ServiceResponse<VerifyResponse> = registrationRepository.registerAccount(
RegistrationData(
code = "123123",
e164 = "+15555550101",
@@ -82,22 +89,27 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
registrationId = registrationRepository.registrationId,
profileKey = registrationRepository.getProfileKey("+15555550101"),
fcmToken = null,
pniRegistrationId = registrationRepository.pniRegistrationId
pniRegistrationId = registrationRepository.pniRegistrationId,
recoveryPassword = "asdfasdfasdfasdf"
),
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false)
VerifyResponse(VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false), null, null),
false
).blockingGet()
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
SignalStore.kbsValues().optOut()
RegistrationUtil.maybeMarkRegistrationComplete(application)
RegistrationUtil.maybeMarkRegistrationComplete()
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
SignalStore.settings().isMessageNotificationsEnabled = false
return Recipient.self()
}
private fun setupOthers(): List<RecipientId> {
private fun setupOthers(): Pair<List<RecipientId>, List<IdentityKeyPair>> {
val others = mutableListOf<RecipientId>()
val othersKeys = mutableListOf<IdentityKeyPair>()
if (othersCount !in 0 until 1000) {
throw IllegalArgumentException("$othersCount must be between 0 and 1000")
@@ -108,14 +120,16 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
others += recipientId
othersKeys += otherIdentity
}
return others
return others to othersKeys
}
inline fun <reified T : Activity> launchActivity(initIntent: Intent.() -> Unit = {}): ActivityScenario<T> {
@@ -130,7 +144,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
}
fun setVerified(recipient: Recipient, status: IdentityDatabase.VerifiedStatus) {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityDatabase.VerifiedStatus.VERIFIED)
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
}
}

View File

@@ -34,7 +34,7 @@ class SignalDatabaseRule(
private fun deleteAllThreads() {
if (deleteAllThreadsOnEachRun) {
SignalDatabase.mms.deleteAllThreads()
SignalDatabase.messages.deleteAllThreads()
}
}
}

View File

@@ -21,7 +21,7 @@ class TestProtos private constructor() {
}
fun metadata(
address: AddressProto = address().build(),
address: AddressProto = address().build()
): MetadataProto.Builder {
return MetadataProto.newBuilder()
.setAddress(address)

View File

@@ -7,6 +7,9 @@ import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.notNullValue
import org.hamcrest.Matchers.nullValue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import kotlin.time.Duration
/**
* Run the given [runnable] on a new thread and wait for it to finish.
@@ -33,7 +36,7 @@ fun <T : Any?> T.assertIsNotNull() {
assertThat(this, notNullValue())
}
infix fun <T : Any> T.assertIs(expected: T) {
infix fun <T : Any?> T.assertIs(expected: T) {
assertThat(this, `is`(expected))
}
@@ -44,3 +47,9 @@ infix fun <T : Any> T.assertIsNot(expected: T) {
infix fun <E, T : Collection<E>> T.assertIsSize(expected: Int) {
assertThat(this, hasSize(expected))
}
fun CountDownLatch.awaitFor(duration: Duration) {
if (!await(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS)) {
throw TimeoutException("Latch await took longer than ${duration.inWholeMilliseconds}ms")
}
}

View File

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

View File

@@ -0,0 +1,78 @@
package org.thoughtcrime.securesms.util
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
/**
* Helper methods for interacting with [MessageTable] in tests.
*/
object MessageTableTestUtils {
fun getMessages(threadId: Long): List<MessageRecord> {
return MessageTable.mmsReaderFor(SignalDatabase.messages.getConversation(threadId)).use {
it.toList()
}
}
fun typeColumnToString(type: Long): String {
return """
isOutgoingMessageType:${MessageTypes.isOutgoingMessageType(type)}
isForcedSms:${type and MessageTypes.MESSAGE_FORCE_SMS_BIT != 0L}
isDraftMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_DRAFT_TYPE}
isFailedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_FAILED_TYPE}
isPendingMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_OUTBOX_TYPE || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENDING_TYPE}
isSentType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_TYPE}
isPendingSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingSecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingInsecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK}
isInboxType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_INBOX_TYPE}
isJoinedType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.JOINED_TYPE}
isUnsupportedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.UNSUPPORTED_MESSAGE_TYPE}
isInvalidMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.INVALID_MESSAGE_TYPE}
isBadDecryptType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BAD_DECRYPT_TYPE}
isSecureType:${type and MessageTypes.SECURE_MESSAGE_BIT != 0L}
isPushType:${type and MessageTypes.PUSH_MESSAGE_BIT != 0L}
isEndSessionType:${type and MessageTypes.END_SESSION_BIT != 0L}
isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L}
isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
isCorruptedKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CORRUPTED_BIT != 0L}
isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L}
isContentBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT != 0L}
isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L}
isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L}
isIncomingAudioCall:${type == MessageTypes.INCOMING_AUDIO_CALL_TYPE}
isIncomingVideoCall:${type == MessageTypes.INCOMING_VIDEO_CALL_TYPE}
isOutgoingAudioCall:${type == MessageTypes.OUTGOING_AUDIO_CALL_TYPE}
isOutgoingVideoCall:${type == MessageTypes.OUTGOING_VIDEO_CALL_TYPE}
isMissedAudioCall:${type == MessageTypes.MISSED_AUDIO_CALL_TYPE}
isMissedVideoCall:${type == MessageTypes.MISSED_VIDEO_CALL_TYPE}
isGroupCall:${type == MessageTypes.GROUP_CALL_TYPE}
isGroupUpdate:${type and MessageTypes.GROUP_UPDATE_BIT != 0L}
isGroupV2:${type and MessageTypes.GROUP_V2_BIT != 0L}
isGroupQuit:${type and MessageTypes.GROUP_LEAVE_BIT != 0L && type and MessageTypes.GROUP_V2_BIT == 0L}
isChatSessionRefresh:${type and MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT != 0L}
isDuplicateMessageType:${type and MessageTypes.ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L}
isDecryptInProgressType:${type and 0x40000000 != 0L}
isNoRemoteSessionType:${type and MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L}
isLegacyType:${type and MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and MessageTypes.ENCRYPTION_REMOTE_BIT != 0L}
isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE}
isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE}
isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE}
isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE}
isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE}
isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE}
isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS}
isSpecialType:${type and MessageTypes.SPECIAL_TYPES_MASK != 0L}
isStoryReaction:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_STORY_REACTION}
isGiftBadge:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_GIFT_BADGE}
isPaymentsNotificaiton:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION}
isRequestToActivatePayments:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST}
isPaymentsActivated:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED}
""".trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "")
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<profileable android:shell="true" />
<activity android:name="org.signal.benchmark.BenchmarkSetupActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
</application>
</manifest>

View File

@@ -0,0 +1,66 @@
package org.signal.benchmark
import android.os.Bundle
import android.widget.TextView
import org.signal.benchmark.setup.TestMessages
import org.signal.benchmark.setup.TestUsers
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.recipients.Recipient
class BenchmarkSetupActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent.extras!!.getString("setup-type")) {
"cold-start" -> setupColdStart()
"conversation-open" -> setupConversationOpen()
}
val textView: TextView = TextView(this).apply {
text = "done"
}
setContentView(textView)
}
private fun setupColdStart() {
TestUsers.setupSelf()
TestUsers.setupTestRecipients(50).forEach {
val recipient: Recipient = Recipient.resolved(it)
TestMessages.insertIncomingTextMessage(other = recipient, body = "Cool text message?!?!")
TestMessages.insertIncomingImageMessage(other = recipient, attachmentCount = 1)
TestMessages.insertIncomingImageMessage(other = recipient, attachmentCount = 2, body = "Album")
TestMessages.insertIncomingImageMessage(other = recipient, body = "Test", attachmentCount = 1, failed = true)
SignalDatabase.messages.setAllMessagesRead()
SignalDatabase.threads.update(SignalDatabase.threads.getOrCreateThreadIdFor(recipient = recipient), true)
}
}
private fun setupConversationOpen() {
TestUsers.setupSelf()
TestUsers.setupTestRecipient().let {
val recipient: Recipient = Recipient.resolved(it)
val messagesToAdd = 1000
val generator: TestMessages.TimestampGenerator = TestMessages.TimestampGenerator(System.currentTimeMillis() - (messagesToAdd * 2000L) - 60_000L)
for (i in 0 until messagesToAdd) {
TestMessages.insertIncomingTextMessage(other = recipient, body = "Test message $i", timestamp = generator.nextTimestamp())
TestMessages.insertOutgoingTextMessage(other = recipient, body = "Test message $i", timestamp = generator.nextTimestamp())
}
val voiceMessageId = TestMessages.insertIncomingVoiceMessage(other = recipient, timestamp = generator.nextTimestamp())
val mmsRecord = SignalDatabase.messages.getMessageRecord(voiceMessageId) as MediaMmsMessageRecord
TestMessages.insertOutgoingImageMessage(other = recipient, body = "test", 2, generator.nextTimestamp())
TestMessages.insertIncomingTextMessage(other = recipient, "reply to the test message", generator.nextTimestamp())
TestMessages.insertIncomingQuoteTextMessage(other = recipient, quote = QuoteModel(mmsRecord.timestamp, recipient.id, "Fake voice message text", false, mmsRecord.slideDeck.asAttachments(), null, QuoteModel.Type.NORMAL, null), body = "Here is a cool quote", timestamp = generator.nextTimestamp())
TestMessages.insertOutgoingTextMessage(other = recipient, body = "longaweorijoaijwerijoiajwer", timestamp = generator.nextTimestamp())
SignalDatabase.threads.update(SignalDatabase.threads.getOrCreateThreadIdFor(recipient = recipient), true)
}
}
}

View File

@@ -0,0 +1,43 @@
package org.signal.benchmark
import android.content.Context
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceIdType
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import java.io.IOException
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),
aci,
pni,
number,
deviceId,
password,
BuildConfig.SIGNAL_AGENT,
FeatureFlags.okHttpAutomaticRetry(),
FeatureFlags.groupLimits().hardLimit
)
}
private class DummyAccountManager(configuration: SignalServiceConfiguration?, aci: ACI?, pni: PNI?, e164: String?, deviceId: Int, password: String?, signalAgent: String?, automaticNetworkRetry: Boolean, maxGroupSize: Int) : SignalServiceAccountManager(configuration, aci, pni, e164, deviceId, password, signalAgent, automaticNetworkRetry, maxGroupSize) {
@Throws(IOException::class)
override fun setGcmId(gcmRegistrationId: Optional<String>) {
}
@Throws(IOException::class)
override fun setPreKeys(serviceIdType: ServiceIdType, identityKey: IdentityKey, signedPreKey: SignedPreKeyRecord, oneTimePreKeys: List<PreKeyRecord>) {
}
}
}

View File

@@ -0,0 +1,189 @@
package org.signal.benchmark.setup
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.TestDbUtils
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
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
import java.util.Collections
import java.util.Optional
object TestMessages {
fun insertOutgoingTextMessage(other: Recipient, body: String, timestamp: Long = System.currentTimeMillis()) {
insertOutgoingMessage(
recipient = other,
message = OutgoingMessage(
recipient = other,
body = body,
timestamp = timestamp,
isSecure = true
),
timestamp = timestamp
)
}
fun insertOutgoingImageMessage(other: Recipient, body: String? = null, attachmentCount: Int, timestamp: Long = System.currentTimeMillis()): Long {
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
imageAttachment()
}
val message = OutgoingMessage(
recipient = other,
body = body,
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
timestamp = timestamp,
isSecure = true
)
return insertOutgoingMediaMessage(recipient = other, message = message, timestamp = timestamp)
}
private fun insertOutgoingMediaMessage(recipient: Recipient, message: OutgoingMessage, timestamp: Long): Long {
val insert = insertOutgoingMessage(recipient, message = message, timestamp = timestamp)
setMessageMediaTransfered(insert)
return insert
}
private fun insertOutgoingMessage(recipient: Recipient, message: OutgoingMessage, timestamp: Long? = null): Long {
val insert = SignalDatabase.messages.insertMessageOutbox(
message,
SignalDatabase.threads.getOrCreateThreadIdFor(recipient),
false,
null
)
if (timestamp != null) {
TestDbUtils.setMessageReceived(insert, timestamp)
}
SignalDatabase.messages.markAsSent(insert, true)
return insert
}
fun insertIncomingTextMessage(other: Recipient, body: String, timestamp: Long? = null) {
val message = IncomingMediaMessage(
from = other.id,
body = body,
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
receivedTimeMillis = timestamp ?: System.currentTimeMillis()
)
SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get().messageId
}
fun insertIncomingQuoteTextMessage(other: Recipient, body: String, quote: QuoteModel, timestamp: Long?) {
val message = IncomingMediaMessage(
from = other.id,
body = body,
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
quote = quote
)
insertIncomingMessage(other, message = message)
}
fun insertIncomingImageMessage(other: Recipient, body: String? = null, attachmentCount: Int, timestamp: Long? = null, failed: Boolean = false): Long {
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
imageAttachment()
}
val message = IncomingMediaMessage(
from = other.id,
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
attachments = PointerAttachment.forPointers(Optional.of(attachments))
)
return insertIncomingMediaMessage(recipient = other, message = message, failed = failed)
}
fun insertIncomingVoiceMessage(other: Recipient, timestamp: Long? = null): Long {
val message = IncomingMediaMessage(
from = other.id,
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
attachments = PointerAttachment.forPointers(Optional.of(Collections.singletonList(voiceAttachment()) as List<SignalServiceAttachment>))
)
return insertIncomingMediaMessage(recipient = other, message = message, failed = false)
}
private fun insertIncomingMediaMessage(recipient: Recipient, message: IncomingMediaMessage, failed: Boolean = false): Long {
val id = insertIncomingMessage(recipient = recipient, message = message)
if (failed) {
setMessageMediaFailed(id)
} else {
setMessageMediaTransfered(id)
}
return id
}
private fun insertIncomingMessage(recipient: Recipient, message: IncomingMediaMessage): Long {
return SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(recipient)).get().messageId
}
private fun setMessageMediaFailed(messageId: Long) {
SignalDatabase.attachments.getAttachmentsForMessage(messageId).forEachIndexed { index, attachment ->
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachment.attachmentId, messageId)
}
}
private fun setMessageMediaTransfered(messageId: Long) {
SignalDatabase.attachments.getAttachmentsForMessage(messageId).forEachIndexed { _, attachment ->
SignalDatabase.attachments.setTransferState(messageId, attachment.attachmentId, AttachmentTable.TRANSFER_PROGRESS_DONE)
}
}
private fun imageAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
Optional.empty(),
Optional.empty(),
1024,
1024,
Optional.empty(),
Optional.of("/not-there.jpg"),
false,
false,
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
)
}
private fun voiceAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
SignalServiceAttachmentRemoteId.from(""),
"audio/aac",
null,
Optional.empty(),
Optional.empty(),
1024,
1024,
Optional.empty(),
Optional.of("/not-there.aac"),
true,
false,
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
)
}
class TimestampGenerator(private var start: Long = System.currentTimeMillis()) {
fun nextTimestamp(): Long {
start += 500L
return start
}
}
}

View File

@@ -0,0 +1,103 @@
package org.signal.benchmark.setup
import android.app.Application
import android.content.SharedPreferences
import android.preference.PreferenceManager
import org.signal.benchmark.DummyAccountManagerFactory
import org.signal.libsignal.protocol.SignalProtocolAddress
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.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.RegistrationRepository
import org.thoughtcrime.securesms.registration.RegistrationUtil
import org.thoughtcrime.securesms.registration.VerifyResponse
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.util.UUID
object TestUsers {
private var generatedOthers: Int = 0
fun setupSelf(): Recipient {
val application: Application = ApplicationDependencies.getApplication()
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
val masterSecret = MasterSecretUtil.generateMasterSecret(application, MasterSecretUtil.UNENCRYPTED_PASSPHRASE)
MasterSecretUtil.generateAsymmetricMasterSecret(application, masterSecret)
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
preferences.edit().putBoolean("passphrase_initialized", true).commit()
val registrationRepository = RegistrationRepository(application)
val registrationData = RegistrationData(
code = "123123",
e164 = "+15555550101",
password = Util.getSecret(18),
registrationId = registrationRepository.registrationId,
profileKey = registrationRepository.getProfileKey("+15555550101"),
fcmToken = "fcm-token",
pniRegistrationId = registrationRepository.pniRegistrationId,
recoveryPassword = "asdfasdfasdfasdf"
)
val verifyResponse = VerifyResponse(VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false), null, null)
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
val response: ServiceResponse<VerifyResponse> = registrationRepository.registerAccount(
registrationData,
verifyResponse,
false
).blockingGet()
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
SignalStore.kbsValues().optOut()
RegistrationUtil.maybeMarkRegistrationComplete()
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
return Recipient.self()
}
fun setupTestRecipient(): RecipientId {
return setupTestRecipients(1).first()
}
fun setupTestRecipients(othersCount: Int): List<RecipientId> {
val others = mutableListOf<RecipientId>()
synchronized(this) {
if (generatedOthers + othersCount !in 0 until 1000) {
throw IllegalArgumentException("$othersCount must be between 0 and 1000")
}
for (i in generatedOthers until generatedOthers + othersCount) {
val aci = ACI.from(UUID.randomUUID())
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
others += recipientId
}
generatedOthers += othersCount
}
return others
}
}

View File

@@ -0,0 +1,14 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import org.signal.core.util.SqlUtil.buildArgs
object TestDbUtils {
fun setMessageReceived(messageId: Long, timestamp: Long) {
val database: SQLiteDatabase = SignalDatabase.messages.databaseHelper.signalWritableDatabase
val contentValues = ContentValues()
contentValues.put(MessageTable.DATE_RECEIVED, timestamp)
val rowsUpdated = database.update(MessageTable.TABLE_NAME, contentValues, DatabaseTable.ID_WHERE, buildArgs(messageId))
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".CanaryApplicationContext"
tools:replace="android:name" />
</manifest>

View File

@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import leakcanary.LeakCanary
import shark.AndroidReferenceMatchers
class CanaryApplicationContext : ApplicationContext() {
override fun onCreate() {
super.onCreate()
StrictMode.setThreadPolicy(
ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
try {
Class.forName("dalvik.system.CloseGuard")
.getMethod("setEnabled", Boolean::class.javaPrimitiveType)
.invoke(null, true)
} catch (e: ReflectiveOperationException) {
throw RuntimeException(e)
}
LeakCanary.config = LeakCanary.config.copy(
referenceMatchers = AndroidReferenceMatchers.appDefaults +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.service.media.MediaBrowserService\$ServiceBinder",
fieldName = "this\$0"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "androidx.media.MediaBrowserServiceCompat\$MediaBrowserServiceImplApi26\$MediaBrowserServiceApi26",
fieldName = "mBase"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.support.v4.media.MediaBrowserCompat",
fieldName = "mImpl"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.support.v4.media.session.MediaControllerCompat",
fieldName = "mToken"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.support.v4.media.session.MediaControllerCompat",
fieldName = "mImpl"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService",
fieldName = "mApplication"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "org.thoughtcrime.securesms.service.GenericForegroundService\$LocalBinder",
fieldName = "this\$0"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "org.thoughtcrime.securesms.contacts.ContactsSyncAdapter",
fieldName = "mContext"
)
)
}
}

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms">
xmlns:tools="http://schemas.android.com/tools">
<application
android:usesCleartextTraffic="true"

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms">
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle,androidx.camera.view" />
@@ -98,9 +97,10 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
tools:replace="android:allowBackup"
android:resizeableActivity="true"
android:allowBackup="false"
android:fullBackupOnly="false"
android:allowBackup="true"
android:backupAgent=".absbackup.SignalBackupAgent"
android:theme="@style/TextSecure.LightTheme"
android:largeHeap="true">
@@ -257,6 +257,307 @@
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltYellow"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_yellow"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_yellow" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_yellow" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltBubbles"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_bubbles"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_bubbles" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_bubbles" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltChat"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_chat"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_chat" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_chat" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltNews"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_news"
android:label="@string/app_icon_label_news"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_news" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_news" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltNotes"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_notes"
android:label="@string/app_icon_label_notes"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_notes" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_notes" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltColor"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_signal_color"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_signal_color" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_signal_color" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltDark"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_signal_dark"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_signal_dark" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_signal_dark" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltDarkVariant"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_signal_dark_variant"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_signal_dark_variant" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_signal_dark_variant" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltWhite"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_signal_white"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_signal_white" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_signal_white" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltWaves"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_waves"
android:label="@string/app_icon_label_waves"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_waves" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_waves" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name=".RoutingActivityAltWeather"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_alt_weather"
android:label="@string/app_icon_label_weather"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher_alt_weather" />
<meta-data
android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher_alt_weather" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias>
<activity android:name=".deeplinks.DeepLinkEntryActivity"
android:exported="true"
android:noHistory="true"
@@ -298,6 +599,24 @@
<data android:scheme="sgnl"
android:host="signal.me" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="signal.link" />
</intent-filter>
</activity>
<activity android:name=".conversation.v2.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".conversation.ConversationActivity"
@@ -332,11 +651,6 @@
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".migrations.ApplicationMigrationActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
@@ -359,6 +673,16 @@
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".calls.links.details.CallLinkDetailsActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".calls.new.NewCallActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PushContactSelectionActivity"
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden"
@@ -373,7 +697,7 @@
android:theme="@style/TextSecure.DarkNoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
<activity android:name=".conversation.mutiselect.forward.MultiselectForwardActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
@@ -441,6 +765,12 @@
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".components.settings.conversation.CallInfoActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".badges.gifts.flow.GiftFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
@@ -469,7 +799,7 @@
<activity android:name=".registration.RegistrationNavigationActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -508,6 +838,13 @@
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity
android:name=".avatar.photo.PhotoEditorActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:label="@string/AndroidManifest__media_preview"
android:theme="@style/TextSecure.DarkNoActionBar"
android:windowSoftInputMode="stateHidden" />
<activity android:name=".mediaoverview.MediaOverviewActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
@@ -602,20 +939,9 @@
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ClearAvatarPromptActivity"
android:theme="@style/Theme.AppCompat.Dialog.Alert"
android:icon="@drawable/clear_profile_avatar"
android:label="@string/AndroidManifest_remove_photo"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contacts.TurnOffContactJoinedNotificationsActivity"
android:theme="@style/Theme.AppCompat.Dialog.Alert" />
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.ContactShareEditActivity"
android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -702,9 +1028,9 @@
<service android:enabled="true" android:name=".exporter.SignalSmsExportService" android:foregroundServiceType="dataSync" />
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService" android:foregroundServiceType="camera|microphone"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$BackgroundService"/>
<service android:name=".service.webrtc.AndroidCallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
@@ -820,6 +1146,8 @@
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
<receiver android:name=".service.ScheduledMessageManager$ScheduledMessagesAlarm" />
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
@@ -888,7 +1216,7 @@
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundUtil$Receiver" android:exported="false" />
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
<intent-filter>

Binary file not shown.

35897
app/src/main/baseline-prof.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,6 @@ import android.content.Context;
import android.net.Uri;
import android.provider.DocumentsContract;
import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
/**
@@ -22,7 +20,6 @@ public class DocumentFileHelper {
*
* @return true if rename successful
*/
@RequiresApi(21)
public static boolean renameTo(Context context, DocumentFile documentFile, String displayName) {
if (documentFile instanceof TreeDocumentFile) {
Log.d(TAG, "Renaming document directly");

View File

@@ -8,6 +8,8 @@ package org.signal.glide.common.executor;
import android.os.HandlerThread;
import android.os.Looper;
import org.signal.core.util.ThreadUtil;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
@@ -39,7 +41,7 @@ public class FrameDecoderExecutor {
public Looper getLooper(int taskId) {
int idx = taskId % sPoolNumber;
if (idx >= mHandlerThreadGroup.size()) {
HandlerThread handlerThread = new HandlerThread("FrameDecoderExecutor-" + idx);
HandlerThread handlerThread = new HandlerThread("FrameDecoderExecutor-" + idx, ThreadUtil.PRIORITY_BACKGROUND_THREAD);
handlerThread.start();
mHandlerThreadGroup.add(handlerThread);

View File

@@ -1,27 +0,0 @@
package org.thoughtcrime.securesms;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.account.AccountAttributes;
public final class AppCapabilities {
private AppCapabilities() {
}
private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
private static final boolean GV1_MIGRATION = true;
private static final boolean ANNOUNCEMENT_GROUPS = true;
private static final boolean SENDER_KEY = true;
private static final boolean CHANGE_NUMBER = true;
private static final boolean STORIES = true;
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, STORIES, FeatureFlags.giftBadgeReceiveSupport(), FeatureFlags.phoneNumberPrivacy());
}
}

View File

@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.account.AccountAttributes
object AppCapabilities {
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
@JvmStatic
fun getCapabilities(storageCapable: Boolean): AccountAttributes.Capabilities {
return AccountAttributes.Capabilities(
storage = storageCapable,
senderKey = true,
announcementGroup = true,
changeNumber = true,
stories = true,
giftBadges = true,
pni = FeatureFlags.phoneNumberPrivacy(),
paymentActivation = true
)
}
}

View File

@@ -23,7 +23,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.multidex.MultiDexApplication;
import com.google.android.gms.security.ProviderInstaller;
@@ -49,6 +48,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
@@ -60,6 +60,7 @@ import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshKbsCredentialsJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
@@ -70,9 +71,9 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -100,6 +101,8 @@ import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWra
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
@@ -120,7 +123,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private static final String TAG = Log.tag(ApplicationContext.class);
private PersistentLogger persistentLogger;
@VisibleForTesting
protected PersistentLogger persistentLogger;
public static ApplicationContext getInstance(Context context) {
return (ApplicationContext)context.getApplicationContext();
@@ -140,8 +144,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
super.onCreate();
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
.addBlocking("sqlcipher-init", () -> {
AppStartup.getInstance().addBlocking("sqlcipher-init", () -> {
SqlCipherLibraryLoader.load();
SignalDatabase.init(this,
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
@@ -151,23 +154,17 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
initializeLogging();
Log.i(TAG, "onCreate()");
})
.addBlocking("security-provider", this::initializeSecurityProvider)
.addBlocking("crash-handling", this::initializeCrashHandling)
.addBlocking("rx-init", this::initializeRx)
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
.addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
.addBlocking("app-migrations", this::initializeApplicationMigrations)
.addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("mark-registration", () -> RegistrationUtil.maybeMarkRegistrationComplete(this))
.addBlocking("mark-registration", () -> RegistrationUtil.maybeMarkRegistrationComplete())
.addBlocking("lifecycle-observer", () -> ApplicationDependencies.getAppForegroundObserver().addListener(this))
.addBlocking("message-retriever", this::initializeMessageRetrieval)
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
.addBlocking("vector-compat", () -> {
if (Build.VERSION.SDK_INT < 21) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
})
.addBlocking("proxy-init", () -> {
if (SignalStore.proxy().isProxyEnabled()) {
Log.w(TAG, "Proxy detected. Enabling Conscrypt.setUseEngineSocketByDefault()");
@@ -176,10 +173,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
})
.addBlocking("blob-provider", this::initializeBlobProvider)
.addBlocking("feature-flags", FeatureFlags::init)
.addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addNonBlocking(() -> GlideApp.get(this))
.addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeScheduledMessageManager)
.addNonBlocking(this::initializeFcmCheck)
.addNonBlocking(PreKeysSyncJob::enqueueIfNeeded)
.addNonBlocking(this::initializePeriodicTasks)
@@ -193,10 +193,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(this::ensureProfileUploaded)
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
.addPostRender(this::initializeTrimThreadsByDateManager)
.addPostRender(RefreshKbsCredentialsJob::enqueueIfNecessary)
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
@@ -208,6 +210,9 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -227,7 +232,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
@@ -270,13 +274,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
private void initializeSecurityProvider() {
try {
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
} catch (ClassNotFoundException e) {
Log.e(TAG, "Failed to find AesGcmCipher class");
throw new ProviderInitializationException();
}
int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1);
Log.i(TAG, "Installed AesGcmProvider: " + aesPosition);
@@ -293,7 +290,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
}
private void initializeLogging() {
@VisibleForTesting
protected void initializeLogging() {
persistentLogger = new PersistentLogger(this);
org.signal.core.util.logging.Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), persistentLogger);
@@ -387,6 +385,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
}
private void initializeScheduledMessageManager() {
ApplicationDependencies.getScheduledMessageManager().scheduleIfNecessary();
}
private void initializeTrimThreadsByDateManager() {
KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration();
if (keepMessagesDuration != KeepMessagesDuration.FOREVER) {
@@ -408,7 +410,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void initializeRingRtc() {
try {
CallManager.initialize(this, new RingRtcLogger());
Map<String, String> fieldTrials = new HashMap<>();
if (FeatureFlags.callingFieldTrialAnyAddressPortsKillSwitch()) {
fieldTrials.put("RingRTC-AnyAddressPortsKillSwitch", "Enabled");
}
CallManager.initialize(this, new RingRtcLogger(), fieldTrials);
} catch (UnsatisfiedLinkError e) {
throw new AssertionError("Unable to load ringrtc library", e);
}

View File

@@ -72,12 +72,10 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
setTheme(R.style.TextSecure_MediaPreview);
setContentView(R.layout.contact_photo_preview_activity);
if (Build.VERSION.SDK_INT >= 21) {
postponeEnterTransition();
TransitionInflater inflater = TransitionInflater.from(this);
getWindow().setSharedElementEnterTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_enter_transition_set));
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
}
postponeEnterTransition();
TransitionInflater inflater = TransitionInflater.from(this);
getWindow().setSharedElementEnterTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_enter_transition_set));
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
Toolbar toolbar = findViewById(R.id.toolbar);
EmojiTextView title = findViewById(R.id.title);
@@ -122,9 +120,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
avatar.setImageDrawable(RoundedBitmapDrawableFactory.create(resources, resource));
if (Build.VERSION.SDK_INT >= 21) {
startPostponedEnterTransition();
}
startPostponedEnterTransition();
}
@Override

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.ConfigurationUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import java.util.Objects;
@@ -42,7 +43,7 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onResume() {
super.onResume();
initializeScreenshotSecurity();
WindowUtil.initializeScreenshotSecurity(this, getWindow());
}
@Override
@@ -64,14 +65,6 @@ public abstract class BaseActivity extends AppCompatActivity {
super.onDestroy();
}
private void initializeScreenshotSecurity() {
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
protected void startActivitySceneTransition(Intent intent, View sharedView, String transitionName) {
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedView, transitionName)
.toBundle();

View File

@@ -10,10 +10,11 @@ import androidx.lifecycle.Observer;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable;
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
@@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -106,11 +108,13 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onInviteToSignalClicked();
void onActivatePaymentsClicked();
void onSendPaymentClicked(@NonNull RecipientId recipientId);
void onScheduledIndicatorClicked(@NonNull View view, @NonNull ConversationMessage conversationMessage);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);
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 onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
}
}

View File

@@ -5,7 +5,6 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.PromptInfo
@@ -26,6 +25,14 @@ class BiometricDeviceAuthentication(
const val TAG: String = "BiometricDeviceAuth"
const val BIOMETRIC_AUTHENTICATORS = BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.BIOMETRIC_WEAK
const val ALLOWED_AUTHENTICATORS = BIOMETRIC_AUTHENTICATORS or BiometricManager.Authenticators.DEVICE_CREDENTIAL
/**
* From the docs on [BiometricManager.canAuthenticate]
*
* > Note that not all combinations of authenticator types are supported prior to Android 11 (API 30). Specifically, DEVICE_CREDENTIAL alone is unsupported
* > prior to API 30, and BIOMETRIC_STRONG | DEVICE_CREDENTIAL is unsupported on API 28-29.
*/
private val DISALLOWED_BIOMETRIC_VERSIONS = setOf(28, 29)
}
fun authenticate(context: Context, force: Boolean, showConfirmDeviceCredentialIntent: () -> Unit): Boolean {
@@ -36,7 +43,7 @@ class BiometricDeviceAuthentication(
return false
}
return if (Build.VERSION.SDK_INT != 29 && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS) {
return if (!DISALLOWED_BIOMETRIC_VERSIONS.contains(Build.VERSION.SDK_INT) && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS) {
if (force) {
Log.i(TAG, "Listening for biometric authentication...")
biometricPrompt.authenticate(biometricPromptInfo)
@@ -44,7 +51,7 @@ class BiometricDeviceAuthentication(
Log.i(TAG, "Skipping show system biometric or device lock dialog unless forced")
}
true
} else if (Build.VERSION.SDK_INT >= 21) {
} else {
if (force) {
Log.i(TAG, "firing intent...")
showConfirmDeviceCredentialIntent()
@@ -52,9 +59,6 @@ class BiometricDeviceAuthentication(
Log.i(TAG, "Skipping firing intent unless forced")
}
true
} else {
Log.w(TAG, "Not compatible...")
false
}
}
@@ -65,7 +69,6 @@ class BiometricDeviceAuthentication(
class BiometricDeviceLockContract : ActivityResultContract<String, Int>() {
@RequiresApi(api = 21)
override fun createIntent(context: Context, input: String): Intent {
val keyguardManager = ServiceUtil.getKeyguardManager(context)
return keyguardManager.createConfirmDeviceCredentialIntent(input, "")

View File

@@ -1,48 +0,0 @@
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Intent;
import android.view.ContextThemeWrapper;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.DynamicTheme;
public final class ClearAvatarPromptActivity extends Activity {
private static final String ARG_TITLE = "arg_title";
public static Intent createForUserProfilePhoto() {
Intent intent = new Intent(ApplicationDependencies.getApplication(), ClearAvatarPromptActivity.class);
intent.putExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo);
return intent;
}
public static Intent createForGroupProfilePhoto() {
Intent intent = new Intent(ApplicationDependencies.getApplication(), ClearAvatarPromptActivity.class);
intent.putExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_group_photo);
return intent;
}
@Override
public void onResume() {
super.onResume();
int message = getIntent().getIntExtra(ARG_TITLE, 0);
new AlertDialog.Builder(new ContextThemeWrapper(this, DynamicTheme.isDarkTheme(this) ? R.style.TextSecure_DarkTheme : R.style.TextSecure_LightTheme))
.setMessage(message)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
Intent result = new Intent();
result.putExtra("delete", true);
setResult(Activity.RESULT_OK, result);
finish();
})
.setOnCancelListener(dialog -> finish())
.show();
}
}

View File

@@ -26,7 +26,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -71,7 +71,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
protected void onCreate(Bundle icicle, boolean ready) {
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
boolean includeSms = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
int displayMode = includeSms ? DisplayMode.FLAG_ALL : DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
int displayMode = includeSms ? ContactSelectionDisplayMode.FLAG_ALL : ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
}
@@ -125,7 +125,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
}
@Override
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
callback.accept(true);
}

View File

@@ -0,0 +1,132 @@
package org.thoughtcrime.securesms
import android.content.Context
import android.view.View
import android.widget.TextView
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
class ContactSelectionListAdapter(
context: Context,
fixedContacts: Set<ContactSearchKey>,
displayOptions: DisplayOptions,
onClickCallbacks: OnContactSelectionClick,
longClickCallbacks: LongClickCallbacks,
storyContextMenuCallbacks: StoryContextMenuCallbacks,
callButtonClickCallbacks: CallButtonClickCallbacks
) : ContactSearchAdapter(context, fixedContacts, displayOptions, onClickCallbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks) {
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(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))
}
class NewGroupModel : MappingModel<NewGroupModel> {
override fun areItemsTheSame(newItem: NewGroupModel): Boolean = true
override fun areContentsTheSame(newItem: NewGroupModel): Boolean = true
}
class InviteToSignalModel : MappingModel<InviteToSignalModel> {
override fun areItemsTheSame(newItem: InviteToSignalModel): Boolean = true
override fun areContentsTheSame(newItem: InviteToSignalModel): Boolean = true
}
class RefreshContactsModel : MappingModel<RefreshContactsModel> {
override fun areItemsTheSame(newItem: RefreshContactsModel): Boolean = true
override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true
}
class MoreHeaderModel : MappingModel<MoreHeaderModel> {
override fun areItemsTheSame(newItem: MoreHeaderModel): Boolean = true
override fun areContentsTheSame(newItem: MoreHeaderModel): Boolean = true
}
private class InviteToSignalViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<InviteToSignalModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: InviteToSignalModel) = Unit
}
private class NewGroupViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<NewGroupModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: NewGroupModel) = Unit
}
private class RefreshContactsViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<RefreshContactsModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: RefreshContactsModel) = Unit
}
private class MoreHeaderViewHolder(itemView: View) : MappingViewHolder<MoreHeaderModel>(itemView) {
private val headerTextView: TextView = itemView.findViewById(R.id.section_header)
override fun bind(model: MoreHeaderModel) {
headerTextView.setText(R.string.contact_selection_activity__more)
}
}
private class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
private val emptyText: TextView = itemView.findViewById(R.id.search_no_results)
override fun bind(model: EmptyModel) {
emptyText.text = context.getString(R.string.SearchFragment_no_results, model.empty.query)
}
}
class ArbitraryRepository : org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository {
enum class ArbitraryRow(val code: String) {
NEW_GROUP("new-group"),
INVITE_TO_SIGNAL("invite-to-signal"),
MORE_HEADING("more-heading"),
REFRESH_CONTACTS("refresh-contacts");
companion object {
fun fromCode(code: String) = values().first { it.code == code }
}
}
override fun getSize(section: ContactSearchConfiguration.Section.Arbitrary, query: String?): Int {
return section.types.size
}
override fun getData(section: ContactSearchConfiguration.Section.Arbitrary, query: String?, startIndex: Int, endIndex: Int, totalSearchSize: Int): List<ContactSearchData.Arbitrary> {
check(section.types.size == 1)
return listOf(ContactSearchData.Arbitrary(section.types.first()))
}
override fun getMappingModel(arbitrary: ContactSearchData.Arbitrary): MappingModel<*> {
return when (ArbitraryRow.fromCode(arbitrary.type)) {
ArbitraryRow.NEW_GROUP -> NewGroupModel()
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
}
}
}
interface OnContactSelectionClick : ClickCallbacks {
fun onNewGroupClicked()
fun onInviteToSignalClicked()
fun onRefreshContactsClicked()
}
}

View File

@@ -21,7 +21,6 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -40,10 +39,7 @@ import androidx.annotation.Px;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -51,46 +47,43 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.transition.AutoTransition;
import androidx.transition.TransitionManager;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.HeaderAction;
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.SelectedContacts;
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder;
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.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareContact;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -104,9 +97,7 @@ import kotlin.Unit;
*
* @author Moxie Marlinspike
*/
public final class ContactSelectionListFragment extends LoggingFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
public final class ContactSelectionListFragment extends LoggingFragment {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
@@ -126,45 +117,47 @@ public final class ContactSelectionListFragment extends LoggingFragment
public static final String RV_PADDING_BOTTOM = "recycler_view_padding_bottom";
public static final String RV_CLIP = "recycler_view_clipping";
private ConstraintLayout constraintLayout;
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;
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private RecyclerView chipRecycler;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
private MappingAdapter contactChipAdapter;
private ContactChipViewModel contactChipViewModel;
private LifecycleDisposable lifecycleDisposable;
private HeaderActionProvider headerActionProvider;
private TextView headerActionView;
private ConstraintLayout constraintLayout;
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;
private RecyclerView chipRecycler;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private MappingAdapter contactChipAdapter;
private ContactChipViewModel contactChipViewModel;
private LifecycleDisposable lifecycleDisposable;
private HeaderActionProvider headerActionProvider;
private TextView headerActionView;
private ContactSearchMediator contactSearchMediator;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback;
@Nullable private OnItemLongClickListener onItemLongClickListener;
private GlideRequests glideRequests;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean hideCount;
private boolean canSelectSelf;
@Nullable private NewConversationCallback newConversationCallback;
@Nullable private NewCallCallback newCallCallback;
@Nullable private ScrollCallback scrollCallback;
@Nullable private OnItemLongClickListener onItemLongClickListener;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean canSelectSelf;
private ListClickListener listClickListener = new ListClickListener();
@Nullable private SwipeRefreshLayout.OnRefreshListener onRefreshListener;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof ListCallback) {
listCallback = (ListCallback) context;
if (context instanceof NewConversationCallback) {
newConversationCallback = (NewConversationCallback) context;
}
if (context instanceof NewCallCallback) {
newCallCallback = (NewCallCallback) context;
}
if (getParentFragment() instanceof ScrollCallback) {
@@ -191,14 +184,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) getParentFragment();
}
if (context instanceof AbstractContactsCursorLoaderFactoryProvider) {
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context;
}
if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) {
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment();
}
if (context instanceof HeaderActionProvider) {
headerActionProvider = (HeaderActionProvider) context;
}
@@ -234,16 +219,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
handleContactPermissionGranted();
} else {
LoaderManager.getInstance(this).initLoader(0, null, this);
contactSearchMediator.refresh();
}
})
.onAnyDenied(() -> {
FragmentActivity activity = requireActivity();
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
if (safeArguments().getBoolean(RECENTS, activity.getIntent().getBooleanExtra(RECENTS, false))) {
LoaderManager.getInstance(this).initLoader(0, null, ContactSelectionListFragment.this);
if (safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false))) {
contactSearchMediator.refresh();
} else {
initializeNoContactsPermission();
}
@@ -255,17 +238,17 @@ public final class ContactSelectionListFragment extends LoggingFragment
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);
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);
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
@@ -305,7 +288,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
swipeRefresh.setNestedScrollingEnabled(isRefreshable);
swipeRefresh.setEnabled(isRefreshable);
hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false));
selectionLimit = arguments.getParcelable(SELECTION_LIMITS);
if (selectionLimit == null) {
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
@@ -353,9 +335,94 @@ public final class ContactSelectionListFragment extends LoggingFragment
headerActionView.setEnabled(false);
}
contactSearchMediator = new ContactSearchMediator(
this,
currentSelection.stream()
.map(r -> new ContactSearchKey.RecipientSearchKey(r, false))
.collect(java.util.stream.Collectors.toSet()),
selectionLimit,
new ContactSearchAdapter.DisplayOptions(
isMulti,
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
newCallCallback != null,
false
),
this::mapStateToConfiguration,
new ContactSearchMediator.SimpleCallbacks() {
@Override
public void onAdapterListCommitted(int size) {
onLoadFinished(size);
}
},
false,
(context, fixedContacts, displayOptions, callbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks) -> new ContactSelectionListAdapter(
context,
fixedContacts,
displayOptions,
new ContactSelectionListAdapter.OnContactSelectionClick() {
@Override
public void onRefreshContactsClicked() {
if (onRefreshListener != null) {
setRefreshing(true);
onRefreshListener.onRefresh();
}
}
@Override
public void onNewGroupClicked() {
newConversationCallback.onNewGroup(false);
}
@Override
public void onInviteToSignalClicked() {
if (newConversationCallback != null) {
newConversationCallback.onInvite();
}
if (newCallCallback != null) {
newCallCallback.onInvite();
}
}
@Override
public void onStoryClicked(@NonNull View view1, @NonNull ContactSearchData.Story story, boolean isSelected) {
throw new UnsupportedOperationException();
}
@Override
public void onKnownRecipientClicked(@NonNull View view1, @NonNull ContactSearchData.KnownRecipient knownRecipient, boolean isSelected) {
listClickListener.onItemClick(knownRecipient.getContactSearchKey());
}
@Override
public void onExpandClicked(@NonNull ContactSearchData.Expand expand) {
callbacks.onExpandClicked(expand);
}
@Override
public void onUnknownRecipientClicked(@NonNull View view, @NonNull ContactSearchData.UnknownRecipient unknownRecipient, boolean isSelected) {
listClickListener.onItemClick(unknownRecipient.getContactSearchKey());
}
},
(anchorView, data) -> listClickListener.onItemLongClick(anchorView, data.getContactSearchKey()),
storyContextMenuCallbacks,
new CallButtonClickCallbacks()
),
new ContactSelectionListAdapter.ArbitraryRepository()
);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
constraintLayout = null;
onRefreshListener = null;
}
private @NonNull Bundle safeArguments() {
return getArguments() != null ? getArguments() : new Bundle();
}
@@ -366,27 +433,30 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
public @NonNull List<SelectedContact> getSelectedContacts() {
if (cursorRecyclerViewAdapter == null) {
if (contactSearchMediator == null) {
return Collections.emptyList();
}
return cursorRecyclerViewAdapter.getSelectedContacts();
return contactSearchMediator.getSelectedContacts()
.stream()
.map(ContactSearchKey::requireSelectedContact)
.collect(java.util.stream.Collectors.toList());
}
public int getSelectedContactsCount() {
if (cursorRecyclerViewAdapter == null) {
if (contactSearchMediator == null) {
return 0;
}
return cursorRecyclerViewAdapter.getSelectedContactsCount();
return contactSearchMediator.getSelectedContacts().size();
}
public int getTotalMemberCount() {
if (cursorRecyclerViewAdapter == null) {
if (contactSearchMediator == null) {
return 0;
}
return cursorRecyclerViewAdapter.getSelectedContactsCount() + cursorRecyclerViewAdapter.getCurrentContactsCount();
return getSelectedContactsCount() + contactSearchMediator.getFixedContactsSize();
}
private Set<RecipientId> getCurrentSelection() {
@@ -396,7 +466,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
return currentSelection == null ? Collections.emptySet()
: Collections.unmodifiableSet(Stream.of(currentSelection).collect(Collectors.toSet()));
: Collections.unmodifiableSet(new HashSet<>(currentSelection));
}
public boolean isMulti() {
@@ -404,34 +474,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
private void initializeCursor() {
glideRequests = GlideApp.with(this);
cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(),
glideRequests,
null,
new ListClickListener(),
isMulti,
currentSelection,
safeArguments().getInt(ContactSelectionArguments.CHECKBOX_RESOURCE, R.drawable.contact_selection_checkbox));
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
if (listCallback != null) {
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
headerAdapter.hide();
concatenateAdapter.addAdapter(headerAdapter);
}
concatenateAdapter.addAdapter(cursorRecyclerViewAdapter);
if (listCallback != null) {
footerAdapter = new FixedViewsAdapter(createInviteActionView(listCallback));
footerAdapter.hide();
concatenateAdapter.addAdapter(footerAdapter);
}
recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
recyclerView.setAdapter(concatenateAdapter);
recyclerView.setAdapter(contactSearchMediator.getAdapter());
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
@@ -452,20 +496,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
return hasQueryFilter() || shouldDisplayRecents();
}
private View createInviteActionView(@NonNull ListCallback listCallback) {
View view = LayoutInflater.from(requireContext())
.inflate(R.layout.contact_selection_invite_action_item, (ViewGroup) requireView(), false);
view.setOnClickListener(v -> listCallback.onInvite());
return view;
}
private View createNewGroupItem(@NonNull ListCallback listCallback) {
View view = LayoutInflater.from(requireContext())
.inflate(R.layout.contact_selection_new_group_item, (ViewGroup) requireView(), false);
view.setOnClickListener(v -> listCallback.onNewGroup(false));
return view;
}
private void initializeNoContactsPermission() {
swipeRefresh.setVisibility(View.GONE);
@@ -490,7 +520,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
public void setQueryFilter(String filter) {
this.cursorFilter = filter;
LoaderManager.getInstance(this).restartLoader(0, null, this);
contactSearchMediator.onFilterChanged(filter);
}
public void resetQueryFilter() {
@@ -507,51 +537,21 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
public void reset() {
cursorRecyclerViewAdapter.clearSelectedContacts();
if (!isDetached() && !isRemoving() && getActivity() != null && !getActivity().isFinishing()) {
LoaderManager.getInstance(this).restartLoader(0, null, this);
}
contactSearchMediator.clearSelection();
fastScroller.setVisibility(View.GONE);
headerActionView.setVisibility(View.GONE);
}
public void setRecyclerViewPaddingBottom(@Px int paddingBottom) {
ViewUtil.setPaddingBottom(recyclerView, paddingBottom);
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
FragmentActivity activity = requireActivity();
int displayMode = safeArguments().getInt(DISPLAY_MODE, activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL));
boolean displayRecents = shouldDisplayRecents();
if (cursorFactoryProvider != null) {
return cursorFactoryProvider.get().create();
} else {
return new ContactsCursorLoader.Factory(activity, displayMode, cursorFilter, displayRecents).create();
}
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, @Nullable Cursor data) {
private void onLoadFinished(int count) {
swipeRefresh.setVisibility(View.VISIBLE);
showContactsLayout.setVisibility(View.GONE);
cursorRecyclerViewAdapter.changeCursor(data);
if (footerAdapter != null) {
footerAdapter.show();
}
if (headerAdapter != null) {
if (TextUtils.isEmpty(cursorFilter)) {
headerAdapter.show();
} else {
headerAdapter.hide();
}
}
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
boolean useFastScroller = data != null && data.getCount() > 20;
boolean useFastScroller = count > 20;
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
if (useFastScroller) {
fastScroller.setVisibility(View.VISIBLE);
@@ -568,13 +568,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
cursorRecyclerViewAdapter.changeCursor(null);
fastScroller.setVisibility(View.GONE);
headerActionView.setVisibility(View.GONE);
}
private boolean shouldDisplayRecents() {
return safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
}
@@ -628,20 +621,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
*
* @param contacts List of the contacts to select. This will not overwrite the current selection, but append to it.
*/
public void markSelected(@NonNull Set<ShareContact> contacts) {
public void markSelected(@NonNull Set<RecipientId> contacts) {
if (contacts.isEmpty()) {
return;
}
Set<SelectedContact> toMarkSelected = contacts.stream()
.map(contact -> {
if (contact.getRecipientId().isPresent()) {
return SelectedContact.forRecipientId(contact.getRecipientId().get());
} else {
return SelectedContact.forPhone(null, contact.getNumber());
}
})
.filter(c -> !cursorRecyclerViewAdapter.isSelectedContact(c))
.filter(r -> !contactSearchMediator.getSelectedContacts()
.contains(new ContactSearchKey.RecipientSearchKey(r, false)))
.map(SelectedContact::forRecipientId)
.collect(java.util.stream.Collectors.toSet());
if (toMarkSelected.isEmpty()) {
@@ -651,22 +639,19 @@ public final class ContactSelectionListFragment extends LoggingFragment
for (final SelectedContact selectedContact : toMarkSelected) {
markContactSelected(selectedContact);
}
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount());
}
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
@Override
public void onItemClick(ContactSelectionListItem contact) {
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orElse(null), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orElse(null), contact.getNumber());
private class ListClickListener {
public void onItemClick(ContactSearchKey contact) {
boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey;
SelectedContact selectedContact = contact.requireSelectedContact();
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
if (!canSelectSelf && !selectedContact.hasUsername() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return;
}
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (!isMulti || !contactSearchMediator.getSelectedContacts().contains(selectedContact.toContactSearchKey())) {
if (selectionHardLimitReached()) {
if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onHardLimitReached(selectionLimit.getHardLimit());
@@ -676,63 +661,62 @@ public final class ContactSelectionListFragment extends LoggingFragment
return;
}
if (contact.isUsernameType()) {
if (contact instanceof ContactSearchKey.UnknownRecipientKey && ((ContactSearchKey.UnknownRecipientKey) contact).getSectionKey() == ContactSearchConfiguration.SectionKey.USERNAME) {
String username = ((ContactSearchKey.UnknownRecipientKey) contact).getQuery();
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
return UsernameUtil.fetchAciForUsername(contact.getNumber());
return UsernameUtil.fetchAciForUsername(username);
}, uuid -> {
loadingDialog.dismiss();
if (uuid.isPresent()) {
Recipient recipient = Recipient.externalUsername(uuid.get(), contact.getNumber());
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
Recipient recipient = Recipient.externalUsername(uuid.get(), username);
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), username);
if (onContactSelectedListener != null) {
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
onContactSelectedListener.onBeforeContactSelected(true, Optional.of(recipient.getId()), null, allowed -> {
if (allowed) {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
});
} else {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, username))
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
}
});
} else {
if (onContactSelectedListener != null) {
onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber(), allowed -> {
onContactSelectedListener.onBeforeContactSelected(
isUnknown,
Optional.ofNullable(selectedContact.getRecipientId()),
selectedContact.getNumber(),
allowed -> {
if (allowed) {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
});
} else {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
}
} else {
markContactUnselected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
onContactSelectedListener.onContactDeselected(Optional.ofNullable(selectedContact.getRecipientId()), selectedContact.getNumber());
}
}
}
@Override
public boolean onItemLongClick(ContactSelectionListItem item) {
public boolean onItemLongClick(View anchorView, ContactSearchKey item) {
if (onItemLongClickListener != null) {
return onItemLongClickListener.onLongClick(item, recyclerView);
return onItemLongClickListener.onLongClick(anchorView, item, recyclerView);
} else {
return false;
}
@@ -752,7 +736,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
private void markContactSelected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
contactSearchMediator.setKeysSelected(Collections.singleton(selectedContact.toContactSearchKey()));
if (isMulti) {
addChipForSelectedContact(selectedContact);
}
@@ -762,8 +746,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
contactSearchMediator.setKeysNotSelected(Collections.singleton(selectedContact.toContactSearchKey()));
contactChipViewModel.remove(selectedContact);
if (onContactSelectedListener != null) {
@@ -828,6 +811,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
public void setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener onRefreshListener) {
this.onRefreshListener = onRefreshListener;
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
}
@@ -836,11 +820,153 @@ public final class ContactSelectionListFragment extends LoggingFragment
chipRecycler.smoothScrollBy(x, 0);
}
private @NonNull ContactSearchConfiguration mapStateToConfiguration(@NonNull ContactSearchState contactSearchState) {
int displayMode = safeArguments().getInt(DISPLAY_MODE, requireActivity().getIntent().getIntExtra(DISPLAY_MODE, ContactSelectionDisplayMode.FLAG_ALL));
boolean includeRecents = safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
boolean includePushContacts = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_PUSH);
boolean includeSmsContacts = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_SMS);
boolean includeActiveGroups = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS);
boolean includeInactiveGroups = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS);
boolean includeSelf = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_SELF);
boolean includeV1Groups = !flagSet(displayMode, ContactSelectionDisplayMode.FLAG_HIDE_GROUPS_V1);
boolean includeNew = !flagSet(displayMode, ContactSelectionDisplayMode.FLAG_HIDE_NEW);
boolean includeRecentsHeader = !flagSet(displayMode, ContactSelectionDisplayMode.FLAG_HIDE_RECENT_HEADER);
boolean includeGroupsAfterContacts = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS);
boolean blocked = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_BLOCK);
boolean includeGroupMembers = flagSet(displayMode, ContactSelectionDisplayMode.FLAG_GROUP_MEMBERS);
boolean hasQuery = !TextUtils.isEmpty(contactSearchState.getQuery());
ContactSearchConfiguration.TransportType transportType = resolveTransportType(includePushContacts, includeSmsContacts);
ContactSearchConfiguration.Section.Recents.Mode mode = resolveRecentsMode(transportType, includeActiveGroups);
ContactSearchConfiguration.NewRowMode newRowMode = resolveNewRowMode(blocked, includeActiveGroups);
return ContactSearchConfiguration.build(builder -> {
builder.setQuery(contactSearchState.getQuery());
if (newConversationCallback != null) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
}
if (transportType != null) {
if (!hasQuery && includeRecents) {
builder.addSection(new ContactSearchConfiguration.Section.Recents(
25,
mode,
includeInactiveGroups,
includeV1Groups,
includeSmsContacts,
includeSelf,
includeRecentsHeader,
null
));
}
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
includeSelf,
transportType,
newCallCallback == null,
null,
!hideLetterHeaders()
));
}
if ((includeGroupsAfterContacts || hasQuery) && includeActiveGroups) {
builder.addSection(new ContactSearchConfiguration.Section.Groups(
includeSmsContacts,
includeV1Groups,
includeInactiveGroups,
false,
ContactSearchSortOrder.NATURAL,
false,
true,
null
));
}
if (hasQuery && includeGroupMembers) {
builder.addSection(new ContactSearchConfiguration.Section.GroupMembers());
}
if (includeNew) {
builder.phone(newRowMode);
builder.username(newRowMode);
}
if (newCallCallback != null || newConversationCallback != null) {
addMoreSection(builder);
builder.withEmptyState(emptyBuilder -> {
emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
addMoreSection(emptyBuilder);
return Unit.INSTANCE;
});
}
return Unit.INSTANCE;
});
}
private void addMoreSection(@NonNull ContactSearchConfiguration.Builder builder) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.MORE_HEADING.getCode());
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode());
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode());
}
private static @Nullable ContactSearchConfiguration.TransportType resolveTransportType(boolean includePushContacts, boolean includeSmsContacts) {
if (includePushContacts && includeSmsContacts) {
return ContactSearchConfiguration.TransportType.ALL;
} else if (includePushContacts) {
return ContactSearchConfiguration.TransportType.PUSH;
} else if (includeSmsContacts) {
return ContactSearchConfiguration.TransportType.SMS;
} else {
return null;
}
}
private static @NonNull ContactSearchConfiguration.Section.Recents.Mode resolveRecentsMode(ContactSearchConfiguration.TransportType transportType, boolean includeGroupContacts) {
if (transportType != null && includeGroupContacts) {
return ContactSearchConfiguration.Section.Recents.Mode.ALL;
} else if (includeGroupContacts) {
return ContactSearchConfiguration.Section.Recents.Mode.GROUPS;
} else {
return ContactSearchConfiguration.Section.Recents.Mode.INDIVIDUALS;
}
}
private @NonNull ContactSearchConfiguration.NewRowMode resolveNewRowMode(boolean isBlocked, boolean isActiveGroups) {
if (isBlocked) {
return ContactSearchConfiguration.NewRowMode.BLOCK;
} else if (newCallCallback != null) {
return ContactSearchConfiguration.NewRowMode.NEW_CALL;
} else if (isActiveGroups) {
return ContactSearchConfiguration.NewRowMode.NEW_CONVERSATION;
} else {
return ContactSearchConfiguration.NewRowMode.ADD_TO_GROUP;
}
}
private static boolean flagSet(int mode, int flag) {
return (mode & flag) > 0;
}
private class CallButtonClickCallbacks implements ContactSearchAdapter.CallButtonClickCallbacks {
@Override
public void onVideoCallButtonClicked(@NonNull Recipient recipient) {
CommunicationActions.startVideoCall(ContactSelectionListFragment.this, recipient);
}
@Override
public void onAudioCallButtonClicked(@NonNull Recipient recipient) {
CommunicationActions.startVoiceCall(ContactSelectionListFragment.this, recipient);
}
}
public interface OnContactSelectedListener {
/**
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
*/
void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, @Nullable String number, @NonNull Consumer<Boolean> callback);
void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, @Nullable String number, @NonNull Consumer<Boolean> callback);
void onContactDeselected(@NonNull Optional<RecipientId> recipientId, @Nullable String number);
@@ -853,12 +979,16 @@ public final class ContactSelectionListFragment extends LoggingFragment
void onHardLimitReached(int limit);
}
public interface ListCallback {
public interface NewConversationCallback {
void onInvite();
void onNewGroup(boolean forceV1);
}
public interface NewCallCallback {
void onInvite();
}
public interface ScrollCallback {
void onBeginScroll();
}
@@ -868,10 +998,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
public interface OnItemLongClickListener {
boolean onLongClick(ContactSelectionListItem contactSelectionListItem, RecyclerView recyclerView);
}
public interface AbstractContactsCursorLoaderFactoryProvider {
@NonNull AbstractContactsCursorLoader.Factory get();
boolean onLongClick(View anchorView, ContactSearchKey contactSearchKey, RecyclerView recyclerView);
}
}

View File

@@ -1,201 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Parcelable;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
import org.thoughtcrime.securesms.service.ApplicationMigrationService.ImportState;
public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
private final ImportServiceConnection serviceConnection = new ImportServiceConnection();
private final ImportStateHandler importStateHandler = new ImportStateHandler();
private final BroadcastReceiver completedReceiver = new NullReceiver();
private LinearLayout promptLayout;
private LinearLayout progressLayout;
private Button skipButton;
private Button importButton;
private ProgressBar progress;
private TextView progressLabel;
private ApplicationMigrationService importService;
private boolean isVisible = false;
@Override
protected void onCreate(Bundle bundle, boolean ready) {
setContentView(R.layout.database_migration_activity);
initializeResources();
initializeServiceBinding();
}
@Override
public void onResume() {
super.onResume();
isVisible = true;
registerForCompletedNotification();
}
@Override
public void onPause() {
super.onPause();
isVisible = false;
unregisterForCompletedNotification();
}
@Override
public void onDestroy() {
super.onDestroy();
shutdownServiceBinding();
}
@Override
public void onBackPressed() {
}
private void initializeServiceBinding() {
Intent intent = new Intent(this, ApplicationMigrationService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
private void initializeResources() {
this.promptLayout = (LinearLayout)findViewById(R.id.prompt_layout);
this.progressLayout = (LinearLayout)findViewById(R.id.progress_layout);
this.skipButton = (Button) findViewById(R.id.skip_button);
this.importButton = (Button) findViewById(R.id.import_button);
this.progress = (ProgressBar) findViewById(R.id.import_progress);
this.progressLabel = (TextView) findViewById(R.id.import_status);
this.progressLayout.setVisibility(View.GONE);
this.promptLayout.setVisibility(View.GONE);
this.importButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(DatabaseMigrationActivity.this, ApplicationMigrationService.class);
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
intent.putExtra("master_secret", (Parcelable)getIntent().getParcelableExtra("master_secret"));
startService(intent);
promptLayout.setVisibility(View.GONE);
progressLayout.setVisibility(View.VISIBLE);
}
});
this.skipButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ApplicationMigrationService.setDatabaseImported(DatabaseMigrationActivity.this);
handleImportComplete();
}
});
}
private void registerForCompletedNotification() {
IntentFilter filter = new IntentFilter();
filter.addAction(ApplicationMigrationService.COMPLETED_ACTION);
filter.setPriority(1000);
registerReceiver(completedReceiver, filter);
}
private void unregisterForCompletedNotification() {
unregisterReceiver(completedReceiver);
}
private void shutdownServiceBinding() {
unbindService(serviceConnection);
}
private void handleStateIdle() {
this.promptLayout.setVisibility(View.VISIBLE);
this.progressLayout.setVisibility(View.GONE);
}
private void handleStateProgress(ProgressDescription update) {
this.promptLayout.setVisibility(View.GONE);
this.progressLayout.setVisibility(View.VISIBLE);
this.progressLabel.setText(update.primaryComplete + "/" + update.primaryTotal);
double max = this.progress.getMax();
double primaryTotal = update.primaryTotal;
double primaryComplete = update.primaryComplete;
double secondaryTotal = update.secondaryTotal;
double secondaryComplete = update.secondaryComplete;
this.progress.setProgress((int)Math.round((primaryComplete / primaryTotal) * max));
this.progress.setSecondaryProgress((int)Math.round((secondaryComplete / secondaryTotal) * max));
}
private void handleImportComplete() {
if (isVisible) {
if (getIntent().hasExtra("next_intent")) {
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
} else {
// TODO [greyson] Navigation
startActivity(MainActivity.clearTop(this));
}
}
finish();
}
private class ImportStateHandler extends Handler {
public ImportStateHandler() {
super(Looper.getMainLooper());
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case ImportState.STATE_IDLE: handleStateIdle(); break;
case ImportState.STATE_MIGRATING_IN_PROGRESS: handleStateProgress((ProgressDescription)message.obj); break;
case ImportState.STATE_MIGRATING_COMPLETE: handleImportComplete(); break;
}
}
}
private class ImportServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
importService = ((ApplicationMigrationService.ApplicationMigrationBinder)service).getService();
importService.setImportStateHandler(importStateHandler);
ImportState state = importService.getState();
importStateHandler.obtainMessage(state.state, state.progress).sendToTarget();
}
@Override
public void onServiceDisconnected(ComponentName name) {
importService.setImportStateHandler(null);
}
}
private static class NullReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
abortBroadcast();
}
}
}

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