Compare commits

..

756 Commits

Author SHA1 Message Date
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
Alex Hart
09afb1be41 Bump version to 6.2.2 2022-11-14 12:48:03 -04:00
Alex Hart
ad2ebfb389 Updated language translations. 2022-11-14 12:45:03 -04:00
Alex Hart
85d7a5c6cc Rotate Credit Cards flag for Beta. 2022-11-14 12:23:10 -04:00
Nicholas Tinsley
4fbbc9d395 Show thumbnail rail when viewing a thread's media. 2022-11-14 11:22:34 -05:00
Alex Hart
e3954ab5e8 Utilize logic from lottie to determine animation scale. 2022-11-14 10:49:55 -04:00
Alex Hart
c1b19390a2 Add 48dp padding to end of gift add message input. 2022-11-14 10:04:02 -04:00
Alex Hart
f7e4e9c855 Fix crash when user does not have a subscription. 2022-11-14 09:59:09 -04:00
Alex Hart
5c6f709faa Do not pre-select my story privacy state. 2022-11-14 09:52:46 -04:00
Alex Hart
47f1d3f594 Add default values to global duration scale resolution. 2022-11-11 13:57:24 -04:00
Greyson Parrelli
2b10f93718 Update hint text for group story replies. 2022-11-11 12:29:36 -05:00
Greyson Parrelli
ccee7577f7 Do not double-insert change number events. 2022-11-11 12:14:13 -05:00
Greyson Parrelli
4e871e2dd8 Bump version to 6.2.1 2022-11-11 10:42:24 -05:00
Greyson Parrelli
455da6649b Updated language translations. 2022-11-11 10:41:52 -05:00
Greyson Parrelli
dc4acd83e8 Fix typo in string. 2022-11-11 10:41:14 -05:00
Alex Hart
0e3a9a3130 Finalize credit card copy. 2022-11-11 10:35:55 -05:00
Greyson Parrelli
ed2edc1ebb Do no double-process the CDSI response. 2022-11-11 10:34:40 -05:00
Greyson Parrelli
6f4de36c6f Bump version to 6.2.0 2022-11-10 17:01:32 -05:00
Greyson Parrelli
e10696b44e Updated language translations. 2022-11-10 17:00:51 -05:00
Alex Hart
6ed1c21a66 Add in new donations strings for credit card support. 2022-11-10 16:58:25 -05:00
Alex Hart
263f7ebac5 Trim zeroes in subscription row items. 2022-11-10 16:58:25 -05:00
Alex Hart
c3063b721d Allow restricted users to update or cancel their subscription. 2022-11-10 16:58:25 -05:00
Cody Henthorne
1dc29fda12 Add in-chat payment messages. 2022-11-10 16:58:25 -05:00
Cody Henthorne
28193c2f61 Allow all notifications to be cancelled when bubbles are disabled. 2022-11-10 16:58:25 -05:00
Alex Hart
9d71c4df81 Refactor a large portion of the payments code to prep it for PayPal support. 2022-11-10 16:58:25 -05:00
Greyson Parrelli
c563ef27da Add UX for handling CDS rate limits. 2022-11-10 16:58:25 -05:00
Cody Henthorne
8eb3a1906e Fully delete remotely deleted stories after sending or on receive. 2022-11-10 10:47:14 -05:00
Cody Henthorne
0309f9ea89 Change destination for remote donation megaphones. 2022-11-10 10:46:57 -05:00
Nicholas
d678341399 Wrap DefaultAudioSink to tolerate init errors. 2022-11-10 10:15:10 -05:00
Nicholas
99f8ba5e0c Enable icons in overflow menu. 2022-11-10 09:37:32 -05:00
Alex Hart
c69b91c4db Add blocked regions from global config for donations payments. 2022-11-10 10:17:13 -04:00
Alex Hart
8f56c1baa5 Add new CC icon for dark mode. 2022-11-09 19:26:48 -05:00
Alex Hart
a0d4026e40 Enable screenshot security for CC fragment. 2022-11-09 19:26:48 -05:00
Cody Henthorne
975b242a08 Fix notification not showing after thread with custom notification is deleted. 2022-11-09 19:26:48 -05:00
Nicholas
f1fafa6516 Gain temporary audio focus during voice memo recording. 2022-11-09 19:26:48 -05:00
Nicholas
fca412b47d Pause videos/GIFs when sharing or forwarding. 2022-11-09 19:26:48 -05:00
Cody Henthorne
18c32a7a80 Only allow active groups to start ringing. 2022-11-09 19:26:48 -05:00
Nicholas
f96c31b38f Always allow remote delete in note to self. 2022-11-09 19:26:48 -05:00
Alex Hart
65a4ef2f70 Update donation strings. 2022-11-09 19:26:48 -05:00
Alex Hart
2b685ea89f Inline the stories flag. 2022-11-09 19:26:48 -05:00
Cody Henthorne
b55954380d Bump various Google Play Services dependencies. 2022-11-09 19:26:48 -05:00
Greyson Parrelli
739a8e9451 Add PNP change number insert events and tests. 2022-11-09 19:26:48 -05:00
Alex Hart
433b5ebc13 Flip case for donation method availability. 2022-11-09 19:26:48 -05:00
Alex Hart
018bb49a03 Cancel boost animations in onStop. 2022-11-09 19:26:48 -05:00
Alex Hart
fc145d7367 Increase height of boost items to align with subscriptions. 2022-11-09 19:26:48 -05:00
Alex Hart
c5f05f322f Rotate Credit Card payments flag. 2022-11-09 19:26:48 -05:00
Greyson Parrelli
b419eb4cd5 Inline internal-only strings. 2022-11-09 19:26:48 -05:00
Alex Hart
9bdf65c4e4 Add androidTest for inserting a direct reply via MessageContentProcessor. 2022-11-09 19:26:48 -05:00
Alex Hart
dbbae7f13f Fix a few flaky instrumentation tests to ensure suite passes. 2022-11-09 19:26:48 -05:00
Alex Hart
513228b366 Update text spacing on donations page. 2022-11-09 19:26:48 -05:00
Greyson Parrelli
a2415261bd Pair usernames flag with the PNP flag. 2022-11-09 19:26:48 -05:00
Alex Hart
8f06381239 Centralize where we make decisions about donations availability. 2022-11-09 19:26:48 -05:00
Alex Hart
f6f1fdb87d Mark unexpected cancellation when silenced so we do not keep hammering the logs. 2022-11-09 19:26:48 -05:00
Alex Hart
b8e16353ab Donations credit card formatting. 2022-11-09 19:26:48 -05:00
Alex Hart
16cbc971a5 Add small amount of unit testing for MessageContentProcessor. 2022-11-09 19:26:47 -05:00
Alex Hart
d1df069669 Add support for Credit Card 3DS during subscriptions. 2022-11-09 19:26:47 -05:00
Greyson Parrelli
844480786e Bump version to 6.1.3 2022-11-09 18:15:01 -05:00
Greyson Parrelli
77aa0424fd Updated language translations. 2022-11-09 18:15:01 -05:00
Alex Hart
4d94d9d968 Utilize areAnimatorsEnabled on API levels that support it. 2022-11-09 18:15:01 -05:00
Greyson Parrelli
89fca76327 Bump version to 6.1.2 2022-11-08 17:38:55 -05:00
Greyson Parrelli
14b9518a48 Updated language translations. 2022-11-08 17:38:55 -05:00
Greyson Parrelli
512ba2b0a8 Show bottom sheet when you tap an avatar in the story viewer. 2022-11-08 17:38:55 -05:00
Cody Henthorne
9851bc300e Fix mentions with share to single group story flow. 2022-11-08 17:38:55 -05:00
Nicholas
a81a4cdb53 Adjust stories view receipts button destination. 2022-11-08 17:38:55 -05:00
Alex Hart
b1d1aee373 Fix infinite animation loop when system animations are disabled. 2022-11-08 17:38:55 -05:00
Cody Henthorne
2cfa31a9b0 Fix crash when typing @ in story add message. 2022-11-07 22:39:54 -05:00
Nicholas
67b6cd164e Manually dismiss keyboard on forwarding messages. 2022-11-07 12:23:26 -05:00
Greyson Parrelli
f241a51fe1 Hopeful fix for crash on API 19. 2022-11-07 11:22:31 -05:00
Nicholas
74c542099a Autoplay all videos. 2022-11-07 09:15:39 -05:00
Nicholas
5d76f13c51 Increase touch target height of seekbar. 2022-11-07 09:15:24 -05:00
Nicholas Tinsley
c6d38600ec Restore wrap_content for album rail. 2022-11-04 17:39:33 -04:00
Cody Henthorne
fc3db538bc Bump version to 6.1.1 2022-11-04 16:38:01 -04:00
Cody Henthorne
acbccc32a6 Updated language translations. 2022-11-04 16:12:29 -04:00
Cody Henthorne
97a502c8c7 Restrict max threads used for large group profile fetching. 2022-11-04 16:08:31 -04:00
Greyson Parrelli
bdba048bc4 Remove possible transaction from identity cache read. 2022-11-04 16:08:30 -04:00
Greyson Parrelli
f7adf2ee5a Fix a typo in a group string. 2022-11-04 16:08:30 -04:00
Alex Hart
dcc9b8ca66 Fix issue with window insets in API30.
Fixes #12525
2022-11-04 16:08:30 -04:00
Nicholas
7ad6d95b27 Fade in media detail view. 2022-11-04 16:08:30 -04:00
Greyson Parrelli
2856697109 Fix string apostrophe. 2022-11-04 16:08:30 -04:00
Nicholas
af89d85696 Fade out video player controls on playback.
2 second delay, cancelable if the video is paused or finished.
2022-11-04 16:08:30 -04:00
Alex Voloshyn
c218e22566 Update MobileCoin enclave measurements for v3.0.0 2022-11-04 16:08:30 -04:00
Varsha
b38ac44d0f Prompt update on MobileCoin enclave failure. 2022-11-03 12:04:51 -04:00
2200 changed files with 99551 additions and 128330 deletions

View File

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

View File

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

View File

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

View File

@@ -43,10 +43,7 @@
</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>
<value />
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />

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

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'
}
apply from: 'translations.gradle'
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 = 1157
def canonicalVersionName = "6.1.0"
ktlint {
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
version = "0.47.1"
}
def canonicalVersionCode = 1220
def canonicalVersionName = "6.13.0"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
@@ -66,6 +62,9 @@ def selectableVariants = [
'nightlyProdSpinner',
'nightlyProdPerf',
'nightlyProdRelease',
'nightlyStagingRelease',
'nightlyPnpPerf',
'nightlyPnpRelease',
'playProdDebug',
'playProdSpinner',
'playProdPerf',
@@ -75,14 +74,18 @@ def selectableVariants = [
'playStagingSpinner',
'playStagingPerf',
'playStagingInstrumentation',
'playPnpDebug',
'playPnpSpinner',
'playStagingRelease',
'websiteProdSpinner',
'websiteProdRelease',
]
android {
buildToolsVersion BUILD_TOOL_VERSION
compileSdkVersion COMPILE_SDK
namespace 'org.thoughtcrime.securesms'
buildToolsVersion BUILD_TOOLS_VERSION
compileSdkVersion COMPILE_SDK_VERSION
flavorDimensions 'distribution', 'environment'
useLibrary 'org.apache.http.legacy'
@@ -93,10 +96,6 @@ android {
freeCompilerArgs = ["-Xallow-result-return-type"]
}
dexOptions {
javaMaxHeapSize "4g"
}
signingConfigs {
if (keystores.debug != null) {
debug {
@@ -114,14 +113,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 {
@@ -138,34 +142,33 @@ android {
sourceCompatibility JAVA_VERSION
targetCompatibility JAVA_VERSION
}
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/NOTICE', '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 MIN_SDK_VERSION
targetSdkVersion TARGET_SDK_VERSION
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "Signal");
project.ext.set("archivesBaseName", "Signal")
manifestPlaceholders = [mapsKey:"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"]
@@ -180,6 +183,7 @@ android {
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.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\""
@@ -193,8 +197,7 @@ android {
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 "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
"\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
@@ -256,6 +259,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 +289,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
@@ -348,7 +354,6 @@ android {
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\")"
@@ -364,6 +369,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 ->
@@ -495,7 +516,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) {
@@ -566,6 +586,9 @@ dependencies {
implementation libs.rxdogtag
androidTestUtil testLibs.androidx.test.orchestrator
implementation project(':core-ui')
ktlintRuleset libs.ktlint.twitter.compose
}
def getLastCommitTimestamp() {
@@ -574,7 +597,7 @@ def getLastCommitTimestamp() {
}
new ByteArrayOutputStream().withStream { os ->
def result = exec {
exec {
executable = 'git'
args = ['log', '-1', '--pretty=format:%ct']
standardOutput = os
@@ -586,20 +609,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 +656,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

@@ -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,7 +19,7 @@ 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.Put
@@ -111,11 +113,11 @@ class ChangeNumberViewModelTest {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { MockResponse().failure(500) },
Put("/v1/accounts/number") { MockResponse().failure(500) }
)
// WHEN
val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs true
@@ -146,7 +148,7 @@ class ChangeNumberViewModelTest {
)
// WHEN
val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
@@ -166,6 +168,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()
@@ -191,7 +195,7 @@ class ChangeNumberViewModelTest {
)
// WHEN
val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false

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

@@ -2,11 +2,12 @@ package org.thoughtcrime.securesms.conversation
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Ignore
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
@@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.testing.SignalActivityRule
/**
* Android test to help show SNC dialog quickly with custom data to make sure it displays properly.
*/
@Ignore("For testing/previewing manually, no assertions")
@RunWith(AndroidJUnit4::class)
class SafetyNumberChangeDialogPreviewer {
@@ -30,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()) }
@@ -50,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)
@@ -62,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

@@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.util.MediaUtil
import java.util.Optional
@RunWith(AndroidJUnit4::class)
class AttachmentDatabaseTest {
class AttachmentTableTest {
@Before
fun setUp() {
@@ -39,7 +39,7 @@ class AttachmentDatabaseTest {
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,8 +55,8 @@ 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)
}
@@ -81,13 +81,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 +96,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

@@ -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,285 @@
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(groupRecord.members.toSet(), setOf(harness.self.id, harness.others[1]))
}
@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)
}
}

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
@@ -291,13 +290,12 @@ class MmsDatabaseTest_stories {
}
@Test
fun givenAGroupStoryWithAReactionFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectFalse() {
fun givenAGroupStoryWithAReactionFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectTrue() {
// GIVEN
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
storyType = StoryType.STORY_WITH_REPLIES
)
MmsHelper.insert(
@@ -312,7 +310,7 @@ class MmsDatabaseTest_stories {
val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
// THEN
assertFalse(result)
assertTrue(result)
}
@Test
@@ -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,661 +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.core.util.ThreadUtil
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.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
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 changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
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)
changeNumberListener.waitForJobManager()
assertFalse(changeNumberListener.numberChangeWasEnqueued)
}
/** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge], but with a number change. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
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)
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
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())
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/**
* 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 changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
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())
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** 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
)
private class ChangeNumberListener {
var numberChangeWasEnqueued = false
private set
fun waitForJobManager() {
ApplicationDependencies.getJobManager().flush()
ThreadUtil.sleep(500)
}
fun enqueue() {
ApplicationDependencies.getJobManager().addListener(
{ job -> job.factoryKey == RecipientChangedNumberJob.KEY },
{ _, _ -> numberChangeWasEnqueued = true }
)
}
}
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,457 +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.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_processPnpTuple {
private lateinit var recipientDatabase: RecipientDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
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.
// TODO Verify change number
test {
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@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.
// TODO Verify change number
test {
given(E164_B, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyAciMatches_changeNumber() {
// TODO Verify change number
test {
given(E164_B, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@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() {
// TODO Verify change number
test {
given(E164_A, PNI_A, null)
given(E164_B, PNI_B, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun merge_e164AndPni_e164Aci_changeNumber() {
// TODO Verify change number
test {
given(E164_A, PNI_A, null)
given(E164_B, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
}
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
fun given(e164: String?, pni: PNI?, aci: ACI?) {
generatedIds += insert(e164, pni, aci)
}
fun process(e164: String?, pni: PNI?, aci: ACI?) {
SignalDatabase.rawDatabase.beginTransaction()
try {
generatedIds += recipientDatabase.processPnpTuple(e164, pni, aci, pniVerified = false).finalId
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))
}
}
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

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

@@ -13,6 +13,7 @@ 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.Verb
import org.thoughtcrime.securesms.testing.runSync
import org.thoughtcrime.securesms.util.Base64
@@ -21,7 +22,6 @@ 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,6 +41,7 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
private val uncensoredConfiguration: SignalServiceConfiguration
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
private val keyBackupService: KeyBackupService
private val recipientCache: LiveRecipientCache
init {
runSync {
@@ -64,7 +65,6 @@ 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)),
@@ -81,6 +81,8 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
}
keyBackupService = mock()
recipientCache = LiveRecipientCache(application) { r -> r.run() }
}
override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess {
@@ -91,6 +93,10 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
return keyBackupService
}
override fun provideRecipientCache(): LiveRecipientCache {
return recipientCache
}
companion object {
lateinit var webServer: MockWebServer
private set

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

@@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.messages
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import org.junit.Rule
import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata
import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.TestProtos
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
abstract class MessageContentProcessorTest {
@get:Rule
val harness = SignalActivityRule()
protected fun MessageContentProcessor.doProcess(
messageState: MessageState = MessageState.DECRYPTED_OK,
content: SignalServiceContent,
exceptionMetadata: ExceptionMetadata = ExceptionMetadata("sender", 1),
timestamp: Long = 100L,
smsMessageId: Long = -1L
) {
process(messageState, content, exceptionMetadata, timestamp, smsMessageId)
}
protected fun createNormalContentTestSubject(): MessageContentProcessor {
val context = ApplicationProvider.getApplicationContext<Application>()
return MessageContentProcessor.create(context)
}
/**
* Creates a valid ServiceContentProto with a data message which can be built via
* `injectDataMessage`. This function is intended to be built on-top of for more
* specific scenario in subclasses.
*
* Example can be seen in __handleStoryMessageTest
*/
protected fun createServiceContentWithDataMessage(
messageSender: Recipient = Recipient.resolved(harness.others.first()),
injectDataMessage: SignalServiceProtos.DataMessage.Builder.() -> Unit
): SignalServiceContentProto {
return TestProtos.build {
serviceContent(
localAddress = address(uuid = harness.self.requireServiceId().uuid()).build(),
metadata = metadata(
address = address(uuid = messageSender.requireServiceId().uuid()).build()
).build()
).apply {
content = content().apply {
dataMessage = dataMessage().apply {
injectDataMessage()
}.build()
}.build()
}.build()
}
}
}

View File

@@ -0,0 +1,181 @@
package org.thoughtcrime.securesms.messages
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.signal.core.util.requireLong
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.MessageTable
import org.thoughtcrime.securesms.database.MmsHelper
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.TestProtos
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
import kotlin.random.Random
@Suppress("ClassName")
class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorTest() {
@Before
fun setUp() {
SignalDatabase.messages.deleteAllThreads()
}
@After
fun tearDown() {
SignalDatabase.messages.deleteAllThreads()
}
@Test
fun givenContentWithADirectStoryReplyWhenIProcessThenIInsertAReplyInTheCorrectThread() {
val sender = Recipient.resolved(harness.others.first())
val senderThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(sender)
val myStory = Recipient.resolved(SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!!)
val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
val expectedSentTime = 200L
val storyMessageId = MmsHelper.insert(
sentTimeMillis = expectedSentTime,
recipient = myStory,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = myStoryThread
)
SignalDatabase.storySends.insert(
messageId = storyMessageId,
recipientIds = listOf(sender.id),
sentTimestamp = expectedSentTime,
allowsReplies = true,
distributionId = DistributionId.MY_STORY
)
val expectedBody = "Hello!"
val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext(
messageSender = sender,
storyAuthor = harness.self,
storySentTimestamp = expectedSentTime
) {
body = expectedBody
}
runTestWithContent(contentProto = storyContent)
val replyId = SignalDatabase.messages.getConversation(senderThreadId, 0, 1).use {
it.moveToFirst()
it.requireLong(MessageTable.ID)
}
val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord
assertEquals(ParentStoryId.DirectReply(storyMessageId).serialize(), replyRecord.parentStoryId!!.serialize())
assertEquals(expectedBody, replyRecord.body)
SignalDatabase.messages.deleteAllThreads()
}
@Test
fun givenContentWithAGroupStoryReplyWhenIProcessThenIInsertAReplyToTheCorrectStory() {
val sender = Recipient.resolved(harness.others[0])
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.newBuilder()
.addAllMembers(
listOf(
DecryptedMember.newBuilder()
.setUuid(harness.self.requireServiceId().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build(),
DecryptedMember.newBuilder()
.setUuid(sender.requireServiceId().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build()
)
)
.setRevision(0)
.build()
val group = SignalDatabase.groups.create(
groupMasterKey,
decryptedGroupState
)
val groupRecipient = Recipient.externalGroupExact(group)
val threadForGroup = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
val insertResult = MmsHelper.insert(
message = IncomingMediaMessage(
from = sender.id,
sentTimeMillis = 100L,
serverTimeMillis = 101L,
receivedTimeMillis = 102L,
storyType = StoryType.STORY_WITH_REPLIES
),
threadId = threadForGroup
)
val expectedBody = "Hello, World!"
val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext(
messageSender = sender,
storyAuthor = sender,
storySentTimestamp = 100L
) {
groupV2 = TestProtos.build { groupContextV2(masterKeyBytes = groupMasterKey.serialize()).build() }
body = expectedBody
}
runTestWithContent(storyContent)
val replyId = SignalDatabase.messages.getStoryReplies(insertResult.get().messageId).use { cursor ->
assertEquals(1, cursor.count)
cursor.moveToFirst()
cursor.requireLong(MessageTable.ID)
}
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.messages.deleteGroupStoryReplies(insertResult.get().messageId)
SignalDatabase.messages.deleteAllThreads()
}
/**
* Creates a ServiceContent proto with a StoryContext, and then
* uses `injectDataMessage` to fill in the data message object.
*/
private fun createServiceContentWithStoryContext(
messageSender: Recipient,
storyAuthor: Recipient,
storySentTimestamp: Long,
injectDataMessage: DataMessage.Builder.() -> Unit
): SignalServiceContentProto {
return createServiceContentWithDataMessage(messageSender) {
storyContext = TestProtos.build {
storyContext(
sentTimestamp = storySentTimestamp,
authorUuid = storyAuthor.requireServiceId().toString()
).build()
}
injectDataMessage()
}
}
private fun runTestWithContent(contentProto: SignalServiceContentProto) {
val content = SignalServiceContent.createFromProto(contentProto)
val testSubject = createNormalContentTestSubject()
testSubject.doProcess(content = content!!)
}
}

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.messages
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.thoughtcrime.securesms.database.SignalDatabase
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
@Suppress("ClassName")
class MessageContentProcessor__handleTextMessageTest : MessageContentProcessorTest() {
@Test
fun givenContentWithATextMessageWhenIProcessThenIInsertTheTextMessage() {
val testSubject: MessageContentProcessor = createNormalContentTestSubject()
val expectedBody = "Hello, World!"
val contentProto: SignalServiceContentProto = createServiceContentWithDataMessage {
body = expectedBody
}
val content = SignalServiceContent.createFromProto(contentProto)
// WHEN
testSubject.doProcess(content = content!!)
// THEN
val record = SignalDatabase.messages.getMessageRecord(1)
val threadSize = SignalDatabase.messages.getMessageCountForThread(record.threadId)
assertEquals(1, threadSize)
assertTrue(record.isSecure)
assertEquals(expectedBody, record.body)
}
}

View File

@@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.reactivex.rxjava3.schedulers.TestScheduler
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -72,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()
@@ -89,6 +91,7 @@ class UsernameEditFragmentTest {
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Ignore("Flakey espresso test.")
@Test
fun testNicknameUpdateHappyPath() {
val nickname = "Spiderman"
@@ -97,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

@@ -81,7 +81,7 @@ 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 {

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

@@ -15,7 +15,7 @@ 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.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
@@ -27,6 +27,7 @@ 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
@@ -74,7 +75,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",
@@ -84,7 +85,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
fcmToken = null,
pniRegistrationId = registrationRepository.pniRegistrationId
),
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false)
VerifyResponse(VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false), null, null)
).blockingGet()
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
@@ -108,7 +109,7 @@ 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)
@@ -130,7 +131,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

@@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.testing
import com.google.protobuf.ByteString
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
import org.whispersystems.signalservice.internal.serialize.protos.AddressProto
import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
import java.util.UUID
import kotlin.random.Random
class TestProtos private constructor() {
fun address(
uuid: UUID = UUID.randomUUID()
): AddressProto.Builder {
return AddressProto.newBuilder()
.setUuid(ServiceId.from(uuid).toByteString())
}
fun metadata(
address: AddressProto = address().build()
): MetadataProto.Builder {
return MetadataProto.newBuilder()
.setAddress(address)
}
fun groupContextV2(
revision: Int = 0,
masterKeyBytes: ByteArray = Random.Default.nextBytes(GroupMasterKey.SIZE)
): GroupContextV2.Builder {
return GroupContextV2.newBuilder()
.setRevision(revision)
.setMasterKey(ByteString.copyFrom(masterKeyBytes))
}
fun storyContext(
sentTimestamp: Long = Random.nextLong(),
authorUuid: String = UUID.randomUUID().toString()
): DataMessage.StoryContext.Builder {
return DataMessage.StoryContext.newBuilder()
.setAuthorUuid(authorUuid)
.setSentTimestamp(sentTimestamp)
}
fun dataMessage(): DataMessage.Builder {
return DataMessage.newBuilder()
}
fun content(): SignalServiceProtos.Content.Builder {
return SignalServiceProtos.Content.newBuilder()
}
fun serviceContent(
localAddress: AddressProto = address().build(),
metadata: MetadataProto = metadata().build()
): SignalServiceContentProto.Builder {
return SignalServiceContentProto.newBuilder()
.setLocalAddress(localAddress)
.setMetadata(metadata)
}
companion object {
fun <T> build(buildFn: TestProtos.() -> T): T {
return TestProtos().buildFn()
}
}
}

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

@@ -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">
@@ -332,11 +332,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"
@@ -469,7 +464,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"/>
@@ -696,9 +691,12 @@
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<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:name=".service.webrtc.AndroidCallConnectionService"
@@ -816,6 +814,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" />
@@ -884,7 +884,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>

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

@@ -1,26 +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;
/**
* @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.isFeatureFlagEnabled(), FeatureFlags.giftBadgeReceiveSupport(), FeatureFlags.phoneNumberPrivacy());
}
}

View File

@@ -0,0 +1,27 @@
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(
isUuid = false,
isGv2 = true,
isStorage = storageCapable,
isGv1Migration = true,
isSenderKey = true,
isAnnouncementGroup = true,
isChangeNumber = true,
isStories = true,
isGiftBadges = true,
isPnp = FeatureFlags.phoneNumberPrivacy(),
paymentActivation = true
)
}
}

View File

@@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
@@ -23,7 +24,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;
@@ -35,6 +35,8 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.signal.donations.GooglePayApi;
import org.signal.donations.StripeApi;
import org.signal.glide.SignalGlideCodecs;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.signal.ringrtc.CallManager;
@@ -60,6 +62,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;
@@ -72,7 +75,6 @@ import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
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;
@@ -89,6 +91,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Environment;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
@@ -100,6 +103,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;
@@ -155,19 +160,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.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("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 +174,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(this::checkIsGooglePayReady)
.addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeScheduledMessageManager)
.addNonBlocking(this::initializeFcmCheck)
.addNonBlocking(PreKeysSyncJob::enqueueIfNeeded)
.addNonBlocking(this::initializePeriodicTasks)
@@ -197,6 +198,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.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,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -387,6 +390,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 +415,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);
}
@@ -460,6 +471,18 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
AvatarPickerStorage.cleanOrphans(this);
}
@SuppressLint("CheckResult")
private void checkIsGooglePayReady() {
GooglePayApi.queryIsReadyToPay(
this,
new StripeApi.Gateway(Environment.Donations.getStripeConfiguration()),
Environment.Donations.getGooglePayConfiguration()
).subscribe(
/* onComplete = */ () -> SignalStore.donationsValues().setGooglePayReady(true),
/* onError = */ t -> SignalStore.donationsValues().setGooglePayReady(false)
);
}
@WorkerThread
private void initializeCleanup() {
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();

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,14 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onInviteToSignalClicked();
void onActivatePaymentsClicked();
void onSendPaymentClicked(@NonNull RecipientId recipientId);
void onScheduledIndicatorClicked(@NonNull View view, @NonNull MessageRecord messageRecord);
/** @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);
}
}

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

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

View File

@@ -0,0 +1,88 @@
package org.thoughtcrime.securesms
import android.content.Context
import android.view.View
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>,
displayCheckBox: Boolean,
displaySmsTag: DisplaySmsTag,
displaySecondaryInformation: DisplaySecondaryInformation,
onClickCallbacks: OnContactSelectionClick,
longClickCallbacks: LongClickCallbacks,
storyContextMenuCallbacks: StoryContextMenuCallbacks
) : ContactSearchAdapter(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, onClickCallbacks, longClickCallbacks, storyContextMenuCallbacks) {
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))
}
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
}
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
}
class ArbitraryRepository : org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository {
enum class ArbitraryRow(val code: String) {
NEW_GROUP("new-group"),
INVITE_TO_SIGNAL("invite-to-signal");
companion object {
fun fromCode(code: String) = values().first { it.code == code }
}
}
override fun getSize(section: ContactSearchConfiguration.Section.Arbitrary, query: String?): Int {
return if (query.isNullOrEmpty()) section.types.size else 0
}
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<*> {
val code = ArbitraryRow.fromCode(arbitrary.type)
return when (code) {
ArbitraryRow.NEW_GROUP -> NewGroupModel()
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
}
}
}
interface OnContactSelectionClick : ClickCallbacks {
fun onNewGroupClicked()
fun onInviteToSignalClicked()
}
}

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;
@@ -59,38 +55,36 @@ 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.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;
@@ -105,7 +99,6 @@ import kotlin.Unit;
* @author Moxie Marlinspike
*/
public final class ContactSelectionListFragment extends LoggingFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
@SuppressWarnings("unused")
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
@@ -137,27 +130,23 @@ public final class ContactSelectionListFragment extends LoggingFragment
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 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;
private ListClickListener listClickListener = new ListClickListener();
@Override
public void onAttach(@NonNull Context context) {
@@ -191,14 +180,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 +215,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();
}
@@ -305,7 +284,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 +331,75 @@ 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,
isMulti,
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
this::mapStateToConfiguration,
new ContactSearchMediator.SimpleCallbacks() {
@Override
public void onAdapterListCommitted(int size) {
onLoadFinished(size);
}
},
false,
(context, fixedContacts, displayCheckBox, displaySmsTag, displaySecondaryInformation, callbacks, longClickCallbacks, storyContextMenuCallbacks) -> new ContactSelectionListAdapter(
context,
fixedContacts,
displayCheckBox,
displaySmsTag,
displaySecondaryInformation,
new ContactSelectionListAdapter.OnContactSelectionClick() {
@Override
public void onNewGroupClicked() {
listCallback.onNewGroup(false);
}
@Override
public void onInviteToSignalClicked() {
listCallback.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 ContactSelectionListAdapter.ArbitraryRepository()
);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
constraintLayout = null;
}
private @NonNull Bundle safeArguments() {
return getArguments() != null ? getArguments() : new Bundle();
}
@@ -366,27 +410,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 +443,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 +451,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 +473,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 +497,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 +514,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 +545,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 +598,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 +616,18 @@ 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) {
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 +637,58 @@ 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 -> {
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(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 +708,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 +718,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) {
@@ -836,6 +791,116 @@ 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);
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 (listCallback != null) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
}
if (transportType != null) {
if (TextUtils.isEmpty(contactSearchState.getQuery()) && includeRecents) {
builder.addSection(new ContactSearchConfiguration.Section.Recents(
25,
mode,
includeInactiveGroups,
includeV1Groups,
includeSmsContacts,
includeSelf,
includeRecentsHeader,
null
));
}
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
includeSelf,
transportType,
true,
null,
!hideLetterHeaders()
));
}
if ((includeGroupsAfterContacts || !TextUtils.isEmpty(contactSearchState.getQuery())) && includeActiveGroups) {
builder.addSection(new ContactSearchConfiguration.Section.Groups(
includeSmsContacts,
includeV1Groups,
includeInactiveGroups,
false,
ContactSearchSortOrder.NATURAL,
false,
true,
null
));
}
if (listCallback != null) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode());
}
if (includeNew) {
builder.phone(newRowMode);
builder.username(newRowMode);
}
return Unit.INSTANCE;
});
}
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 static @NonNull ContactSearchConfiguration.NewRowMode resolveNewRowMode(boolean isBlocked, boolean isActiveGroups) {
if (isBlocked) {
return ContactSearchConfiguration.NewRowMode.BLOCK;
} 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;
}
public interface OnContactSelectedListener {
/**
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
@@ -868,10 +933,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();
}
}
}

View File

@@ -140,27 +140,18 @@ public class DeviceActivity extends PassphraseRequiredActivity
Uri uri = Uri.parse(data);
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
if (Build.VERSION.SDK_INT >= 21) {
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
deviceLinkFragment.setSharedElementEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceLinkFragment.setEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
deviceLinkFragment.setSharedElementEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceLinkFragment.setEnterTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
getSupportFragmentManager().beginTransaction()
.addToBackStack(null)
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
.replace(R.id.fragment_container, deviceLinkFragment)
.commit();
getSupportFragmentManager().beginTransaction()
.addToBackStack(null)
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
.replace(R.id.fragment_container, deviceLinkFragment)
.commit();
} else {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_bottom)
.replace(R.id.fragment_container, deviceLinkFragment)
.addToBackStack(null)
.commit();
}
});
}

View File

@@ -41,22 +41,19 @@ public class DeviceAddFragment extends LoggingFragment {
this.devicesImage = container.findViewById(R.id.devices);
ViewCompat.setTransitionName(devicesImage, "devices");
if (Build.VERSION.SDK_INT >= 21) {
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@TargetApi(21)
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom)
{
v.removeOnLayoutChangeListener(this);
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom)
{
v.removeOnLayoutChangeListener(this);
Animator reveal = ViewAnimationUtils.createCircularReveal(v, right, bottom, 0, (int) Math.hypot(right, bottom));
reveal.setInterpolator(new DecelerateInterpolator(2f));
reveal.setDuration(800);
reveal.start();
}
});
}
Animator reveal = ViewAnimationUtils.createCircularReveal(v, right, bottom, 0, (int) Math.hypot(right, bottom));
reveal.setInterpolator(new DecelerateInterpolator(2f));
reveal.setDuration(800);
reveal.start();
}
});
scannerView.start(getViewLifecycleOwner(), CameraXModelBlocklist.isBlocklisted());

View File

@@ -23,15 +23,15 @@ import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Util;
@@ -62,7 +62,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_SMS);
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactSelectionDisplayMode.FLAG_SMS);
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS);
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
@@ -254,7 +254,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
Recipient recipient = Recipient.resolved(recipientId);
int subscriptionId = recipient.getDefaultSubscriptionId().orElse(-1);
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
MessageSender.send(context, OutgoingMessage.sms(recipient, message, subscriptionId), -1L, MessageSender.SendType.SMS, null, null);
if (recipient.getContactUri() != null) {
SignalDatabase.recipients().setHasSentInvite(recipient.getId());

View File

@@ -20,6 +20,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.result.ActivityResultLauncher;
@@ -39,26 +40,22 @@ import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository;
import org.thoughtcrime.securesms.contacts.management.ContactsManagementViewModel;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -233,18 +230,14 @@ public class NewConversationActivity extends ContactSelectionActivity
}
@Override
public boolean onLongClick(ContactSelectionListItem contactSelectionListItem, RecyclerView recyclerView) {
RecipientId recipientId = contactSelectionListItem.getRecipientId().orElse(null);
if (recipientId == null) {
return false;
}
public boolean onLongClick(View anchorView, ContactSearchKey contactSearchKey, RecyclerView recyclerView) {
RecipientId recipientId = contactSearchKey.requireRecipientSearchKey().getRecipientId();
List<ActionItem> actions = generateContextualActionsForRecipient(recipientId);
if (actions.isEmpty()) {
return false;
}
new SignalContextMenu.Builder(contactSelectionListItem, (ViewGroup) contactSelectionListItem.getRootView())
new SignalContextMenu.Builder(anchorView, (ViewGroup) anchorView.getRootView())
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW)
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
.offsetX((int) DimensionUnit.DP.toPixels(12))

View File

@@ -307,10 +307,8 @@ public class PassphrasePromptActivity extends PassphraseActivity {
public Unit showConfirmDeviceCredentialIntent() {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
Intent intent = null;
if (Build.VERSION.SDK_INT >= 21) {
intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
}
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
startActivityForResult(intent, AUTHENTICATE_REQUEST_CODE);
return Unit.INSTANCE;
}

View File

@@ -602,7 +602,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
public void handleGroupMemberCountChange(int count) {
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize() && FeatureFlags.groupCallRinging();
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize();
callScreen.enableRingGroup(canRing);
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
}

View File

@@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.absbackup
/**
* Abstracts away the implementation of pieces of data we want to hand off to various backup services.
* Here we can control precisely which data gets backed up and more importantly, what does not.
*/
interface AndroidBackupItem {
fun getKey(): String
fun getDataForBackup(): ByteArray
fun restoreData(data: ByteArray)
}

View File

@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.absbackup
import android.app.backup.BackupAgent
import android.app.backup.BackupDataInput
import android.app.backup.BackupDataOutput
import android.os.ParcelFileDescriptor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.absbackup.backupables.KbsAuthTokens
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
/**
* Uses the [Android Backup Service](https://developer.android.com/guide/topics/data/keyvaluebackup) and backs up everything in [items]
*/
class SignalBackupAgent : BackupAgent() {
private val items: List<AndroidBackupItem> = listOf(
KbsAuthTokens
)
override fun onBackup(oldState: ParcelFileDescriptor?, data: BackupDataOutput, newState: ParcelFileDescriptor) {
val contentsHash = cumulativeHashCode()
if (oldState == null) {
performBackup(data)
} else {
val hash = try {
DataInputStream(FileInputStream(oldState.fileDescriptor)).use { it.readInt() }
} catch (e: IOException) {
Log.w(TAG, "No old state, may be first backup request or bug with not writing to newState at end.", e)
}
if (hash != contentsHash) {
performBackup(data)
}
}
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(contentsHash) }
}
private fun performBackup(data: BackupDataOutput) {
items.forEach {
val backupData = it.getDataForBackup()
data.writeEntityHeader(it.getKey(), backupData.size)
data.writeEntityData(backupData, backupData.size)
}
}
override fun onRestore(dataInput: BackupDataInput, appVersionCode: Int, newState: ParcelFileDescriptor) {
while (dataInput.readNextHeader()) {
val buffer = ByteArray(dataInput.dataSize)
dataInput.readEntityData(buffer, 0, dataInput.dataSize)
items.find { dataInput.key == it.getKey() }?.restoreData(buffer)
}
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) }
}
private fun cumulativeHashCode(): Int {
return items.fold("") { acc: String, androidBackupItem: AndroidBackupItem -> acc + androidBackupItem.getDataForBackup().decodeToString() }.hashCode()
}
companion object {
private const val TAG = "SignalBackupAgent"
}
}

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.absbackup.backupables
import com.google.protobuf.InvalidProtocolBufferException
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.absbackup.AndroidBackupItem
import org.thoughtcrime.securesms.absbackup.protos.KbsAuthToken
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* This backs up the not-secret KBS Auth tokens, which can be combined with a PIN to prove ownership of a phone number in order to complete the registration process.
*/
object KbsAuthTokens : AndroidBackupItem {
private const val TAG = "KbsAuthTokens"
override fun getKey(): String {
return TAG
}
override fun getDataForBackup(): ByteArray {
val proto = KbsAuthToken(tokens = SignalStore.kbsValues().kbsAuthTokenList)
return proto.encode()
}
override fun restoreData(data: ByteArray) {
if (SignalStore.kbsValues().kbsAuthTokenList.isNotEmpty()) {
return
}
try {
val proto = KbsAuthToken.ADAPTER.decode(data)
SignalStore.kbsValues().putAuthTokenList(proto.tokens)
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, "Cannot restore KbsAuthToken from backup service.")
}
}
}

View File

@@ -11,7 +11,7 @@ public abstract class AnimationCompleteListener implements Animator.AnimatorList
public abstract void onAnimationEnd(Animator animation);
@Override
public final void onAnimationCancel(Animator animation) {}
public void onAnimationCancel(Animator animation) {}
@Override
public final void onAnimationRepeat(Animator animation) {}
}

View File

@@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.animation
import android.animation.Animator
abstract class AnimationStartListener : Animator.AnimatorListener {
override fun onAnimationEnd(animation: Animator) = Unit
override fun onAnimationCancel(animation: Animator) = Unit
override fun onAnimationRepeat(animation: Animator) = Unit
}

View File

@@ -13,7 +13,6 @@ import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import androidx.annotation.RequiresApi
private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
private const val WIDTH = "signal.circleavatartransition.width"
@@ -22,7 +21,6 @@ private const val HEIGHT = "signal.circleavatartransition.height"
/**
* Custom transition for Circular avatars, because once you have multiple things animating stuff was getting broken and weird.
*/
@RequiresApi(21)
class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
override fun captureStartValues(transitionValues: TransitionValues) {
captureValues(transitionValues)

View File

@@ -7,11 +7,9 @@ import android.transition.Transition
import android.transition.TransitionValues
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
@RequiresApi(21)
class CrossfaderTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
companion object {

View File

@@ -10,7 +10,6 @@ import android.transition.TransitionValues
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.core.animation.addListener
import androidx.fragment.app.FragmentContainerView
@@ -19,7 +18,6 @@ private const val BOUNDS = "signal.wipedowntransition.bottom"
/**
* WipeDownTransition will animate the bottom position of a view such that it "wipes" down the screen to a final position.
*/
@RequiresApi(21)
class WipeDownTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
override fun captureStartValues(transitionValues: TransitionValues) {
captureValues(transitionValues)

View File

@@ -7,8 +7,8 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.stickers.StickerLocator;
public abstract class Attachment {
@@ -118,13 +118,13 @@ public abstract class Attachment {
}
public boolean isInProgress() {
return transferState != AttachmentDatabase.TRANSFER_PROGRESS_DONE &&
transferState != AttachmentDatabase.TRANSFER_PROGRESS_FAILED &&
transferState != AttachmentDatabase.TRANSFER_PROGRESS_PERMANENT_FAILURE;
return transferState != AttachmentTable.TRANSFER_PROGRESS_DONE &&
transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED &&
transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE;
}
public boolean isPermanentlyFailed() {
return transferState == AttachmentDatabase.TRANSFER_PROGRESS_PERMANENT_FAILURE;
return transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE;
}
public long getSize() {

View File

@@ -6,7 +6,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.stickers.StickerLocator;

View File

@@ -5,8 +5,8 @@ import android.net.Uri;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.MessageTable;
public class MmsNotificationAttachment extends Attachment {
@@ -26,14 +26,14 @@ public class MmsNotificationAttachment extends Attachment {
}
private static int getTransferStateFromStatus(int status) {
if (status == MmsDatabase.Status.DOWNLOAD_INITIALIZED ||
status == MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY)
if (status == MessageTable.MmsStatus.DOWNLOAD_INITIALIZED ||
status == MessageTable.MmsStatus.DOWNLOAD_NO_CONNECTIVITY)
{
return AttachmentDatabase.TRANSFER_PROGRESS_PENDING;
} else if (status == MmsDatabase.Status.DOWNLOAD_CONNECTING) {
return AttachmentDatabase.TRANSFER_PROGRESS_STARTED;
return AttachmentTable.TRANSFER_PROGRESS_PENDING;
} else if (status == MessageTable.MmsStatus.DOWNLOAD_CONNECTING) {
return AttachmentTable.TRANSFER_PROGRESS_STARTED;
} else {
return AttachmentDatabase.TRANSFER_PROGRESS_FAILED;
return AttachmentTable.TRANSFER_PROGRESS_FAILED;
}
}
}

View File

@@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
@@ -68,7 +68,7 @@ public class PointerAttachment extends Attachment {
return results;
}
public static List<Attachment> forPointers(List<SignalServiceDataMessage.Quote.QuotedAttachment> pointers) {
public static List<Attachment> forPointers(@Nullable List<SignalServiceDataMessage.Quote.QuotedAttachment> pointers) {
List<Attachment> results = new LinkedList<>();
if (pointers != null) {
@@ -102,7 +102,7 @@ public class PointerAttachment extends Attachment {
}
return Optional.of(new PointerAttachment(pointer.get().getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
AttachmentTable.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().orElse(0),
pointer.get().asPointer().getFileName().orElse(null),
pointer.get().asPointer().getCdnNumber(),
@@ -126,7 +126,7 @@ public class PointerAttachment extends Attachment {
SignalServiceAttachment thumbnail = pointer.getThumbnail();
return Optional.of(new PointerAttachment(pointer.getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
AttachmentTable.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
pointer.getFileName(),
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,

View File

@@ -5,7 +5,7 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentTable;
/**
* An attachment that represents where an attachment used to be. Useful when you need to know that
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
public class TombstoneAttachment extends Attachment {
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
}
@Override

View File

@@ -7,9 +7,11 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import java.util.Objects;
public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri;
@@ -51,7 +53,7 @@ public class UriAttachment extends Attachment {
@Nullable TransformProperties transformProperties)
{
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.dataUri = dataUri;
this.dataUri = Objects.requireNonNull(dataUri);
}
@Override

View File

@@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.audio;
import androidx.annotation.NonNull;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
import java.util.concurrent.TimeUnit;
public class AudioFileInfo {
private final long durationUs;
private final byte[] waveFormBytes;
private final float[] waveForm;
public static @NonNull AudioFileInfo fromDatabaseProtobuf(@NonNull AudioWaveFormData audioWaveForm) {
return new AudioFileInfo(audioWaveForm.getDurationUs(), audioWaveForm.getWaveForm().toByteArray());
}
AudioFileInfo(long durationUs, byte[] waveFormBytes) {
this.durationUs = durationUs;
this.waveFormBytes = waveFormBytes;
this.waveForm = new float[waveFormBytes.length];
for (int i = 0; i < waveFormBytes.length; i++) {
int unsigned = waveFormBytes[i] & 0xff;
this.waveForm[i] = unsigned / 255f;
}
}
public long getDuration(@NonNull TimeUnit timeUnit) {
return timeUnit.convert(durationUs, TimeUnit.MICROSECONDS);
}
public float[] getWaveForm() {
return waveForm;
}
public @NonNull AudioWaveFormData toDatabaseProtobuf() {
return AudioWaveFormData.newBuilder()
.setDurationUs(durationUs)
.setWaveForm(ByteString.copyFrom(waveFormBytes))
.build();
}
}

View File

@@ -1,42 +1,69 @@
package org.thoughtcrime.securesms.audio;
import android.content.Context;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.subjects.SingleSubject;
public class AudioRecorder {
private static final String TAG = Log.tag(AudioRecorder.class);
private static final ExecutorService executor = SignalExecutors.newCachedSingleThreadExecutor("signal-AudioRecorder");
private final Context context;
private final Context context;
private final AudioRecordingHandler uiHandler;
private final AudioRecorderFocusManager audioFocusManager;
private Recorder recorder;
private Uri captureUri;
public AudioRecorder(@NonNull Context context) {
this.context = context;
private SingleSubject<VoiceNoteDraft> recordingSubject;
public AudioRecorder(@NonNull Context context, @Nullable AudioRecordingHandler uiHandler) {
this.context = context;
this.uiHandler = uiHandler;
AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener;
if (this.uiHandler != null) {
onAudioFocusChangeListener = focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording");
this.uiHandler.onRecordCanceled(false);
}
};
} else {
onAudioFocusChangeListener = focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording");
stopRecording();
}
};
}
audioFocusManager = AudioRecorderFocusManager.create(context, onAudioFocusChangeListener);
}
public void startRecording() {
public @NonNull Single<VoiceNoteDraft> startRecording() {
Log.i(TAG, "startRecording()");
final SingleSubject<VoiceNoteDraft> recordingSingle = SingleSubject.create();
executor.execute(() -> {
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
try {
@@ -50,48 +77,46 @@ public class AudioRecorder {
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType(MediaUtil.AUDIO_AAC)
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
int focusResult = audioFocusManager.requestAudioFocus();
if (focusResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.w(TAG, "Could not gain audio focus. Received result code " + focusResult);
}
recorder.start(fds[1]);
this.recordingSubject = recordingSingle;
} catch (IOException e) {
recordingSingle.onError(e);
recorder = null;
Log.w(TAG, e);
}
});
return recordingSingle;
}
public @NonNull ListenableFuture<VoiceNoteDraft> stopRecording() {
public void stopRecording() {
Log.i(TAG, "stopRecording()");
final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
executor.execute(() -> {
if (recorder == null) {
sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!"));
Log.e(TAG, "MediaRecorder was never initialized successfully!");
return;
}
audioFocusManager.abandonAudioFocus();
recorder.stop();
try {
long size = MediaUtil.getMediaSize(context, captureUri);
sendToFuture(future, new VoiceNoteDraft(captureUri, size));
recordingSubject.onSuccess(new VoiceNoteDraft(captureUri, size));
} catch (IOException ioe) {
Log.w(TAG, ioe);
sendToFuture(future, ioe);
recordingSubject.onError(ioe);
}
recorder = null;
captureUri = null;
recordingSubject = null;
recorder = null;
captureUri = null;
});
return future;
}
private <T> void sendToFuture(final SettableFuture<T> future, final Exception exception) {
ThreadUtil.runOnMain(() -> future.setException(exception));
}
private <T> void sendToFuture(final SettableFuture<T> future, final T result) {
ThreadUtil.runOnMain(() -> future.set(result));
}
}

View File

@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.audio
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener
import android.os.Build
import androidx.annotation.RequiresApi
import org.thoughtcrime.securesms.util.ServiceUtil
abstract class AudioRecorderFocusManager(val context: Context) {
protected val audioManager: AudioManager = ServiceUtil.getAudioManager(context)
abstract fun requestAudioFocus(): Int
abstract fun abandonAudioFocus(): Int
companion object {
@JvmStatic
fun create(context: Context, changeListener: OnAudioFocusChangeListener): AudioRecorderFocusManager {
return if (Build.VERSION.SDK_INT >= 26) {
AudioRecorderFocusManager26(context, changeListener)
} else {
AudioRecorderFocusManagerLegacy(context, changeListener)
}
}
}
}
@RequiresApi(26)
private class AudioRecorderFocusManager26(context: Context, changeListener: OnAudioFocusChangeListener) : AudioRecorderFocusManager(context) {
val audioFocusRequest: AudioFocusRequest
init {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
.setAudioAttributes(audioAttributes)
.setOnAudioFocusChangeListener(changeListener)
.build()
}
override fun requestAudioFocus(): Int {
return audioManager.requestAudioFocus(audioFocusRequest)
}
override fun abandonAudioFocus(): Int {
return audioManager.abandonAudioFocusRequest(audioFocusRequest)
}
}
@Suppress("DEPRECATION")
private class AudioRecorderFocusManagerLegacy(context: Context, val changeListener: OnAudioFocusChangeListener) : AudioRecorderFocusManager(context) {
override fun requestAudioFocus(): Int {
return audioManager.requestAudioFocus(changeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
}
override fun abandonAudioFocus(): Int {
return audioManager.abandonAudioFocus(changeListener)
}
}

View File

@@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.audio
interface AudioRecordingHandler {
fun onRecordPressed()
fun onRecordReleased()
fun onRecordCanceled(byUser: Boolean)
fun onRecordLocked()
fun onRecordMoved(offsetX: Float, absoluteX: Float)
fun onRecordPermissionRequired()
}

View File

@@ -1,325 +0,0 @@
package org.thoughtcrime.securesms.audio;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.net.Uri;
import android.os.Build;
import android.util.LruCache;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import com.google.protobuf.ByteString;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
import org.thoughtcrime.securesms.media.MediaInput;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@RequiresApi(api = Build.VERSION_CODES.M)
public final class AudioWaveForm {
private static final String TAG = Log.tag(AudioWaveForm.class);
private static final int BAR_COUNT = 46;
private static final int SAMPLES_PER_BAR = 4;
private final Context context;
private final AudioSlide slide;
public AudioWaveForm(@NonNull Context context, @NonNull AudioSlide slide) {
this.context = context.getApplicationContext();
this.slide = slide;
}
private static final LruCache<String, AudioFileInfo> WAVE_FORM_CACHE = new LruCache<>(200);
private static final Executor AUDIO_DECODER_EXECUTOR = new SerialExecutor(SignalExecutors.BOUNDED);
@AnyThread
public void getWaveForm(@NonNull Consumer<AudioFileInfo> onSuccess, @NonNull Runnable onFailure) {
Uri uri = slide.getUri();
Attachment attachment = slide.asAttachment();
if (uri == null) {
Log.w(TAG, "No uri");
ThreadUtil.runOnMain(onFailure);
return;
}
String cacheKey = uri.toString();
AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
if (cached != null) {
Log.i(TAG, "Loaded wave form from cache " + cacheKey);
ThreadUtil.runOnMain(() -> onSuccess.accept(cached));
return;
}
AUDIO_DECODER_EXECUTOR.execute(() -> {
AudioFileInfo cachedInExecutor = WAVE_FORM_CACHE.get(cacheKey);
if (cachedInExecutor != null) {
Log.i(TAG, "Loaded wave form from cache inside executor" + cacheKey);
ThreadUtil.runOnMain(() -> onSuccess.accept(cachedInExecutor));
return;
}
AudioHash audioHash = attachment.getAudioHash();
if (audioHash != null) {
AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
if (audioFileInfo.waveForm.length == 0) {
Log.w(TAG, "Recovering from a wave form generation error " + cacheKey);
ThreadUtil.runOnMain(onFailure);
return;
} else if (audioFileInfo.waveForm.length != BAR_COUNT) {
Log.w(TAG, "Wave form from database does not match bar count, regenerating " + cacheKey);
} else {
WAVE_FORM_CACHE.put(cacheKey, audioFileInfo);
Log.i(TAG, "Loaded wave form from DB " + cacheKey);
ThreadUtil.runOnMain(() -> onSuccess.accept(audioFileInfo));
return;
}
}
if (attachment instanceof DatabaseAttachment) {
try {
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
long startTime = System.currentTimeMillis();
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
AudioFileInfo fileInfo = generateWaveForm(uri);
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (Throwable e) {
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
ThreadUtil.runOnMain(onFailure);
}
} else {
try {
Log.i(TAG, "Not in database and not cached. Generating wave form on-the-fly.");
long startTime = System.currentTimeMillis();
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
AudioFileInfo fileInfo = generateWaveForm(uri);
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (IOException e) {
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
ThreadUtil.runOnMain(onFailure);
}
}
});
}
/**
* Based on decode sample from:
* <p>
* https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/DecoderTest.java
*/
@WorkerThread
@RequiresApi(api = 23)
private @NonNull AudioFileInfo generateWaveForm(@NonNull Uri uri) throws IOException {
try (MediaInput dataSource = DecryptableUriMediaInput.createForUri(context, uri)) {
long[] wave = new long[BAR_COUNT];
int[] waveSamples = new int[BAR_COUNT];
MediaExtractor extractor = dataSource.createExtractor();
if (extractor.getTrackCount() == 0) {
throw new IOException("No audio track");
}
MediaFormat format = extractor.getTrackFormat(0);
if (!format.containsKey(MediaFormat.KEY_DURATION)) {
throw new IOException("Unknown duration");
}
long totalDurationUs = format.getLong(MediaFormat.KEY_DURATION);
String mime = format.getString(MediaFormat.KEY_MIME);
if (!mime.startsWith("audio/")) {
throw new IOException("Mime not audio");
}
MediaCodec codec = MediaCodec.createDecoderByType(mime);
if (totalDurationUs == 0) {
throw new IOException("Zero duration");
}
codec.configure(format, null, null, 0);
codec.start();
ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
extractor.selectTrack(0);
long kTimeOutUs = 5000;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
int noOutputCounter = 0;
while (!sawOutputEOS && noOutputCounter < 50) {
noOutputCounter++;
if (!sawInputEOS) {
int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
if (inputBufIndex >= 0) {
ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
int sampleSize = extractor.readSampleData(dstBuf, 0);
long presentationTimeUs = 0;
if (sampleSize < 0) {
sawInputEOS = true;
sampleSize = 0;
} else {
presentationTimeUs = extractor.getSampleTime();
}
codec.queueInputBuffer(
inputBufIndex,
0,
sampleSize,
presentationTimeUs,
sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
if (!sawInputEOS) {
int barSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
sawInputEOS = !extractor.advance();
int nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
while (!sawInputEOS && nextBarSampleIndex == barSampleIndex) {
sawInputEOS = !extractor.advance();
if (!sawInputEOS) {
nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
}
}
}
}
}
int outputBufferIndex;
do {
outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
if (outputBufferIndex >= 0) {
if (info.size > 0) {
noOutputCounter = 0;
}
ByteBuffer buf = codecOutputBuffers[outputBufferIndex];
int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs);
long total = 0;
for (int i = 0; i < info.size; i += 2 * 4) {
short aShort = buf.getShort(i);
total += Math.abs(aShort);
}
if (barIndex >= 0 && barIndex < wave.length) {
wave[barIndex] += total;
waveSamples[barIndex] += info.size / 2;
}
codec.releaseOutputBuffer(outputBufferIndex, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
sawOutputEOS = true;
}
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers();
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
Log.d(TAG, "output format has changed to " + codec.getOutputFormat());
}
} while (outputBufferIndex >= 0);
}
codec.stop();
codec.release();
extractor.release();
float[] floats = new float[BAR_COUNT];
byte[] bytes = new byte[BAR_COUNT];
float max = 0;
for (int i = 0; i < BAR_COUNT; i++) {
if (waveSamples[i] == 0) continue;
floats[i] = wave[i] / (float) waveSamples[i];
if (floats[i] > max) {
max = floats[i];
}
}
for (int i = 0; i < BAR_COUNT; i++) {
float normalized = floats[i] / max;
bytes[i] = (byte) (255 * normalized);
}
return new AudioFileInfo(totalDurationUs, bytes);
}
}
public static class AudioFileInfo {
private final long durationUs;
private final byte[] waveFormBytes;
private final float[] waveForm;
private static @NonNull AudioFileInfo fromDatabaseProtobuf(@NonNull AudioWaveFormData audioWaveForm) {
return new AudioFileInfo(audioWaveForm.getDurationUs(), audioWaveForm.getWaveForm().toByteArray());
}
private AudioFileInfo(long durationUs, byte[] waveFormBytes) {
this.durationUs = durationUs;
this.waveFormBytes = waveFormBytes;
this.waveForm = new float[waveFormBytes.length];
for (int i = 0; i < waveFormBytes.length; i++) {
int unsigned = waveFormBytes[i] & 0xff;
this.waveForm[i] = unsigned / 255f;
}
}
public long getDuration(@NonNull TimeUnit timeUnit) {
return timeUnit.convert(durationUs, TimeUnit.MICROSECONDS);
}
public float[] getWaveForm() {
return waveForm;
}
private @NonNull AudioWaveFormData toDatabaseProtobuf() {
return AudioWaveFormData.newBuilder()
.setDurationUs(durationUs)
.setWaveForm(ByteString.copyFrom(waveFormBytes))
.build();
}
}
}

View File

@@ -0,0 +1,168 @@
package org.thoughtcrime.securesms.audio;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
import org.thoughtcrime.securesms.media.MediaInput;
import java.io.IOException;
import java.nio.ByteBuffer;
@RequiresApi(api = 23)
public final class AudioWaveFormGenerator {
private static final String TAG = Log.tag(AudioWaveFormGenerator.class);
public static final int BAR_COUNT = 46;
private static final int SAMPLES_PER_BAR = 4;
private AudioWaveFormGenerator() {}
/**
* Based on decode sample from:
* <p>
* https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/DecoderTest.java
*/
@WorkerThread
public static @NonNull AudioFileInfo generateWaveForm(@NonNull Context context, @NonNull Uri uri) throws IOException {
try (MediaInput dataSource = DecryptableUriMediaInput.createForUri(context, uri)) {
long[] wave = new long[BAR_COUNT];
int[] waveSamples = new int[BAR_COUNT];
MediaExtractor extractor = dataSource.createExtractor();
if (extractor.getTrackCount() == 0) {
throw new IOException("No audio track");
}
MediaFormat format = extractor.getTrackFormat(0);
if (!format.containsKey(MediaFormat.KEY_DURATION)) {
throw new IOException("Unknown duration");
}
long totalDurationUs = format.getLong(MediaFormat.KEY_DURATION);
String mime = format.getString(MediaFormat.KEY_MIME);
if (!mime.startsWith("audio/")) {
throw new IOException("Mime not audio");
}
MediaCodec codec = MediaCodec.createDecoderByType(mime);
if (totalDurationUs == 0) {
throw new IOException("Zero duration");
}
codec.configure(format, null, null, 0);
codec.start();
extractor.selectTrack(0);
long kTimeOutUs = 5000;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
int noOutputCounter = 0;
while (!sawOutputEOS && noOutputCounter < 50) {
noOutputCounter++;
if (!sawInputEOS) {
int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
if (inputBufIndex >= 0) {
ByteBuffer dstBuf = codec.getInputBuffer(inputBufIndex);
int sampleSize = extractor.readSampleData(dstBuf, 0);
long presentationTimeUs = 0;
if (sampleSize < 0) {
sawInputEOS = true;
sampleSize = 0;
} else {
presentationTimeUs = extractor.getSampleTime();
}
codec.queueInputBuffer(
inputBufIndex,
0,
sampleSize,
presentationTimeUs,
sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
if (!sawInputEOS) {
int barSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
sawInputEOS = !extractor.advance();
int nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
while (!sawInputEOS && nextBarSampleIndex == barSampleIndex) {
sawInputEOS = !extractor.advance();
if (!sawInputEOS) {
nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
}
}
}
}
}
int outputBufferIndex;
do {
outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
if (outputBufferIndex >= 0) {
if (info.size > 0) {
noOutputCounter = 0;
}
ByteBuffer buf = codec.getOutputBuffer(outputBufferIndex);
int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs);
long total = 0;
for (int i = 0; i < info.size; i += 2 * 4) {
short aShort = buf.getShort(i);
total += Math.abs(aShort);
}
if (barIndex >= 0 && barIndex < wave.length) {
wave[barIndex] += total;
waveSamples[barIndex] += info.size / 2;
}
codec.releaseOutputBuffer(outputBufferIndex, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
sawOutputEOS = true;
}
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
Log.d(TAG, "output format has changed to " + codec.getOutputFormat());
}
} while (outputBufferIndex >= 0);
}
codec.stop();
codec.release();
extractor.release();
float[] floats = new float[BAR_COUNT];
byte[] bytes = new byte[BAR_COUNT];
float max = 0;
for (int i = 0; i < BAR_COUNT; i++) {
if (waveSamples[i] == 0) continue;
floats[i] = wave[i] / (float) waveSamples[i];
if (floats[i] > max) {
max = floats[i];
}
}
for (int i = 0; i < BAR_COUNT; i++) {
float normalized = floats[i] / max;
bytes[i] = (byte) (255 * normalized);
}
return new AudioFileInfo(totalDurationUs, bytes);
}
}
}

View File

@@ -0,0 +1,152 @@
package org.thoughtcrime.securesms.audio
import android.content.Context
import android.net.Uri
import android.util.LruCache
import androidx.annotation.AnyThread
import androidx.annotation.RequiresApi
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData
import java.io.IOException
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
/**
* Uses [AudioWaveFormGenerator] to generate audio wave forms.
*
* Maintains an in-memory cache of recently requested wave forms.
*/
@RequiresApi(23)
object AudioWaveForms {
private val TAG = Log.tag(AudioWaveForms::class.java)
private val cache = ThreadSafeLruCache(200)
@AnyThread
@JvmStatic
fun getWaveForm(context: Context, attachment: Attachment): Single<AudioFileInfo> {
val uri = attachment.uri
if (uri == null) {
Log.i(TAG, "No uri")
return Single.error(IllegalArgumentException("No uri from attachment"))
}
val cacheKey = uri.toString()
val cachedInfo = cache.get(cacheKey)
if (cachedInfo != null) {
Log.i(TAG, "Loaded wave form from cache $cacheKey")
return Single.just(cachedInfo)
}
val databaseCache = Single.fromCallable {
val audioHash = attachment.audioHash
return@fromCallable if (audioHash != null) {
checkDatabaseCache(cacheKey, audioHash.audioWaveForm)
} else {
Miss
}
}.subscribeOn(Schedulers.io())
val generateWaveForm: Single<CacheCheckResult> = if (attachment is DatabaseAttachment) {
Single.fromCallable { generateWaveForm(context, uri, cacheKey, attachment.attachmentId) }
} else {
Single.fromCallable { generateWaveForm(context, uri, cacheKey) }
}.subscribeOn(Schedulers.io())
return databaseCache
.flatMap { r ->
if (r is Miss) {
generateWaveForm
} else {
Single.just(r)
}
}
.map { r ->
if (r is Success) {
r.audioFileInfo
} else {
throw IOException("Unable to generate wave form")
}
}
}
private fun checkDatabaseCache(cacheKey: String, audioWaveForm: AudioWaveFormData): CacheCheckResult {
val audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioWaveForm)
if (audioFileInfo.waveForm.isEmpty()) {
Log.w(TAG, "Recovering from a wave form generation error $cacheKey")
return Failure
} else if (audioFileInfo.waveForm.size != AudioWaveFormGenerator.BAR_COUNT) {
Log.w(TAG, "Wave form from database does not match bar count, regenerating $cacheKey")
} else {
cache.put(cacheKey, audioFileInfo)
Log.i(TAG, "Loaded wave form from DB $cacheKey")
return Success(audioFileInfo)
}
return Miss
}
private fun generateWaveForm(context: Context, uri: Uri, cacheKey: String, attachmentId: AttachmentId): CacheCheckResult {
try {
val startTime = System.currentTimeMillis()
SignalDatabase.attachments.writeAudioHash(attachmentId, AudioWaveFormData.getDefaultInstance())
Log.i(TAG, "Starting wave form generation ($cacheKey)")
val fileInfo: AudioFileInfo = AudioWaveFormGenerator.generateWaveForm(context, uri)
Log.i(TAG, "Audio wave form generation time ${System.currentTimeMillis() - startTime} ms ($cacheKey)")
SignalDatabase.attachments.writeAudioHash(attachmentId, fileInfo.toDatabaseProtobuf())
cache.put(cacheKey, fileInfo)
return Success(fileInfo)
} catch (e: Throwable) {
Log.w(TAG, "Failed to create audio wave form for $cacheKey", e)
return Failure
}
}
private fun generateWaveForm(context: Context, uri: Uri, cacheKey: String): CacheCheckResult {
try {
Log.i(TAG, "Not in database and not cached. Generating wave form on-the-fly.")
val startTime = System.currentTimeMillis()
Log.i(TAG, "Starting wave form generation ($cacheKey)")
val fileInfo: AudioFileInfo = AudioWaveFormGenerator.generateWaveForm(context, uri)
Log.i(TAG, "Audio wave form generation time ${System.currentTimeMillis() - startTime} ms ($cacheKey)")
cache.put(cacheKey, fileInfo)
return Success(fileInfo)
} catch (e: Throwable) {
Log.w(TAG, "Failed to create audio wave form for $cacheKey", e)
return Failure
}
}
private class ThreadSafeLruCache(maxSize: Int) {
private val cache = LruCache<String, AudioFileInfo>(maxSize)
private val lock = ReentrantReadWriteLock()
fun put(key: String, info: AudioFileInfo) {
lock.write { cache.put(key, info) }
}
fun get(key: String): AudioFileInfo? {
return lock.read { cache.get(key) }
}
}
private sealed class CacheCheckResult
private class Success(val audioFileInfo: AudioFileInfo) : CacheCheckResult()
private object Failure : CacheCheckResult()
private object Miss : CacheCheckResult()
}

View File

@@ -21,7 +21,7 @@ sealed class Avatar(
data class Text(
val text: String,
val color: Avatars.ColorPair,
override val databaseId: DatabaseId,
override val databaseId: DatabaseId
) : Avatar(databaseId) {
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
return copy(databaseId = databaseId)
@@ -35,7 +35,7 @@ sealed class Avatar(
data class Vector(
val key: String,
val color: Avatars.ColorPair,
override val databaseId: DatabaseId,
override val databaseId: DatabaseId
) : Avatar(databaseId) {
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
return copy(databaseId = databaseId)

View File

@@ -6,6 +6,7 @@ import android.graphics.Canvas
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.annotation.MainThread
import androidx.appcompat.content.res.AppCompatResources
import com.airbnb.lottie.SimpleColorFilter
import org.signal.core.util.concurrent.SignalExecutors
@@ -28,8 +29,13 @@ object AvatarRenderer {
val DIMENSIONS = AvatarHelper.AVATAR_DIMENSIONS
private var typeface: Typeface? = null
@MainThread
fun getTypeface(context: Context): Typeface {
return Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
val interMedium = typeface ?: Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
typeface = interMedium
return interMedium
}
fun renderAvatar(context: Context, avatar: Avatar, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {

View File

@@ -74,7 +74,7 @@ object Avatars {
"avatar_sunset" to DefaultAvatar(R.drawable.ic_avatar_sunset, "A120"),
"avatar_surfboard" to DefaultAvatar(R.drawable.ic_avatar_surfboard, "A110"),
"avatar_soccerball" to DefaultAvatar(R.drawable.ic_avatar_soccerball, "A130"),
"avatar_football" to DefaultAvatar(R.drawable.ic_avatar_football, "A220"),
"avatar_football" to DefaultAvatar(R.drawable.ic_avatar_football, "A220")
)
@DrawableRes

View File

@@ -30,6 +30,7 @@ class TextAvatarDrawable(
setBounds(0, 0, size, size)
}
@Suppress("DEPRECATION")
override fun draw(canvas: Canvas) {
val width = bounds.width()
val textSize = Avatars.getTextSizeForLength(context, avatar.text, width * 0.8f, width * 0.45f)

View File

@@ -87,8 +87,9 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
val selectedPosition = items.indexOfFirst { it.isSelected }
adapter.submitList(items) {
if (selectedPosition > -1)
if (selectedPosition > -1) {
recycler.smoothScrollToPosition(selectedPosition)
}
}
}

View File

@@ -5,7 +5,7 @@ import org.thoughtcrime.securesms.avatar.AvatarColorItem
import org.thoughtcrime.securesms.avatar.Avatars
data class TextAvatarCreationState(
val currentAvatar: Avatar.Text,
val currentAvatar: Avatar.Text
) {
fun colors(): List<AvatarColorItem> = Avatars.colors.map { AvatarColorItem(it, currentAvatar.color == it) }
}

View File

@@ -5,7 +5,7 @@ import org.thoughtcrime.securesms.avatar.AvatarColorItem
import org.thoughtcrime.securesms.avatar.Avatars
data class VectorAvatarCreationState(
val currentAvatar: Avatar.Vector,
val currentAvatar: Avatar.Vector
) {
fun colors(): List<AvatarColorItem> = Avatars.colors.map { AvatarColorItem(it, currentAvatar.color == it) }
}

View File

@@ -1,30 +1,27 @@
package org.thoughtcrime.securesms.backup
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.MessageTable
/**
* Queries used by backup exporter to estimate total counts for various complicated tables.
*/
object BackupCountQueries {
const val mmsCount: String = "SELECT COUNT(*) FROM ${MmsDatabase.TABLE_NAME} WHERE ${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.VIEW_ONCE} <= 0"
const val smsCount: String = "SELECT COUNT(*) FROM ${SmsDatabase.TABLE_NAME} WHERE ${SmsDatabase.EXPIRES_IN} <= 0"
const val mmsCount: String = "SELECT COUNT(*) FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.VIEW_ONCE} <= 0"
@get:JvmStatic
val groupReceiptCount: String = """
SELECT COUNT(*) FROM ${GroupReceiptDatabase.TABLE_NAME}
INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${GroupReceiptDatabase.TABLE_NAME}.${GroupReceiptDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID}
WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0
SELECT COUNT(*) FROM ${GroupReceiptTable.TABLE_NAME}
INNER JOIN ${MessageTable.TABLE_NAME} ON ${GroupReceiptTable.TABLE_NAME}.${GroupReceiptTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0
""".trimIndent()
@get:JvmStatic
val attachmentCount: String = """
SELECT COUNT(*) FROM ${AttachmentDatabase.TABLE_NAME}
INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${AttachmentDatabase.TABLE_NAME}.${AttachmentDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID}
WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0
SELECT COUNT(*) FROM ${AttachmentTable.TABLE_NAME}
INNER JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0
""".trimIndent()
}

View File

@@ -42,7 +42,7 @@ public enum BackupFileIOError {
public void postNotification(@NonNull Context context) {
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), PendingIntentFlags.mutable());
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
.setSmallIcon(R.drawable.ic_signal_backup)
.setContentTitle(context.getString(titleId))
.setContentText(context.getString(messageId))

View File

@@ -6,6 +6,8 @@ import org.signal.core.util.Conversions;
import org.signal.core.util.StreamUtil;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.backup.proto.BackupFrame;
import org.thoughtcrime.securesms.backup.proto.Header;
import java.io.IOException;
import java.io.InputStream;
@@ -45,21 +47,21 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream {
byte[] headerFrame = new byte[headerLength];
StreamUtil.readFully(in, headerFrame);
BackupProtos.BackupFrame frame = BackupProtos.BackupFrame.parseFrom(headerFrame);
BackupFrame frame = BackupFrame.ADAPTER.decode(headerFrame);
if (!frame.hasHeader()) {
if (frame.header_ == null) {
throw new IOException("Backup stream does not start with header!");
}
BackupProtos.Header header = frame.getHeader();
Header header = frame.header_;
this.iv = header.getIv().toByteArray();
this.iv = header.iv.toByteArray();
if (iv.length != 16) {
throw new IOException("Invalid IV length!");
}
byte[] key = getBackupKey(passphrase, header.hasSalt() ? header.getSalt().toByteArray() : null);
byte[] key = getBackupKey(passphrase, header.salt != null ? header.salt.toByteArray() : null);
byte[] derived = HKDF.deriveSecrets(key, "Backup Export".getBytes(), 64);
byte[][] split = ByteUtil.split(derived, 32, 32);
@@ -76,7 +78,7 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream {
}
}
BackupProtos.BackupFrame readFrame() throws IOException {
BackupFrame readFrame() throws IOException {
return readFrame(in);
}
@@ -128,7 +130,7 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream {
}
}
private BackupProtos.BackupFrame readFrame(InputStream in) throws IOException {
private BackupFrame readFrame(InputStream in) throws IOException {
try {
byte[] length = new byte[4];
StreamUtil.readFully(in, length);
@@ -151,7 +153,7 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream {
byte[] plaintext = cipher.doFinal(frame, 0, frame.length - 10);
return BackupProtos.BackupFrame.parseFrom(plaintext);
return BackupFrame.ADAPTER.decode(plaintext);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}

View File

@@ -2,7 +2,10 @@ package org.thoughtcrime.securesms.backup
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import org.thoughtcrime.securesms.backup.proto.Attachment
import org.thoughtcrime.securesms.backup.proto.Avatar
import org.thoughtcrime.securesms.backup.proto.BackupFrame
import org.thoughtcrime.securesms.backup.proto.Sticker
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@@ -15,60 +18,65 @@ object BackupVerifier {
private val TAG = Log.tag(BackupVerifier::class.java)
@JvmStatic
fun verifyFile(cipherStream: InputStream, passphrase: String, expectedCount: Long): Boolean {
@Throws(IOException::class, FullBackupExporter.BackupCanceledException::class)
fun verifyFile(cipherStream: InputStream, passphrase: String, expectedCount: Long, cancellationSignal: FullBackupExporter.BackupCancellationSignal): Boolean {
val inputStream = BackupRecordInputStream(cipherStream, passphrase)
var count = 0L
var frame: BackupFrame = inputStream.readFrame()
while (!frame.end) {
val verified = when {
frame.hasAttachment() -> verifyAttachment(frame.attachment, inputStream)
frame.hasSticker() -> verifySticker(frame.sticker, inputStream)
frame.hasAvatar() -> verifyAvatar(frame.avatar, inputStream)
else -> true
cipherStream.use {
while (frame.end != true && !cancellationSignal.isCanceled) {
val verified = when {
frame.attachment != null -> verifyAttachment(frame.attachment!!, inputStream)
frame.sticker != null -> verifySticker(frame.sticker!!, inputStream)
frame.avatar != null -> verifyAvatar(frame.avatar!!, inputStream)
else -> true
}
if (!verified) {
return false
}
EventBus.getDefault().post(BackupEvent(BackupEvent.Type.PROGRESS_VERIFYING, ++count, expectedCount))
frame = inputStream.readFrame()
}
if (!verified) {
return false
}
EventBus.getDefault().post(BackupEvent(BackupEvent.Type.PROGRESS_VERIFYING, ++count, expectedCount))
frame = inputStream.readFrame()
}
cipherStream.close()
if (cancellationSignal.isCanceled) {
throw FullBackupExporter.BackupCanceledException()
}
return true
}
private fun verifyAttachment(attachment: BackupProtos.Attachment, inputStream: BackupRecordInputStream): Boolean {
private fun verifyAttachment(attachment: Attachment, inputStream: BackupRecordInputStream): Boolean {
try {
inputStream.readAttachmentTo(NullOutputStream, attachment.length)
inputStream.readAttachmentTo(NullOutputStream, attachment.length ?: 0)
} catch (e: IOException) {
Log.w(TAG, "Bad attachment: ${attachment.attachmentId}", e)
Log.w(TAG, "Bad attachment id: ${attachment.attachmentId} len: ${attachment.length}", e)
return false
}
return true
}
private fun verifySticker(sticker: BackupProtos.Sticker, inputStream: BackupRecordInputStream): Boolean {
private fun verifySticker(sticker: Sticker, inputStream: BackupRecordInputStream): Boolean {
try {
inputStream.readAttachmentTo(NullOutputStream, sticker.length)
inputStream.readAttachmentTo(NullOutputStream, sticker.length ?: 0)
} catch (e: IOException) {
Log.w(TAG, "Bad sticker: ${sticker.rowId}", e)
Log.w(TAG, "Bad sticker id: ${sticker.rowId} len: ${sticker.length}", e)
return false
}
return true
}
private fun verifyAvatar(avatar: BackupProtos.Avatar, inputStream: BackupRecordInputStream): Boolean {
private fun verifyAvatar(avatar: Avatar, inputStream: BackupRecordInputStream): Boolean {
try {
inputStream.readAttachmentTo(NullOutputStream, avatar.length)
inputStream.readAttachmentTo(NullOutputStream, avatar.length ?: 0)
} catch (e: IOException) {
Log.w(TAG, "Bad sticker: ${avatar.recipientId}", e)
Log.w(TAG, "Bad avatar id: ${avatar.recipientId} len: ${avatar.length}", e)
return false
}
return true

View File

@@ -8,10 +8,10 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.documentfile.provider.DocumentFile;
import com.annimon.stream.function.Predicate;
import com.google.protobuf.ByteString;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
@@ -19,31 +19,39 @@ import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SetUtil;
import org.signal.core.util.SqlUtil;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.backup.proto.Attachment;
import org.thoughtcrime.securesms.backup.proto.Avatar;
import org.thoughtcrime.securesms.backup.proto.BackupFrame;
import org.thoughtcrime.securesms.backup.proto.DatabaseVersion;
import org.thoughtcrime.securesms.backup.proto.Header;
import org.thoughtcrime.securesms.backup.proto.KeyValue;
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
import org.thoughtcrime.securesms.backup.proto.Sticker;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.EmojiSearchTable;
import org.thoughtcrime.securesms.database.GroupReceiptTable;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.MentionDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
import org.thoughtcrime.securesms.database.ReactionDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.SenderKeyDatabase;
import org.thoughtcrime.securesms.database.SenderKeySharedDatabase;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.MentionTable;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.OneTimePreKeyTable;
import org.thoughtcrime.securesms.database.PendingRetryReceiptTable;
import org.thoughtcrime.securesms.database.ReactionTable;
import org.thoughtcrime.securesms.database.SearchTable;
import org.thoughtcrime.securesms.database.SenderKeyTable;
import org.thoughtcrime.securesms.database.SenderKeySharedTable;
import org.thoughtcrime.securesms.database.SessionTable;
import org.thoughtcrime.securesms.database.SignedPreKeyTable;
import org.thoughtcrime.securesms.database.StickerTable;
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -62,10 +70,15 @@ import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -75,6 +88,8 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import okio.ByteString;
public class FullBackupExporter extends FullBackupBase {
private static final String TAG = Log.tag(FullBackupExporter.class);
@@ -84,16 +99,19 @@ public class FullBackupExporter extends FullBackupBase {
private static final long IDENTITY_KEY_BACKUP_RECORD_COUNT = 2L;
private static final long FINAL_MESSAGE_COUNT = 1L;
private static final Set<String> BLACKLISTED_TABLES = SetUtil.newHashSet(
SignedPreKeyDatabase.TABLE_NAME,
OneTimePreKeyDatabase.TABLE_NAME,
SessionDatabase.TABLE_NAME,
SearchDatabase.SMS_FTS_TABLE_NAME,
SearchDatabase.MMS_FTS_TABLE_NAME,
EmojiSearchDatabase.TABLE_NAME,
SenderKeyDatabase.TABLE_NAME,
SenderKeySharedDatabase.TABLE_NAME,
PendingRetryReceiptDatabase.TABLE_NAME,
/**
* Tables in list will still have their *schema* exported (so the tables will be created),
* but we will not export the actual contents.
*/
private static final Set<String> TABLE_CONTENT_BLOCKLIST = SetUtil.newHashSet(
SignedPreKeyTable.TABLE_NAME,
OneTimePreKeyTable.TABLE_NAME,
SessionTable.TABLE_NAME,
SearchTable.FTS_TABLE_NAME,
EmojiSearchTable.TABLE_NAME,
SenderKeyTable.TABLE_NAME,
SenderKeySharedTable.TABLE_NAME,
PendingRetryReceiptTable.TABLE_NAME,
AvatarPickerDatabase.TABLE_NAME
);
@@ -161,27 +179,25 @@ public class FullBackupExporter extends FullBackupBase {
for (String table : tables) {
throwIfCanceled(cancellationSignal);
if (table.equals(MmsDatabase.TABLE_NAME)) {
if (table.equals(MessageTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, estimatedCount, cancellationSignal);
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count, estimatedCount, cancellationSignal);
} else if (table.equals(ReactionDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, new MessageId(CursorUtil.requireLong(cursor, ReactionDatabase.MESSAGE_ID), CursorUtil.requireBoolean(cursor, ReactionDatabase.IS_MMS))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(MentionDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, CursorUtil.requireLong(cursor, MentionDatabase.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (table.equals(StickerDatabase.TABLE_NAME)) {
} else if (table.equals(ReactionTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, new MessageId(CursorUtil.requireLong(cursor, ReactionTable.MESSAGE_ID))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(MentionTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, CursorUtil.requireLong(cursor, MentionTable.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(GroupReceiptTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptTable.MMS_ID))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(AttachmentTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (table.equals(StickerTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) {
} else if (!TABLE_CONTENT_BLOCKLIST.contains(table)) {
count = exportTable(table, input, outputStream, null, null, count, estimatedCount, cancellationSignal);
}
stopwatch.split("table::" + table);
}
for (BackupProtos.SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) {
for (SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) {
throwIfCanceled(cancellationSignal);
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(preference);
@@ -219,17 +235,15 @@ public class FullBackupExporter extends FullBackupBase {
long count = DATABASE_VERSION_RECORD_COUNT + TABLE_RECORD_COUNT_MULTIPLIER * tables.size();
for (String table : tables) {
if (table.equals(MmsDatabase.TABLE_NAME)) {
if (table.equals(MessageTable.TABLE_NAME)) {
count += getCount(input, BackupCountQueries.mmsCount);
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
count += getCount(input, BackupCountQueries.smsCount);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
} else if (table.equals(GroupReceiptTable.TABLE_NAME)) {
count += getCount(input, BackupCountQueries.getGroupReceiptCount());
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
} else if (table.equals(AttachmentTable.TABLE_NAME)) {
count += getCount(input, BackupCountQueries.getAttachmentCount());
} else if (table.equals(StickerDatabase.TABLE_NAME)) {
} else if (table.equals(StickerTable.TABLE_NAME)) {
count += getCount(input, "SELECT COUNT(*) FROM " + table);
} else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) {
} else if (!TABLE_CONTENT_BLOCKLIST.contains(table)) {
count += getCount(input, "SELECT COUNT(*) FROM " + table);
}
}
@@ -266,31 +280,110 @@ public class FullBackupExporter extends FullBackupBase {
private static List<String> exportSchema(@NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream)
throws IOException
{
List<String> tables = new LinkedList<>();
List<String> tablesInOrder = getTablesToExportInOrder(input);
try (Cursor cursor = input.rawQuery("SELECT sql, name, type FROM sqlite_master", null)) {
Map<String, String> createStatementsByTable = new HashMap<>();
try (Cursor cursor = input.rawQuery("SELECT sql, name, type FROM sqlite_master WHERE type = 'table' AND sql NOT NULL", null)) {
while (cursor != null && cursor.moveToNext()) {
String sql = cursor.getString(0);
String name = cursor.getString(1);
String type = cursor.getString(2);
if (sql != null) {
boolean isSmsFtsSecretTable = name != null && !name.equals(SearchDatabase.SMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME);
boolean isMmsFtsSecretTable = name != null && !name.equals(SearchDatabase.MMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME);
boolean isEmojiFtsSecretTable = name != null && !name.equals(EmojiSearchDatabase.TABLE_NAME) && name.startsWith(EmojiSearchDatabase.TABLE_NAME);
createStatementsByTable.put(name, sql);
}
}
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable && !isEmojiFtsSecretTable) {
if ("table".equals(type)) {
tables.add(name);
}
for (String table : tablesInOrder) {
String statement = createStatementsByTable.get(table);
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(cursor.getString(0)).build());
}
if (statement != null) {
outputStream.write(new SqlStatement.Builder().statement(statement).build());
} else {
throw new IOException("Failed to find a create statement for table: " + table);
}
}
try (Cursor cursor = input.rawQuery("SELECT sql, name, type FROM sqlite_master where type != 'table' AND sql NOT NULL", null)) {
while (cursor != null && cursor.moveToNext()) {
String sql = cursor.getString(0);
String name = cursor.getString(1);
if (isTableAllowed(name)) {
outputStream.write(new SqlStatement.Builder().statement(sql).build());
}
}
}
return tables;
return tablesInOrder;
}
/**
* Returns the list of tables we should export, in the order they should be exported in.
* The order is chosen to ensure we won't violate any foreign key constraints when we import them.
*/
private static List<String> getTablesToExportInOrder(@NonNull SQLiteDatabase input) {
List<String> tables = SqlUtil.getAllTables(input)
.stream()
.filter(FullBackupExporter::isTableAllowed)
.sorted()
.collect(Collectors.toList());
Map<String, Set<String>> dependsOn = new LinkedHashMap<>();
for (String table : tables) {
dependsOn.put(table, SqlUtil.getForeignKeyDependencies(input, table));
}
return computeTableOrder(dependsOn);
}
@VisibleForTesting
static List<String> computeTableOrder(@NonNull Map<String, Set<String>> dependsOn) {
List<String> rootNodes = dependsOn.keySet()
.stream()
.filter(table -> {
boolean nothingDependsOnIt = dependsOn.values().stream().noneMatch(it -> it.contains(table));
return nothingDependsOnIt;
})
.sorted()
.collect(Collectors.toList());
LinkedHashSet<String> outputOrder = new LinkedHashSet<>();
for (String root : rootNodes) {
postOrderTraversal(root, dependsOn, outputOrder);
}
return new ArrayList<>(outputOrder);
}
private static void postOrderTraversal(String current, Map<String, Set<String>> dependsOn, LinkedHashSet<String> outputOrder) {
Set<String> dependencies = dependsOn.get(current);
if (dependencies == null || dependencies.isEmpty()) {
outputOrder.add(current);
return;
}
for (String dependency : dependencies) {
postOrderTraversal(dependency, dependsOn, outputOrder);
}
outputOrder.add(current);
}
private static boolean isTableAllowed(@Nullable String table) {
if (table == null) {
return true;
}
boolean isReservedTable = table.startsWith("sqlite_");
boolean isMmsFtsSecretTable = !table.equals(SearchTable.FTS_TABLE_NAME) && table.startsWith(SearchTable.FTS_TABLE_NAME);
boolean isEmojiFtsSecretTable = !table.equals(EmojiSearchTable.TABLE_NAME) && table.startsWith(EmojiSearchTable.TABLE_NAME);
return !isReservedTable &&
!isMmsFtsSecretTable &&
!isEmojiFtsSecretTable;
}
private static int exportTable(@NonNull String table,
@@ -310,8 +403,10 @@ public class FullBackupExporter extends FullBackupBase {
throwIfCanceled(cancellationSignal);
if (predicate == null || predicate.test(cursor)) {
StringBuilder statement = new StringBuilder(template);
BackupProtos.SqlStatement.Builder statementBuilder = BackupProtos.SqlStatement.newBuilder();
StringBuilder statement = new StringBuilder(template);
SqlStatement.Builder statementBuilder = new SqlStatement.Builder();
statementBuilder.parameters = new ArrayList<>();
statement.append('(');
@@ -319,15 +414,15 @@ public class FullBackupExporter extends FullBackupBase {
statement.append('?');
if (cursor.getType(i) == Cursor.FIELD_TYPE_STRING) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setStringParamter(cursor.getString(i)));
statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().stringParamter(cursor.getString(i)).build());
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_FLOAT) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setDoubleParameter(cursor.getDouble(i)));
statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().doubleParameter(cursor.getDouble(i)).build());
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_INTEGER) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setIntegerParameter(cursor.getLong(i)));
statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().integerParameter(cursor.getLong(i)).build());
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))));
statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().blobParameter(new ByteString(cursor.getBlob(i))).build());
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_NULL) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setNullparameter(true));
statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().nullparameter(true).build());
} else {
throw new AssertionError("unknown type?" + cursor.getType(i));
}
@@ -340,7 +435,7 @@ public class FullBackupExporter extends FullBackupBase {
statement.append(')');
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(statementBuilder.setStatement(statement.toString()).build());
outputStream.write(statementBuilder.statement(statement.toString()).build());
if (postProcess != null) {
count = postProcess.postProcess(cursor, count);
@@ -359,12 +454,12 @@ public class FullBackupExporter extends FullBackupBase {
long estimatedCount)
throws IOException
{
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE));
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.ROW_ID));
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.UNIQUE_ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.SIZE));
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM));
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentTable.DATA));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentTable.DATA_RANDOM));
if (!TextUtils.isEmpty(data)) {
long fileLength = new File(data).length();
@@ -381,7 +476,7 @@ public class FullBackupExporter extends FullBackupBase {
try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) {
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
} catch (FileNotFoundException e) {
Log.w(TAG, "Missing attachment: " + e.getMessage());
Log.w(TAG, "Missing attachment", e);
}
}
@@ -395,18 +490,18 @@ public class FullBackupExporter extends FullBackupBase {
long estimatedCount)
throws IOException
{
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTable._ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTable.FILE_LENGTH));
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerTable.FILE_PATH));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerTable.FILE_RANDOM));
if (!TextUtils.isEmpty(data) && size > 0) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
outputStream.writeSticker(rowId, inputStream, size);
} catch (FileNotFoundException e) {
Log.w(TAG, "Missing sticker: " + e.getMessage());
Log.w(TAG, "Missing sticker", e);
}
}
@@ -424,7 +519,7 @@ public class FullBackupExporter extends FullBackupBase {
result += read;
}
} catch (FileNotFoundException e) {
Log.w(TAG, "Missing attachment: " + e.getMessage());
Log.w(TAG, "Missing attachment for size calculation", e);
return 0;
} catch (IOException e) {
Log.w(TAG, "Failed to determine stream length", e);
@@ -456,29 +551,30 @@ public class FullBackupExporter extends FullBackupBase {
if (!dataSet.containsKey(key)) {
continue;
}
BackupProtos.KeyValue.Builder builder = BackupProtos.KeyValue.newBuilder()
.setKey(key);
KeyValue.Builder builder = new KeyValue.Builder()
.key(key);
Class<?> type = dataSet.getType(key);
if (type == byte[].class) {
byte[] data = dataSet.getBlob(key, null);
if (data != null) {
builder.setBlobValue(ByteString.copyFrom(dataSet.getBlob(key, null)));
builder.blobValue(new ByteString(dataSet.getBlob(key, null)));
} else {
Log.w(TAG, "Skipping storing null blob for key: " + key);
}
} else if (type == Boolean.class) {
builder.setBooleanValue(dataSet.getBoolean(key, false));
builder.booleanValue(dataSet.getBoolean(key, false));
} else if (type == Float.class) {
builder.setFloatValue(dataSet.getFloat(key, 0));
builder.floatValue(dataSet.getFloat(key, 0));
} else if (type == Integer.class) {
builder.setIntegerValue(dataSet.getInteger(key, 0));
builder.integerValue(dataSet.getInteger(key, 0));
} else if (type == Long.class) {
builder.setLongValue(dataSet.getLong(key, 0));
builder.longValue(dataSet.getLong(key, 0));
} else if (type == String.class) {
String data = dataSet.getString(key, null);
if (data != null) {
builder.setStringValue(dataSet.getString(key, null));
builder.stringValue(dataSet.getString(key, null));
} else {
Log.w(TAG, "Skipping storing null string for key: " + key);
}
@@ -494,42 +590,24 @@ public class FullBackupExporter extends FullBackupBase {
}
private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) {
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 &&
cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
return cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.EXPIRES_IN)) <= 0 &&
cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.VIEW_ONCE)) <= 0;
}
private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) {
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0;
return cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.EXPIRES_IN)) <= 0;
}
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull MessageId messageId) {
if (messageId.isMms()) {
return isForNonExpiringMmsMessage(db, messageId.getId());
} else {
return isForNonExpiringSmsMessage(db, messageId.getId());
}
}
private static boolean isForNonExpiringSmsMessage(@NonNull SQLiteDatabase db, long smsId) {
String[] columns = new String[] { SmsDatabase.EXPIRES_IN };
String where = SmsDatabase.ID + " = ?";
String[] args = new String[] { String.valueOf(smsId) };
try (Cursor cursor = db.query(SmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
return isNonExpiringSmsMessage(cursor);
}
}
return false;
return isForNonExpiringMmsMessage(db, messageId.getId());
}
private static boolean isForNonExpiringMmsMessage(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MmsDatabase.RECIPIENT_ID, MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE };
String where = MmsDatabase.ID + " = ?";
String[] columns = new String[] { MessageTable.RECIPIENT_ID, MessageTable.EXPIRES_IN, MessageTable.VIEW_ONCE };
String where = MessageTable.ID + " = ?";
String[] args = new String[] { String.valueOf(mmsId) };
try (Cursor mmsCursor = db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
try (Cursor mmsCursor = db.query(MessageTable.TABLE_NAME, columns, where, args, null, null, null)) {
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return isNonExpiringMmsMessage(mmsCursor);
}
@@ -566,10 +644,12 @@ public class FullBackupExporter extends FullBackupBase {
mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
byte[] header = BackupProtos.BackupFrame.newBuilder().setHeader(BackupProtos.Header.newBuilder()
.setIv(ByteString.copyFrom(iv))
.setSalt(ByteString.copyFrom(salt)))
.build().toByteArray();
byte[] header = new BackupFrame.Builder().header_(new Header.Builder()
.iv(new okio.ByteString(iv))
.salt(new okio.ByteString(salt))
.build())
.build()
.encode();
outputStream.write(Conversions.intToByteArray(header.length));
outputStream.write(header);
@@ -578,26 +658,26 @@ public class FullBackupExporter extends FullBackupBase {
}
}
public void write(BackupProtos.SharedPreference preference) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setPreference(preference).build());
public void write(SharedPreference preference) throws IOException {
write(outputStream, new BackupFrame.Builder().preference(preference).build());
}
public void write(BackupProtos.KeyValue keyValue) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setKeyValue(keyValue).build());
public void write(KeyValue keyValue) throws IOException {
write(outputStream, new BackupFrame.Builder().keyValue(keyValue).build());
}
public void write(BackupProtos.SqlStatement statement) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setStatement(statement).build());
public void write(SqlStatement statement) throws IOException {
write(outputStream, new BackupFrame.Builder().statement(statement).build());
}
public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException {
try {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAvatar(BackupProtos.Avatar.newBuilder()
.setRecipientId(avatarName)
.setLength(Util.toIntExact(size))
.build())
.build());
write(outputStream, new BackupFrame.Builder()
.avatar(new Avatar.Builder()
.recipientId(avatarName)
.length(Util.toIntExact(size))
.build())
.build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write avatar to backup", e);
throw new InvalidBackupStreamException();
@@ -610,13 +690,13 @@ public class FullBackupExporter extends FullBackupBase {
public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException {
try {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAttachment(BackupProtos.Attachment.newBuilder()
.setRowId(attachmentId.getRowId())
.setAttachmentId(attachmentId.getUniqueId())
.setLength(Util.toIntExact(size))
.build())
.build());
write(outputStream, new BackupFrame.Builder()
.attachment(new Attachment.Builder()
.rowId(attachmentId.getRowId())
.attachmentId(attachmentId.getUniqueId())
.length(Util.toIntExact(size))
.build())
.build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write " + attachmentId + " to backup", e);
throw new InvalidBackupStreamException();
@@ -629,12 +709,12 @@ public class FullBackupExporter extends FullBackupBase {
public void writeSticker(long rowId, @NonNull InputStream in, long size) throws IOException {
try {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setSticker(BackupProtos.Sticker.newBuilder()
.setRowId(rowId)
.setLength(Util.toIntExact(size))
.build())
.build());
write(outputStream, new BackupFrame.Builder()
.sticker(new Sticker.Builder()
.rowId(rowId)
.length(Util.toIntExact(size))
.build())
.build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write sticker to backup", e);
throw new InvalidBackupStreamException();
@@ -646,13 +726,13 @@ public class FullBackupExporter extends FullBackupBase {
}
void writeDatabaseVersion(int version) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setVersion(BackupProtos.DatabaseVersion.newBuilder().setVersion(version))
.build());
write(outputStream, new BackupFrame.Builder()
.version(new DatabaseVersion.Builder().version(version).build())
.build());
}
void writeEnd() throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder().setEnd(true).build());
write(outputStream, new BackupFrame.Builder().end(true).build());
}
/**
@@ -693,12 +773,12 @@ public class FullBackupExporter extends FullBackupBase {
}
}
private void write(@NonNull OutputStream out, @NonNull BackupProtos.BackupFrame frame) throws IOException {
private void write(@NonNull OutputStream out, @NonNull BackupFrame frame) throws IOException {
try {
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
byte[] frameCiphertext = cipher.doFinal(frame.toByteArray());
byte[] frameCiphertext = cipher.doFinal(frame.encode());
byte[] frameMac = mac.doFinal(frameCiphertext);
byte[] length = Conversions.intToByteArray(frameCiphertext.length + 10);

View File

@@ -5,30 +5,33 @@ import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import net.zetetic.database.sqlcipher.SQLiteConstraintException;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.SqlUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment;
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion;
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference;
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement;
import org.thoughtcrime.securesms.backup.BackupProtos.Sticker;
import org.thoughtcrime.securesms.backup.proto.Attachment;
import org.thoughtcrime.securesms.backup.proto.Avatar;
import org.thoughtcrime.securesms.backup.proto.BackupFrame;
import org.thoughtcrime.securesms.backup.proto.DatabaseVersion;
import org.thoughtcrime.securesms.backup.proto.KeyValue;
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
import org.thoughtcrime.securesms.backup.proto.Sticker;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.EmojiSearchTable;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.SearchTable;
import org.thoughtcrime.securesms.database.StickerTable;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -42,29 +45,24 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.stream.Collectors;
public class FullBackupImporter extends FullBackupBase {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(FullBackupImporter.class);
private static final String[] TABLES_TO_DROP_FIRST = {
"distribution_list_member",
"distribution_list",
"message_send_log_recipients",
"msl_recipient",
"msl_message",
"reaction",
"notification_profile_schedule",
"notification_profile_allowed_members",
"story_sends"
};
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
throws IOException
@@ -91,17 +89,17 @@ public class FullBackupImporter extends FullBackupBase {
BackupFrame frame;
while (!(frame = inputStream.readFrame()).getEnd()) {
while ((frame = inputStream.readFrame()).end != Boolean.TRUE) {
if (count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count, 0));
count++;
if (frame.hasVersion()) processVersion(db, frame.getVersion());
else if (frame.hasStatement()) processStatement(db, frame.getStatement());
else if (frame.hasPreference()) processPreference(context, frame.getPreference());
else if (frame.hasAttachment()) processAttachment(context, attachmentSecret, db, frame.getAttachment(), inputStream);
else if (frame.hasSticker()) processSticker(context, attachmentSecret, db, frame.getSticker(), inputStream);
else if (frame.hasAvatar()) processAvatar(context, db, frame.getAvatar(), inputStream);
else if (frame.hasKeyValue()) processKeyValue(frame.getKeyValue());
if (frame.version != null) processVersion(db, frame.version);
else if (frame.statement != null) tryProcessStatement(db, frame.statement);
else if (frame.preference != null) processPreference(context, frame.preference);
else if (frame.attachment != null) processAttachment(context, attachmentSecret, db, frame.attachment, inputStream);
else if (frame.sticker != null) processSticker(context, attachmentSecret, db, frame.sticker, inputStream);
else if (frame.avatar != null) processAvatar(context, db, frame.avatar, inputStream);
else if (frame.keyValue != null) processKeyValue(frame.keyValue);
else count--;
}
@@ -124,117 +122,151 @@ public class FullBackupImporter extends FullBackupBase {
}
private static void processVersion(@NonNull SQLiteDatabase db, DatabaseVersion version) throws IOException {
if (version.getVersion() > db.getVersion()) {
throw new DatabaseDowngradeException(db.getVersion(), version.getVersion());
if (version.version == null || version.version > db.getVersion()) {
throw new DatabaseDowngradeException(db.getVersion(), version.version != null ? version.version : -1);
}
db.setVersion(version.getVersion());
db.setVersion(version.version);
}
private static void tryProcessStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
try {
processStatement(db, statement);
} catch (SQLiteConstraintException e) {
String tableName = "?";
String statementString = statement.statement;
if (statementString != null && statementString.startsWith("INSERT INTO ")) {
int nameStart = "INSERT INTO ".length();
int nameEnd = statementString.indexOf(" ", "INSERT INTO ".length());
if (nameStart < statementString.length() && nameEnd > nameStart) {
tableName = statementString.substring(nameStart, nameEnd);
}
}
if (tableName.startsWith("msl_")) {
Log.w(TAG, "Constraint failed when inserting into " + tableName + ". Ignoring.");
} else {
Log.w(TAG, "Constraint failed when inserting into " + tableName + ". Throwing!");
throw e;
}
}
}
private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
boolean isForSmsFtsSecretTable = statement.getStatement().contains(SearchDatabase.SMS_FTS_TABLE_NAME + "_");
boolean isForMmsFtsSecretTable = statement.getStatement().contains(SearchDatabase.MMS_FTS_TABLE_NAME + "_");
boolean isForEmojiSecretTable = statement.getStatement().contains(EmojiSearchDatabase.TABLE_NAME + "_");
boolean isForSqliteSecretTable = statement.getStatement().toLowerCase().startsWith("create table sqlite_");
if (statement.statement == null) {
Log.w(TAG, "Null statement!");
return;
}
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForEmojiSecretTable || isForSqliteSecretTable) {
Log.i(TAG, "Ignoring import for statement: " + statement.getStatement());
boolean isForMmsFtsSecretTable = statement.statement.contains(SearchTable.FTS_TABLE_NAME + "_");
boolean isForEmojiSecretTable = statement.statement.contains(EmojiSearchTable.TABLE_NAME + "_");
boolean isForSqliteSecretTable = statement.statement.toLowerCase().startsWith("create table sqlite_");
if (isForMmsFtsSecretTable || isForEmojiSecretTable || isForSqliteSecretTable) {
Log.i(TAG, "Ignoring import for statement: " + statement.statement);
return;
}
List<Object> parameters = new LinkedList<>();
for (SqlStatement.SqlParameter parameter : statement.getParametersList()) {
if (parameter.hasStringParamter()) parameters.add(parameter.getStringParamter());
else if (parameter.hasDoubleParameter()) parameters.add(parameter.getDoubleParameter());
else if (parameter.hasIntegerParameter()) parameters.add(parameter.getIntegerParameter());
else if (parameter.hasBlobParameter()) parameters.add(parameter.getBlobParameter().toByteArray());
else if (parameter.hasNullparameter()) parameters.add(null);
for (SqlStatement.SqlParameter parameter : statement.parameters) {
if (parameter.stringParamter != null) parameters.add(parameter.stringParamter);
else if (parameter.doubleParameter != null) parameters.add(parameter.doubleParameter);
else if (parameter.integerParameter != null) parameters.add(parameter.integerParameter);
else if (parameter.blobParameter != null) parameters.add(parameter.blobParameter.toByteArray());
else if (parameter.nullparameter != null) parameters.add(null);
}
if (parameters.size() > 0) db.execSQL(statement.getStatement(), parameters.toArray());
else db.execSQL(statement.getStatement());
if (parameters.size() > 0) db.execSQL(statement.statement, parameters.toArray());
else db.execSQL(statement.statement);
}
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
throws IOException
{
File dataFile = AttachmentDatabase.newFile(context);
File dataFile = AttachmentTable.newFile(context);
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
ContentValues contentValues = new ContentValues();
try {
inputStream.readAttachmentTo(output.second, attachment.getLength());
inputStream.readAttachmentTo(output.second, attachment.length);
contentValues.put(AttachmentDatabase.DATA, dataFile.getAbsolutePath());
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first);
contentValues.put(AttachmentTable.DATA, dataFile.getAbsolutePath());
contentValues.put(AttachmentTable.DATA_RANDOM, output.first);
} catch (BackupRecordInputStream.BadMacException e) {
Log.w(TAG, "Bad MAC for attachment " + attachment.getAttachmentId() + "! Can't restore it.", e);
Log.w(TAG, "Bad MAC for attachment " + attachment.attachmentId + "! Can't restore it.", e);
dataFile.delete();
contentValues.put(AttachmentDatabase.DATA, (String) null);
contentValues.put(AttachmentDatabase.DATA_RANDOM, (String) null);
contentValues.put(AttachmentTable.DATA, (String) null);
contentValues.put(AttachmentTable.DATA_RANDOM, (String) null);
}
db.update(AttachmentDatabase.TABLE_NAME, contentValues,
AttachmentDatabase.ROW_ID + " = ? AND " + AttachmentDatabase.UNIQUE_ID + " = ?",
new String[] {String.valueOf(attachment.getRowId()), String.valueOf(attachment.getAttachmentId())});
db.update(AttachmentTable.TABLE_NAME, contentValues,
AttachmentTable.ROW_ID + " = ? AND " + AttachmentTable.UNIQUE_ID + " = ?",
new String[] {String.valueOf(attachment.rowId), String.valueOf(attachment.attachmentId)});
}
private static void processSticker(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Sticker sticker, BackupRecordInputStream inputStream)
throws IOException
{
File stickerDirectory = context.getDir(StickerDatabase.DIRECTORY, Context.MODE_PRIVATE);
File stickerDirectory = context.getDir(StickerTable.DIRECTORY, Context.MODE_PRIVATE);
File dataFile = File.createTempFile("sticker", ".mms", stickerDirectory);
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
inputStream.readAttachmentTo(output.second, sticker.getLength());
inputStream.readAttachmentTo(output.second, sticker.length);
ContentValues contentValues = new ContentValues();
contentValues.put(StickerDatabase.FILE_PATH, dataFile.getAbsolutePath());
contentValues.put(StickerDatabase.FILE_LENGTH, sticker.getLength());
contentValues.put(StickerDatabase.FILE_RANDOM, output.first);
contentValues.put(StickerTable.FILE_PATH, dataFile.getAbsolutePath());
contentValues.put(StickerTable.FILE_LENGTH, sticker.length);
contentValues.put(StickerTable.FILE_RANDOM, output.first);
db.update(StickerDatabase.TABLE_NAME, contentValues,
StickerDatabase._ID + " = ?",
new String[] {String.valueOf(sticker.getRowId())});
db.update(StickerTable.TABLE_NAME, contentValues,
StickerTable._ID + " = ?",
new String[] {String.valueOf(sticker.rowId)});
}
private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
if (avatar.hasRecipientId()) {
RecipientId recipientId = RecipientId.from(avatar.getRecipientId());
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId, false), avatar.getLength());
private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
if (avatar.recipientId != null) {
RecipientId recipientId = RecipientId.from(avatar.recipientId);
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId, false), avatar.length);
} else {
if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) {
if (avatar.name != null && SqlUtil.tableExists(db, "recipient_preferences")) {
Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later.");
db.execSQL("UPDATE recipient_preferences SET signal_profile_avatar = NULL WHERE recipient_ids = ?", new String[] { avatar.getName() });
} else if (avatar.hasName() && SqlUtil.tableExists(db, "recipient")) {
db.execSQL("UPDATE recipient_preferences SET signal_profile_avatar = NULL WHERE recipient_ids = ?", new String[] { avatar.name });
} else if (avatar.name != null && SqlUtil.tableExists(db, "recipient")) {
Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar so it can be fetched later.");
db.execSQL("UPDATE recipient SET signal_profile_avatar = NULL WHERE phone = ?", new String[] { avatar.getName() });
db.execSQL("UPDATE recipient SET signal_profile_avatar = NULL WHERE phone = ?", new String[] { avatar.name });
} else {
Log.w(TAG, "Avatar is missing a recipientId. Skipping avatar restore.");
}
inputStream.readAttachmentTo(new ByteArrayOutputStream(), avatar.getLength());
inputStream.readAttachmentTo(new ByteArrayOutputStream(), avatar.length);
}
}
private static void processKeyValue(BackupProtos.KeyValue keyValue) {
private static void processKeyValue(KeyValue keyValue) {
KeyValueDataSet dataSet = new KeyValueDataSet();
if (keyValue.hasBlobValue()) {
dataSet.putBlob(keyValue.getKey(), keyValue.getBlobValue().toByteArray());
} else if (keyValue.hasBooleanValue()) {
dataSet.putBoolean(keyValue.getKey(), keyValue.getBooleanValue());
} else if (keyValue.hasFloatValue()) {
dataSet.putFloat(keyValue.getKey(), keyValue.getFloatValue());
} else if (keyValue.hasIntegerValue()) {
dataSet.putInteger(keyValue.getKey(), keyValue.getIntegerValue());
} else if (keyValue.hasLongValue()) {
dataSet.putLong(keyValue.getKey(), keyValue.getLongValue());
} else if (keyValue.hasStringValue()) {
dataSet.putString(keyValue.getKey(), keyValue.getStringValue());
if (keyValue.key == null) {
Log.w(TAG, "Null preference key!");
return;
}
if (keyValue.blobValue != null) {
dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray());
} else if (keyValue.booleanValue != null) {
dataSet.putBoolean(keyValue.key, keyValue.booleanValue);
} else if (keyValue.floatValue != null) {
dataSet.putFloat(keyValue.key, keyValue.floatValue);
} else if (keyValue.integerValue != null) {
dataSet.putInteger(keyValue.key, keyValue.integerValue);
} else if (keyValue.longValue != null) {
dataSet.putLong(keyValue.key, keyValue.longValue);
} else if (keyValue.stringValue != null) {
dataSet.putString(keyValue.key, keyValue.stringValue);
} else {
Log.i(TAG, "Unknown KeyValue backup value, skipping");
return;
@@ -245,44 +277,92 @@ public class FullBackupImporter extends FullBackupBase {
@SuppressLint("ApplySharedPref")
private static void processPreference(@NonNull Context context, SharedPreference preference) {
SharedPreferences preferences = context.getSharedPreferences(preference.getFile(), 0);
SharedPreferences preferences = context.getSharedPreferences(preference.file_, 0);
// Identity keys were moved from shared prefs into SignalStore. Need to handle importing backups made before the migration.
if ("SecureSMS-Preferences".equals(preference.getFile())) {
if ("pref_identity_public_v3".equals(preference.getKey()) && preference.hasValue()) {
SignalStore.account().restoreLegacyIdentityPublicKeyFromBackup(preference.getValue());
} else if ("pref_identity_private_v3".equals(preference.getKey()) && preference.hasValue()) {
SignalStore.account().restoreLegacyIdentityPrivateKeyFromBackup(preference.getValue());
if ("SecureSMS-Preferences".equals(preference.file_)) {
if ("pref_identity_public_v3".equals(preference.key) && preference.value_ != null) {
SignalStore.account().restoreLegacyIdentityPublicKeyFromBackup(preference.value_);
} else if ("pref_identity_private_v3".equals(preference.key) && preference.value_ != null) {
SignalStore.account().restoreLegacyIdentityPrivateKeyFromBackup(preference.value_);
}
return;
}
if (preference.hasValue()) {
preferences.edit().putString(preference.getKey(), preference.getValue()).commit();
} else if (preference.hasBooleanValue()) {
preferences.edit().putBoolean(preference.getKey(), preference.getBooleanValue()).commit();
} else if (preference.hasIsStringSetValue() && preference.getIsStringSetValue()) {
preferences.edit().putStringSet(preference.getKey(), new HashSet<>(preference.getStringSetValueList())).commit();
if (preference.value_ != null) {
preferences.edit().putString(preference.key, preference.value_).commit();
} else if (preference.booleanValue != null) {
preferences.edit().putBoolean(preference.key, preference.booleanValue).commit();
} else if (preference.isStringSetValue == Boolean.TRUE) {
preferences.edit().putStringSet(preference.key, new HashSet<>(preference.stringSetValue)).commit();
}
}
private static void dropAllTables(@NonNull SQLiteDatabase db) {
for (String name : TABLES_TO_DROP_FIRST) {
db.execSQL("DROP TABLE IF EXISTS " + name);
for (String trigger : SqlUtil.getAllTriggers(db)) {
Log.i(TAG, "Dropping trigger: " + trigger);
db.execSQL("DROP TRIGGER IF EXISTS " + trigger);
}
for (String table : getTablesToDropInOrder(db)) {
Log.i(TAG, "Dropping table: " + table);
db.execSQL("DROP TABLE IF EXISTS " + table);
}
}
/**
* Returns the list of tables we should drop, in the order they should be dropped in.
* The order is chosen to ensure we won't violate any foreign key constraints when we import them.
*/
private static List<String> getTablesToDropInOrder(@NonNull SQLiteDatabase input) {
List<String> tables = SqlUtil.getAllTables(input)
.stream()
.filter(table -> !table.startsWith("sqlite_"))
.sorted()
.collect(Collectors.toList());
Map<String, Set<String>> dependsOn = new LinkedHashMap<>();
for (String table : tables) {
dependsOn.put(table, SqlUtil.getForeignKeyDependencies(input, table));
}
try (Cursor cursor = db.rawQuery("SELECT name, type FROM sqlite_master", null)) {
while (cursor != null && cursor.moveToNext()) {
String name = cursor.getString(0);
String type = cursor.getString(1);
for (String table : tables) {
Set<String> dependsOnTable = dependsOn.keySet().stream().filter(t -> dependsOn.get(t).contains(table)).collect(Collectors.toSet());
Log.i(TAG, "Tables that depend on " + table + ": " + dependsOnTable);
}
if ("table".equals(type) && !name.startsWith("sqlite_")) {
Log.i(TAG, "Dropping table: " + name);
db.execSQL("DROP TABLE IF EXISTS " + name);
}
return computeTableDropOrder(dependsOn);
}
@VisibleForTesting
static List<String> computeTableDropOrder(@NonNull Map<String, Set<String>> dependsOn) {
List<String> rootNodes = dependsOn.keySet()
.stream()
.filter(table -> {
boolean nothingDependsOnIt = dependsOn.values().stream().noneMatch(it -> it.contains(table));
return nothingDependsOnIt;
})
.sorted()
.collect(Collectors.toList());
LinkedHashSet<String> dropOrder = new LinkedHashSet<>();
Queue<String> processOrder = new LinkedList<>(rootNodes);
while (!processOrder.isEmpty()) {
String head = processOrder.remove();
dropOrder.remove(head);
dropOrder.add(head);
Set<String> dependencies = dependsOn.get(head);
if (dependencies != null) {
processOrder.addAll(dependencies);
}
}
return new ArrayList<>(dropOrder);
}
public static class DatabaseDowngradeException extends IOException {

View File

@@ -6,7 +6,7 @@ import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
@@ -40,13 +40,13 @@ class BadgeRepository(context: Context) {
): List<Badge> {
Log.d(TAG, "[setVisibilityForAllBadgesSync] Setting badge visibility...", true)
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
val recipientTable: RecipientTable = SignalDatabase.recipients
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
Log.d(TAG, "[setVisibilityForAllBadgesSync] Uploading profile...", true)
ProfileUtil.uploadProfileWithBadges(context, badges)
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile)
recipientDatabase.markNeedsSync(Recipient.self().id)
recipientTable.markNeedsSync(Recipient.self().id)
Log.d(TAG, "[setVisibilityForAllBadgesSync] Requesting data change sync...", true)
StorageSyncHelper.scheduleSyncForDataChange()

View File

@@ -30,14 +30,15 @@ object ExpiredGiftSheetConfiguration {
private fun DSLConfiguration.expiredSheet(onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
textPref(
title = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__your_gift_badge_has_expired,
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
stringId = R.string.ExpiredGiftSheetConfiguration__your_badge_has_expired,
DSLSettingsText.CenterModifier,
DSLSettingsText.TitleLargeModifier
)
)
textPref(
title = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__your_gift_badge_has_expired_and_is,
stringId = R.string.ExpiredGiftSheetConfiguration__your_badge_has_expired_and_is,
DSLSettingsText.CenterModifier
)
)

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.badges.gifts.Gifts.formatExpiry
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Displays a gift badge sent to or received from a user, and allows the user to
@@ -49,8 +50,7 @@ class GiftMessageView @JvmOverloads constructor(
}
}
fun setGiftBadge(glideRequests: GlideRequests, giftBadge: GiftBadge, isOutgoing: Boolean, callback: Callback) {
titleView.setText(R.string.GiftMessageView__gift_badge)
fun setGiftBadge(glideRequests: GlideRequests, giftBadge: GiftBadge, isOutgoing: Boolean, callback: Callback, recipient: Recipient) {
descriptionView.text = giftBadge.formatExpiry(context)
actionView.icon = null
actionView.setOnClickListener { callback.onViewGiftBadgeClicked() }
@@ -58,7 +58,9 @@ class GiftMessageView @JvmOverloads constructor(
if (isOutgoing) {
actionView.setText(R.string.GiftMessageView__view)
titleView.text = context.getString(R.string.GiftMessageView__donation_to_s, recipient.getDisplayName(context))
} else {
titleView.text = context.getString(R.string.GiftMessageView__s_donated_to_signal_on, recipient.getShortDisplayName(context))
when (giftBadge.redemptionState) {
GiftBadge.RedemptionState.REDEEMED -> {
stopAnimationIfNeeded()

View File

@@ -4,11 +4,8 @@ import android.content.Context
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
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 org.thoughtcrime.securesms.util.Base64
import java.lang.Integer.min
@@ -32,23 +29,14 @@ object Gifts {
giftBadge: GiftBadge,
sentTimestamp: Long,
expiresIn: Long
): OutgoingMediaMessage {
return OutgoingSecureMediaMessage(
recipient,
Base64.encodeBytes(giftBadge.toByteArray()),
listOf(),
sentTimestamp,
ThreadDatabase.DistributionTypes.CONVERSATION,
expiresIn,
false,
StoryType.NONE,
null,
false,
null,
listOf(),
listOf(),
listOf(),
giftBadge
): OutgoingMessage {
return OutgoingMessage(
recipient = recipient,
body = Base64.encodeBytes(giftBadge.toByteArray()),
isSecure = true,
sentTimeMillis = sentTimestamp,
expiresIn = expiresIn,
giftBadge = giftBadge
)
}

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