Compare commits

...

370 Commits

Author SHA1 Message Date
Greyson Parrelli
56392b87f7 Bump version to 4.69.2 2020-08-18 19:22:42 -04:00
Greyson Parrelli
1b1a4aeb38 Updated language translations. 2020-08-18 19:22:18 -04:00
Greyson Parrelli
16147e0c08 Ensure link preview fetches are canceled on message send. 2020-08-18 18:34:18 -04:00
Cody Henthorne
139317cf1b Improve various aspects of mentions. 2020-08-18 18:13:45 -04:00
Cody Henthorne
72b94127fb Stop muted threads from triggering full notification updates. 2020-08-18 14:15:55 -04:00
Alan Evans
1f1fc94d22 Fix flakey robolectric test. 2020-08-18 11:57:35 -03:00
Greyson Parrelli
a574fe026c Bump version to 4.69.1 2020-08-17 12:04:41 -04:00
Greyson Parrelli
aa82083d30 Updated language translations. 2020-08-17 12:04:41 -04:00
Greyson Parrelli
08d5df70c2 Don't show the link preview megaphone if previously disabled. 2020-08-17 12:04:41 -04:00
Greyson Parrelli
29b8fa5897 Keep pinned chats at the top of the 'recent' chat section. 2020-08-17 11:12:10 -04:00
Alex Hart
e96faf31d4 Fix browser opening on long-press of debug log links. 2020-08-17 11:54:41 -03:00
Greyson Parrelli
157a73aa99 Fix title of conversation pin menu item. 2020-08-17 10:37:17 -04:00
Greyson Parrelli
bdd298c8a0 Prevent swipe actions on the 'Pinned' header. 2020-08-17 10:31:28 -04:00
Greyson Parrelli
3f7dd21186 Do not attempt to create link previews for .onion links. 2020-08-17 10:27:30 -04:00
Greyson Parrelli
086b708cf7 Fix NPE when double-tapping the conversation pinning icon. 2020-08-17 10:07:58 -04:00
Alan Evans
57e0e57f48 Fix NPE when link preview image cannot be decoded. 2020-08-15 10:10:15 -03:00
Greyson Parrelli
4b7efbfdc0 Bump version to 4.69.0 2020-08-14 15:54:06 -04:00
Greyson Parrelli
7dc2653042 Updated language translations. 2020-08-14 15:54:06 -04:00
Cody Henthorne
e428453835 Fix conversation list bug with pinned chats.
Co-authored-by: Alex Hart <alex@signal.org>
2020-08-14 15:54:06 -04:00
Greyson Parrelli
f84c8229de Revert "Replace a call to a deprecated method to update context with the new one."
This reverts commit 5f0d384c9e.

Introduced a bug where the system theme wasn't changing until app
restart.
2020-08-14 15:54:06 -04:00
Alex Hart
a73427d68d Fix issues with conversation list position. 2020-08-14 15:54:05 -04:00
Alan Evans
e4456bb236 Handle GV2 addresses. 2020-08-14 15:54:05 -04:00
Alex Hart
06eadd0c15 Add mentions unread counter. 2020-08-14 15:54:05 -04:00
Alan Evans
3c90dfa660 Ensure a GV2 update message mentioning you as a new member is first in the list. 2020-08-14 15:54:05 -04:00
Greyson Parrelli
ace1b8ee71 Update link preview settings and add some UI polish. 2020-08-14 15:54:05 -04:00
Cody Henthorne
676356e800 Add Mentions Megaphone. 2020-08-14 15:54:05 -04:00
Greyson Parrelli
f732e54c22 Update group size flag. 2020-08-14 15:54:05 -04:00
Cody Henthorne
cdc2e74f68 Stop conversations without meaningful messages from showing in list. 2020-08-14 15:54:05 -04:00
Cody Henthorne
724f3e872b Update Mention UI/UX to match latest designs. 2020-08-14 15:54:05 -04:00
Alex Hart
d63e5165eb Add ability to pin up to 4 conversations. 2020-08-14 15:54:05 -04:00
Cody Henthorne
9892c4392e Fix janky avatar preview transition for notched devices. 2020-08-14 15:54:05 -04:00
Cody Henthorne
5ced1a775c Fix bug where SN change dialog appeared unnecessarily. 2020-08-14 15:54:05 -04:00
Cody Henthorne
761de1318e Update mention data during recipient merge. 2020-08-14 15:54:05 -04:00
Cody Henthorne
02508512d5 Fix incorrect snippet generation by ignoring profile name change messages. 2020-08-14 15:54:05 -04:00
Greyson Parrelli
6e6105af05 Open up link previews to work with all sites. 2020-08-14 15:54:05 -04:00
Jared Andrews
d569419e13 Fixes conversation overflow menu items not being tappable.
Fixes #9908
2020-08-13 19:47:46 -04:00
Greyson Parrelli
93f1641803 Bump version to 4.68.8 2020-08-10 21:13:19 -04:00
Greyson Parrelli
ff52bf93fa Make the CDS flag remote capable. 2020-08-10 13:27:11 -04:00
Greyson Parrelli
a039275a0c Bump version to 4.68.7 2020-08-10 11:40:37 -04:00
Greyson Parrelli
a98d10104d Updated language translations. 2020-08-10 11:39:30 -04:00
Alan Evans
8924bc59b1 Hide legacy group warning when GV2 create feature flag is off or MMS is forced.
Fixes #9913
2020-08-08 17:43:07 -03:00
Greyson Parrelli
eefe60a9c9 Bump version to 4.68.6 2020-08-07 19:37:05 -04:00
Greyson Parrelli
fe1cb3d904 Updated language translations. 2020-08-07 19:36:26 -04:00
Greyson Parrelli
0448278a78 Include a recipient in sent transcripts when possible. 2020-08-07 19:20:35 -04:00
Greyson Parrelli
99c0c2ff4c Fix crash when opening debuglogs during registration. 2020-08-07 19:20:35 -04:00
Greyson Parrelli
b369b734ca Improve storage service insert recovery. 2020-08-07 19:20:35 -04:00
Greyson Parrelli
57150a20fd Make verificationV2 a separate flag. 2020-08-07 19:20:35 -04:00
Cody Henthorne
1634d7d531 Show mention picker immediately after @ entered. 2020-08-07 15:27:15 -04:00
Cody Henthorne
d563de4207 Add mention detection to search flows. 2020-08-07 15:18:40 -04:00
Greyson Parrelli
5cd4b82ed0 Bump version to 4.68.5 2020-08-06 21:03:31 -04:00
Greyson Parrelli
5f728d348c Updated language translations. 2020-08-06 21:02:22 -04:00
Greyson Parrelli
596c4b6e40 Don't include inactive groups when listing groups in common. 2020-08-06 20:57:50 -04:00
Alex Hart
36d1e7c44a Disable Contact Join Notification via Action. 2020-08-06 20:57:50 -04:00
Alan Evans
25c17082f2 Share a common groups v2 capacity flag across clients. 2020-08-06 20:57:50 -04:00
Alan Evans
810ccf8e94 Improve GV2 Invitation revoke experience. 2020-08-06 20:57:50 -04:00
Alex Hart
c8ed0b19f0 Do not update thread on profile name change. 2020-08-06 20:57:50 -04:00
Alan Evans
9e09444c65 Increment the Groups V2 feature flags version. 2020-08-06 20:57:50 -04:00
Greyson Parrelli
5923fa0cd5 Block sends on CDS lookups. 2020-08-06 20:57:50 -04:00
Cody Henthorne
b2d4c5d14b Add mentions for v2 group chats. 2020-08-06 20:57:50 -04:00
Alex Hart
0bb9c1d650 Add light and dark spinner lotties with correct coloring. 2020-08-06 20:57:50 -04:00
Alan Evans
fbfa3abffd Skip delete actions where the removed member/pending member is not in the group. 2020-08-06 20:57:50 -04:00
Alan Evans
b5656aa5dd Exclude non-translatable multiline blocks. 2020-08-06 20:57:50 -04:00
Alan Evans
d53fd6a109 Change invite cancel to invite revoke. 2020-08-06 20:57:50 -04:00
Alan Evans
b0650b926b Fix pending member group edit rights. 2020-08-06 20:57:50 -04:00
Alan Evans
845f6a0a93 Notify user during group create of members that do not support GV2. 2020-08-06 20:57:50 -04:00
Alex Hart
d8daa83c79 Remove autoLink from conversation update items. 2020-08-06 20:57:50 -04:00
Alex Hart
7bb0199e83 Change additional groups copy to match iOS. 2020-08-06 20:57:50 -04:00
Alex Hart
f014dadf06 Adjust Zoom levels and transition duration. 2020-08-06 20:57:50 -04:00
Alex Hart
393e54ce91 Update how we mark messages as read. 2020-08-06 20:57:50 -04:00
Alan Evans
fdf4ad9543 Remove the GV2 "anyone" access level. 2020-08-06 20:57:50 -04:00
Fumiaki Yoshimatsu
5f0d384c9e Replace a call to a deprecated method to update context with the new one.
Fixes #9736
2020-08-06 20:57:50 -04:00
Christian Ascheberg
4271700046 Do not collapse list to hide only one entry. 2020-08-06 20:57:50 -04:00
Niko Lockenvitz
e153b0ab78 Fix message compose hint on fullscreen.
Fixes #5294
Closes #5348
2020-08-06 20:57:50 -04:00
Alan Evans
26868ae668 Get authoritative profile keys from group changes only. 2020-08-06 20:57:50 -04:00
Greyson Parrelli
17c0364eda Ensure group avatars have V2 attachmentIds. 2020-08-06 20:57:50 -04:00
Alan Evans
b28ac7af8c Additional tests around rigid Groups V2 change application. 2020-08-06 20:57:50 -04:00
Greyson Parrelli
2dcaa21a44 Remove UuidRecipientError. 2020-08-04 19:12:25 -04:00
Greyson Parrelli
33cc8363f9 Add internal setting to see recipient details. 2020-08-04 19:12:25 -04:00
Greyson Parrelli
9b61e1c85c Show a message request for certain GV2 adds. 2020-08-04 19:12:25 -04:00
Greyson Parrelli
6f53fdc02d Clean up log statement in FcmFetchService. 2020-08-04 19:12:25 -04:00
Greyson Parrelli
6f850f5a55 Bump version to 4.68.4 2020-08-04 17:53:22 -04:00
Greyson Parrelli
a482a4b1f4 Updated language translations. 2020-08-04 17:46:11 -04:00
Greyson Parrelli
3664e6f96d Fix processing of unsupported messages. 2020-08-04 17:37:25 -04:00
Greyson Parrelli
dda8808173 Bump version to 4.68.3 2020-08-03 12:30:51 -04:00
Greyson Parrelli
63a24c23cc Updated language translations. 2020-08-03 12:29:52 -04:00
Greyson Parrelli
1ec3a72f79 Fix issue with thread summaries being updated after message deletion.
Fixes #9902
2020-08-03 10:36:02 -04:00
Greyson Parrelli
566285ec0e Fix crash in MMS group creation.
Fixes #9901
2020-08-03 10:03:45 -04:00
Greyson Parrelli
d5ba82338d Fix issue with text rendering in search results. 2020-08-03 09:47:27 -04:00
Greyson Parrelli
cbecd2a2fc Bump version to 4.68.2 2020-07-31 16:47:55 -04:00
Greyson Parrelli
3772dd40ac Updated language translations. 2020-07-31 16:46:01 -04:00
Alex Hart
f69a0f0261 Refine reaction details fragment. 2020-07-31 16:49:52 -03:00
Alex Hart
cb323ffb84 Fix reaction overlay toolbar and status bar. 2020-07-31 15:51:41 -03:00
Alex Hart
0db73e71a0 Remove sticky header on list reinitailization.
When we forward a message or share into the app, it is possible that we are going to reuse the same activity. In this case, when the adapter was reinitialized, we were just adding a new ItemDecoration every time.

This fix checks if we've already added one and removes it if necessary, just like the last seen decorator.
2020-07-31 14:26:31 -03:00
Alex Hart
eeb0c838db Fix masking when attachment keyboard is visible. 2020-07-31 11:34:46 -03:00
Greyson Parrelli
dc48ee5aed Bump version to 4.68.1 2020-07-30 23:32:20 -04:00
Greyson Parrelli
c0acfa57a9 Updated language translations. 2020-07-30 23:32:19 -04:00
Greyson Parrelli
3e166ef927 Fix issue where group updates were mis-rendered. 2020-07-30 23:32:19 -04:00
Greyson Parrelli
4942d83de5 Properly render reset session update messages. 2020-07-30 23:32:19 -04:00
Alex Hart
4c30b39e71 Add section to recent reactions page listing emoji already applied to message. 2020-07-30 23:32:19 -04:00
Alex Hart
e55f4fe6b6 Save preference on emoji send. 2020-07-30 22:26:59 -04:00
Greyson Parrelli
aff74cffa0 Fix crash with UnknownSenderView.
The listener was being called on a background thread, but it was doing
UI work.
2020-07-30 13:31:51 -04:00
Alex Hart
8b29bb8664 Fix info icon in light mode. 2020-07-30 10:48:45 -03:00
Greyson Parrelli
3cee57b6c2 Bump version to 4.68.0 2020-07-29 23:54:46 -04:00
Greyson Parrelli
857f4a4fc8 Updated language translations. 2020-07-29 23:54:09 -04:00
Jim Gustafson
a942293a74 RingRTC v2.4.0 Release Integration.
Co-authored-by: Peter Thatcher <peter@signal.org>
2020-07-29 23:43:06 -04:00
Greyson Parrelli
550b121990 Prevent UUID-only contacts from being added to GV1 groups. 2020-07-29 23:43:06 -04:00
Alex Hart
cc84901a49 Add dropshadow to emoji variation popup. 2020-07-29 23:43:06 -04:00
Alex Hart
9d3764c5d9 Reactions UX polish. 2020-07-29 23:43:06 -04:00
Greyson Parrelli
0950235ccd Fix typo in RemappedRecords. 2020-07-29 23:19:21 -04:00
Greyson Parrelli
8ed7fc894e Improve handling of partially bi-directional text. 2020-07-29 23:19:21 -04:00
Greyson Parrelli
e504ffa225 Clean up conversation list data loading sequence.
- The Paging library was giving us empty paged lists when loading was
invalidated, but only *sometimes*. This library, man. Fixed it by
ignoring invalid lists, which you'd think the library would do for us...
- Noticed we were doing a ton of list refreshes because of how we were
listening to archive count. Switched from combine to switchMap.
- Noticed that we could become double-subscribed to LiveDatas in the
ConversationListFragment if you went to archived. Fixed by observing on
the fragment's view lifecycle.

Fixes #9803
2020-07-29 23:19:21 -04:00
Cody Henthorne
9c63b37bb4 Refactor use of MessageRecord to increase flexibility of ConversationAdapter. 2020-07-29 23:19:21 -04:00
Greyson Parrelli
5c110ca359 Remove UUIDs from GV1 membership lists. 2020-07-29 23:19:21 -04:00
Cody Henthorne
1ab61beeb9 Add initial Mentions UI/UX for picker and compose edit. 2020-07-28 15:20:20 -04:00
Alan Evans
8e45a546c9 Fix NPE on Group multi-invite. 2020-07-28 15:20:20 -04:00
Alan Evans
745a7f76ea Change position of GroupsV2 leave update message. 2020-07-28 15:20:20 -04:00
Alan Evans
8cb9ab3204 Fetch newly found profiles on Groups V2 inline. 2020-07-28 15:20:20 -04:00
Alan Evans
12533d1414 Ensure profile key is up to date on Group V2 conversation open. 2020-07-28 15:20:20 -04:00
Alan Evans
bd1c164d57 Live group update messages on conversation list and conversation. 2020-07-28 15:20:20 -04:00
Greyson Parrelli
7446c2096d Don't ellipsize multi-line text in conversation list.
Instead, basically convert newlines to spaces.
2020-07-28 15:19:52 -04:00
Greyson Parrelli
8ce5c4b885 Cleanup naming of RecipientDatabase GLOB search. 2020-07-28 15:19:52 -04:00
Alan Evans
ab76112f5f Prevent leading and trailing whitespace in group names. 2020-07-28 15:19:52 -04:00
Alan Evans
9c54e39eae Adjust scope of Groups V2 feature flag. 2020-07-28 15:19:52 -04:00
Greyson Parrelli
61eab44474 Bump version to 4.67.3 2020-07-27 18:04:05 -04:00
Greyson Parrelli
f6285ec710 Updated language translations. 2020-07-27 18:02:31 -04:00
Alex Hart
ed878ec4b4 Add more generic SMS verification code pattern. 2020-07-27 17:57:56 -04:00
Greyson Parrelli
e38d41d67a Reduce the number of cats in giphy sticker search results. 2020-07-27 15:25:26 -04:00
Greyson Parrelli
3d237d72bd Fix issue where feature flag fetches weren't limited. 2020-07-27 15:25:01 -04:00
Cody Henthorne
8044d2390c Fix bug causing profile updates to unarchive threads. 2020-07-27 13:32:38 -04:00
Greyson Parrelli
6b82e6b5ac Bump version to 4.67.2 2020-07-24 14:31:06 -04:00
Greyson Parrelli
842e6a93e2 Updated language translations. 2020-07-24 14:31:06 -04:00
Alan Evans
f140f054e5 Ignore typing indicators from blocked group members. 2020-07-24 14:31:06 -04:00
Greyson Parrelli
5cd4726e23 Do not show profile name changes if names are visually identical.
Fixes #9860
2020-07-24 14:30:58 -04:00
Greyson Parrelli
bccc58d693 Bump version to 4.67.1 2020-07-22 22:58:21 -04:00
Greyson Parrelli
e25f1c1481 Updated language translations. 2020-07-22 22:58:21 -04:00
Greyson Parrelli
fc4e690996 Revert "Ensure GV1 length is exactly the length expected."
This reverts commit 8e962bf992.
2020-07-22 22:58:21 -04:00
Greyson Parrelli
dadb2f9d37 Allow auto-downloads from groups you've accepted. 2020-07-22 22:58:21 -04:00
Greyson Parrelli
5bf15b0587 Fix casing issues with non-ASCII characters in contact search.
SQLite's case-related stuff is ASCII-only. That means that even though LIKE is supposed to be case-insensitive, it fails when used on non-ASCII characters.

There appears to be no relief in SQLite itself, so I swapped our contact search to use GLOB instead of LIKE and wrote a little thing to convert query strings into a case-insensitive unicode-compatible patterns. Didn't see any noticeable performance difference.
2020-07-22 22:58:21 -04:00
Cody Henthorne
5f9c0c3204 Fix bug with skipping resend message on safety number change. 2020-07-22 22:58:21 -04:00
Alan Evans
dfa4f0c309 Fix group change failure reason display logic. 2020-07-22 22:58:21 -04:00
Greyson Parrelli
f0063b4b0d Sync ContactRecords as whitelisted if they're a system contact. 2020-07-22 22:58:21 -04:00
Alan Evans
5dc51c34ea Fix recipient resolution during add to Groups V2. 2020-07-22 22:58:21 -04:00
Greyson Parrelli
5bf7a55bfa Bump version to 4.67.0 2020-07-21 16:11:45 -04:00
Greyson Parrelli
eb9ae8d5dc Updated language translations. 2020-07-21 16:11:45 -04:00
Greyson Parrelli
2a133587cc Add a flag for recipientTrust. 2020-07-21 16:11:45 -04:00
Greyson Parrelli
0e4a19c368 Improve exception stack traces in OptimizedMessageNotifier. 2020-07-21 15:31:53 -04:00
Greyson Parrelli
813c820227 Fix issue with GV1 avatars using attachmentsV3. 2020-07-21 15:31:53 -04:00
Greyson Parrelli
870cee5707 Remove uuidOnlyContacts feature flag. 2020-07-21 15:31:53 -04:00
Alan Evans
4e55d2d941 Tint pending group invites menu icon. 2020-07-21 15:31:53 -04:00
Alan Evans
8e962bf992 Ensure GV1 length is exactly the length expected. 2020-07-21 15:31:53 -04:00
Cody Henthorne
0815715f7b Enable call requests always. 2020-07-21 15:31:53 -04:00
Alan Evans
85e4697b7f Increment the Groups V2 feature flags version. 2020-07-21 15:31:53 -04:00
Alan Evans
16fdb9bf4c Make identity record list immutable. 2020-07-21 12:53:25 -03:00
Greyson Parrelli
46f3d50a54 Increment the attachmentsV3 feature flag version. 2020-07-21 10:49:19 -04:00
Alan Evans
3a38240fb2 Groups V2 group manager copy updates. 2020-07-21 11:47:11 -03:00
Greyson Parrelli
662f0b8fb6 Improve detection of websocket drained status.
Will now work when you lose and regain network. Also removes the
unnecessary InitialMessageRetriever.
2020-07-21 10:38:42 -04:00
Alan Evans
96ce42ae91 Legacy group learn more badge and info bottom sheet. 2020-07-21 06:05:16 -03:00
Alan Evans
93f587b851 For atomic Groups V2 block and leave, block after leaving group. 2020-07-21 06:04:44 -03:00
Greyson Parrelli
89a940ec81 Fix issue with contact syncing with attachmentsV3. 2020-07-20 17:57:22 -04:00
Alan Evans
a33771b15d Added progress feedback to leave and block group actions and additional group v2 error handling. 2020-07-20 15:20:56 -03:00
Greyson Parrelli
9a566e5559 Group together skin tone variations of the same reaction. 2020-07-20 10:26:39 -04:00
Greyson Parrelli
6e75d42a92 Enable skin tone selection for emoji reactions. 2020-07-20 10:26:39 -04:00
Alan Evans
575413cac9 Wait for message queue to drain before updating v2 groups. 2020-07-20 11:09:42 -03:00
Greyson Parrelli
6a9476c6d0 Fix retry issues with RotateProfileKeyJob. 2020-07-19 10:45:20 -04:00
Greyson Parrelli
5468f1705c Ensure we refresh attributes if key changes from storage service. 2020-07-19 10:45:20 -04:00
Greyson Parrelli
5ea132e712 Delay directory refresh until registration is complete. 2020-07-19 10:22:05 -04:00
Cody Henthorne
8128fcf8bc Hide compose for inactive groups. 2020-07-19 09:32:16 -04:00
Greyson Parrelli
e89655f793 Resolve newly-entered numbers before starting a conversation. 2020-07-19 09:32:16 -04:00
Cody Henthorne
2db2b068c4 Do not show typing indicators for inactive groups. 2020-07-19 09:32:16 -04:00
Alan Evans
a59e214317 Show Group V2 invited member dialog explaining invites on new group and add to group. 2020-07-19 09:32:16 -04:00
Cody Henthorne
ae2b6e4d7a Prevent last admin from leaving without selecting new admin. 2020-07-19 09:32:16 -04:00
Alan Evans
b10fc6a0b0 Support Groups v2 Change Epochs. 2020-07-19 09:32:16 -04:00
Cody Henthorne
70977e5228 Show expiration time exactly as set instead of rounding. 2020-07-19 09:32:16 -04:00
Greyson Parrelli
4482391574 Update libphonenumber to v8.12.6 2020-07-19 09:32:16 -04:00
Greyson Parrelli
bd078fc883 Handle UUID-only recipients and merging. 2020-07-19 09:32:16 -04:00
Alan Evans
644af87782 Groups V2 invite decline. 2020-07-19 09:32:16 -04:00
Greyson Parrelli
1ce36c1069 Bump version to 4.66.8 2020-07-17 17:32:33 -04:00
Greyson Parrelli
0a71005ecc Updated language translations. 2020-07-17 17:32:07 -04:00
Cody Henthorne
698618a4b3 Only show profile updates in active groups. 2020-07-17 17:32:07 -04:00
Alan Evans
f9642dd79f Reduce scrim overlap when scrolling new manage screens. 2020-07-17 17:32:07 -04:00
Cody Henthorne
85d1a3c016 Add system contact indicator to recipient bottom sheet. 2020-07-17 17:32:07 -04:00
Alan Evans
38c74c81a6 Add qa to translate task. 2020-07-17 17:32:07 -04:00
Greyson Parrelli
4c04991b70 Refresh recipient after viewing system contact details.
They might have changed the name or otherwise edited the contact, so we
want to try to keep things in sync.
2020-07-17 17:32:07 -04:00
Cody Henthorne
293a339fed Only show delete action when long pressing on profile change update. 2020-07-17 17:32:07 -04:00
Greyson Parrelli
5255a527f9 Do not show profile name changes for blocked users. 2020-07-17 17:32:07 -04:00
Cody Henthorne
9440dfb66c Do not show profile name changes on first update. 2020-07-17 09:42:13 -04:00
Alan Evans
7a019eee19 Updated language translations. 2020-07-16 16:21:02 -03:00
Greyson Parrelli
93f56a5dc8 Bump version to 4.66.7 2020-07-16 10:40:04 -04:00
Greyson Parrelli
68264228b8 Updated language translations. 2020-07-16 10:33:33 -04:00
Greyson Parrelli
66c1b8e26c Fix contact icon tint issues on older android versions. 2020-07-16 10:27:23 -04:00
Cody Henthorne
5776c048ea Do not update threads that do not exist. 2020-07-16 09:27:41 -04:00
Greyson Parrelli
76dd09bc50 Handle null profile names better. 2020-07-16 08:34:53 -04:00
Greyson Parrelli
73d18d3abd Bump version to 4.66.6 2020-07-15 17:12:37 -04:00
Greyson Parrelli
c1c9d0c8a3 Updated language translations. 2020-07-15 17:12:09 -04:00
Cody Henthorne
64420ead7c Show Profile Name Change update messages. 2020-07-15 16:15:15 -04:00
Alan Evans
6d035c6888 Allow sending of group v2 updates to inactive groups. 2020-07-15 12:31:59 -03:00
Alan Evans
833ca8cce9 Add disable GV2 creation option to internal preferences UI. 2020-07-15 12:28:47 -03:00
Ehren Kret
d02d506b13 Add force refresh of remote values to internal preferences UI. 2020-07-15 12:16:07 -03:00
Alan Evans
f306056e5d Enable lint StopShip comments. 2020-07-15 12:04:05 -03:00
Greyson Parrelli
58ec669d15 Fix quote attachmentV3 usage. 2020-07-14 19:43:17 -04:00
Greyson Parrelli
d1b61bfed3 Add indicator for system contacts. 2020-07-14 10:37:09 -04:00
Greyson Parrelli
325e0c6781 Bump version to 4.66.5 2020-07-14 10:26:15 -04:00
Greyson Parrelli
8d66cd52b5 Updated language translations. 2020-07-14 10:25:46 -04:00
Greyson Parrelli
4b9277629c Fix issue with tracking registration state. 2020-07-13 19:00:44 -04:00
Greyson Parrelli
6515a6188b Bump version to 4.66.4 2020-07-13 11:01:18 -04:00
Greyson Parrelli
8b3ca52502 Updated language translations. 2020-07-13 11:00:34 -04:00
Alan Evans
fae003e085 Do not sync group v2 recipients that we do not have the master key for. 2020-07-13 11:52:06 -03:00
Greyson Parrelli
4b961d2d8f Simplify PIN opt-out code. 2020-07-13 09:29:17 -04:00
Greyson Parrelli
e27fc512b4 Add a migration for users of the previous PIN opt-out flow. 2020-07-13 08:53:02 -04:00
Greyson Parrelli
8f0f600b6b Bump version to 4.66.3 2020-07-11 11:42:51 -04:00
Greyson Parrelli
5950610690 Updated language translations. 2020-07-11 11:42:51 -04:00
Greyson Parrelli
fce3df0c82 Update pin opt-out strings and behavior. 2020-07-11 11:42:51 -04:00
Greyson Parrelli
e2021231c6 Bump version to 4.66.2 2020-07-10 17:23:50 -04:00
Greyson Parrelli
f61dd7509e Updated language translations. 2020-07-10 17:23:50 -04:00
Greyson Parrelli
db2b64e58c Update PIN opt-out strings. 2020-07-10 17:23:50 -04:00
Alan Evans
d70999c386 Add storage force push internal option. 2020-07-10 17:23:50 -04:00
Alan Evans
eb6ecc59ab Consolidate duplicated group send job logic. 2020-07-10 17:23:50 -04:00
Cody Henthorne
1e0e2fadfd Improve scroll to last position behavior. 2020-07-10 17:23:50 -04:00
Alan Evans
4325f714b9 Silent group update send job for profile key rotation. 2020-07-10 17:23:50 -04:00
Alan Evans
137cd45497 Hide "Add to a group" if you don't have any groups. 2020-07-10 17:23:50 -04:00
Alan Evans
f3dbe4416f Add lint to detect non-numeric version code checks. 2020-07-10 17:23:50 -04:00
Greyson Parrelli
7fb55c0f51 Keep borderless property when forwarding media. 2020-07-10 17:23:50 -04:00
Greyson Parrelli
fdc6cbc507 Bump version to 4.66.1 2020-07-09 19:10:27 -04:00
Greyson Parrelli
072085ae82 Updated language translations. 2020-07-09 19:09:54 -04:00
Greyson Parrelli
04a8996348 Add the ability to opt out of PINs. 2020-07-09 19:07:21 -04:00
Cody Henthorne
c26dcc2618 Fix theming issues with snackbars and alert dialogs. 2020-07-09 19:07:21 -04:00
Alan Evans
a4dc340bbc Handle empty group change byte array. 2020-07-09 19:07:21 -04:00
Cody Henthorne
3c069fb588 Enable Media Preview to respond to media changes. 2020-07-09 19:07:21 -04:00
Fumiaki Yoshimatsu
1fe38f5ed1 Fix pen/highlighter tool single tap.
Fixes #9745
2020-07-09 11:25:10 -03:00
Greyson Parrelli
841c9424e9 Remove GV2 flag requirement for WakeGroupV2Job. 2020-07-09 10:02:59 -04:00
Greyson Parrelli
9c44a0c7d3 Don't run ProfileUploadJob if you're not registered. 2020-07-09 07:57:37 -04:00
Greyson Parrelli
2883d2eb31 Enable video call PiP. 2020-07-09 07:50:38 -04:00
Greyson Parrelli
f5aade943e Bump version to 4.66.0 2020-07-08 17:15:10 -04:00
Greyson Parrelli
d17c3f39d0 Updated language translations. 2020-07-08 17:12:19 -04:00
Alan Evans
9ac9ace6b8 Groups V2 state comparison and gap handling. 2020-07-08 17:12:19 -04:00
Greyson Parrelli
c9d2cef58d Add support for sending borderless keyboard stickers. 2020-07-08 16:51:30 -04:00
Alan Evans
a9e30eefdc Prevent adding self to group by number.
Fixes #9821
2020-07-08 16:51:30 -04:00
Cody Henthorne
1a895db9bd Finalize support for calling with system PIP. 2020-07-08 16:51:30 -04:00
Alan Evans
a955bc3b9b Fix single line text input for group names. 2020-07-08 16:51:30 -04:00
Alan Evans
96e888a4f5 Remove versioned profiles feature flag. 2020-07-08 16:51:30 -04:00
Alan Evans
99ff0c1e3c Ensure direct add members to a group removes any matching pending. 2020-07-08 16:51:30 -04:00
Alan Evans
599e89b1f9 Fix audio waveform RTL rendering.
Fixes #9823
2020-07-08 16:51:30 -04:00
Greyson Parrelli
33c527f15e Remove the final KBS feature flags. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
eb02dacfdc Convert HEIC/HEIF to JPEG. 2020-07-08 16:51:30 -04:00
Alan Evans
e6a0e5b858 Add internal preferences under Advanced behind feature flag.
Initially for GV2 testing.
2020-07-08 16:51:30 -04:00
Greyson Parrelli
545ba80697 Add support for borderless images.
Added support for 'borderless' images. Basically images that we'd like to render 
as if they were stickers, even though they're not stickers. On iOS, this will be 
stuff like memoji and bitmoji. On Android, in my initial pass, I've just added 
support for Giphy stickers. However, we can also detect bitmoji and keyboard 
stickers in the future. This is kind of a 'best effort' thing, so as long as we 
support receiving, we can just add sending support for more things as we go.
2020-07-08 16:51:30 -04:00
Cody Henthorne
1e250ee95c Add Calling Requests. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
5a12eedc2c Prevent possible deadlock with LiveRecipientCache.
Thread A: DirectoryHelper#updateContactsDatabase() acquires database lock
Thread B: LiveRecipientCache#getSelf() acquires lock on LiveRecipientCache
Thread A: DirectoryHelper#updateContactsDatabase() calls Recipient.externalContact(), which eventually needs LiveRecipientCache lock
Thread B: Needs to read the database (e.g. line 120) to get information about itself

So A has the DB lock but needs the LiveRecipientCache lock, and B has
the LiveRecipientCache lock but needs the DB lock.

In general, we need to avoid acquiring any new locks in a transaction,
but for now, this specific instance looks like it could be solved by
using a unique lock for LiveRecipientCache#getSelf().
2020-07-08 16:51:30 -04:00
Greyson Parrelli
5605fde777 Rename the UUID flag to be more explicit. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
9ac142688a Increase the max PIN reminder interval to 4 weeks. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
2791790bf5 Implement new CDS changes. 2020-07-08 16:51:30 -04:00
Cody Henthorne
1752972be9 Update delete for everyone functionality to match requirements. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
c877aba09f Use resolved recipients in the conversation list. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
70e33518a9 Do registration checks for new numbers during group creation. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
cb81a9f783 Disallow 'visually empty' profile names. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
b6b499d865 Refresh recipients outside of a transaction for storage service. 2020-07-08 16:51:30 -04:00
Alan Evans
6704ad8193 Do not show update messages for profile key updates. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
942628a261 Improve ConversationListDataSource logging. 2020-07-08 16:51:30 -04:00
Greyson Parrelli
4ea8bac10d Re-enable view prefetching. 2020-07-08 16:51:30 -04:00
Alan Evans
eafccc5721 Add GV2 copy for the unknown editor. 2020-06-30 14:46:10 -03:00
Greyson Parrelli
a01bec3a11 Bump version to 4.65.2 2020-06-30 11:38:26 -04:00
Greyson Parrelli
3868175b85 Updated language translations. 2020-06-30 11:37:44 -04:00
Greyson Parrelli
904cb01067 Use the BlobProvider in the contact and group sync jobs. 2020-06-30 11:17:29 -04:00
Alan Evans
5c0cb425a6 Only sync V1 groups with linked devices. 2020-06-30 10:17:42 -03:00
Cody Henthorne
9dbb2ef630 Ensure user knows Safety Number Change Dialog list is scrollable when necessary. 2020-06-26 16:36:01 -04:00
Alan Evans
bafd2817ee Fix pending member activity background color. 2020-06-26 17:29:05 -03:00
Greyson Parrelli
3380293923 Bump version to 4.65.1 2020-06-26 15:40:23 -04:00
Greyson Parrelli
a549c1ec8b Updated language translations. 2020-06-26 15:38:48 -04:00
Greyson Parrelli
ad84997ce0 Fix display of quotes in 'All Media' view. 2020-06-26 15:33:08 -04:00
Alan Evans
42e2576813 Prevent repeat attempts when waveforms cannot be generated. 2020-06-26 16:18:27 -03:00
Cody Henthorne
31b995fa98 Retrieve profiles on mismatch to notify user of updates quicker. 2020-06-26 14:25:39 -03:00
Greyson Parrelli
0364bec995 Allow skipping if you hit a network error during PIN restore. 2020-06-26 14:25:39 -03:00
Alan Evans
aa39f3d0a3 Fix create new pin option in registration flow. 2020-06-26 13:29:00 -03:00
Greyson Parrelli
db545f43ea Remove profile name reminder megaphone. 2020-06-26 11:52:00 -04:00
Cody Henthorne
bbe003a454 Improve messaging and UX around safety number changes. 2020-06-26 11:10:54 -04:00
Greyson Parrelli
819f0f68f6 Fix issue with some search results returning empty. 2020-06-26 10:46:44 -04:00
Greyson Parrelli
8c0160937b Fix crash with 'select all' in conversation list.
Fixes #9790
2020-06-26 10:12:16 -04:00
Cody Henthorne
6de789dfe3 Prevent attachment download button re-animation. 2020-06-26 10:10:34 -04:00
Greyson Parrelli
afa2bb3bf5 Disallow swipe actions in search mode.
Fixes #9771
2020-06-26 10:08:01 -04:00
Greyson Parrelli
89e66c0741 Bump version to 4.65.0 2020-06-25 18:14:54 -04:00
Greyson Parrelli
0dc4afba99 Updated language translations. 2020-06-25 18:14:54 -04:00
Greyson Parrelli
152578e576 Add reserved job runners for inbound and outbound messages. 2020-06-25 18:14:54 -04:00
Greyson Parrelli
63d6ab6fa7 Throttle conversation list update frequency.
This helps fast phones process messages faster by reducing contention on
the database while processing a large batch of messages.
2020-06-25 18:14:54 -04:00
Greyson Parrelli
75c8c59d78 Reduce notification update interval. 2020-06-25 18:14:54 -04:00
Greyson Parrelli
87a59b6a9b Add support for memory-only jobs. 2020-06-25 18:14:54 -04:00
Alan Evans
2001fa86cf Log capabilities. 2020-06-25 18:14:54 -04:00
Alan Evans
52747782a7 Full screen avatar circle to square shape transition. 2020-06-25 18:14:54 -04:00
Fumiaki Yoshimatsu
66f2668326 Do not cache locale in each conversation object.
Fixes #9751
2020-06-25 18:14:54 -04:00
Cody Henthorne
b262efc24c Clear up warnings in string resource file. 2020-06-25 18:14:54 -04:00
Alan Evans
ce7ad76447 Cycle Versioned Profiles feature flag. 2020-06-25 08:29:48 -04:00
Greyson Parrelli
9e98b6616e Log job run time. 2020-06-25 08:29:48 -04:00
Alan Evans
f4c9eaa904 Remove some unused resources. 2020-06-25 08:29:48 -04:00
Greyson Parrelli
f8a0988e5f Various JobManager performance improvements. 2020-06-25 08:29:48 -04:00
Greyson Parrelli
bf919207ed Various logging improvements.
* Improve lifecycle logging.
* Remove 'action bar' from base activity names.
* Remove some unnecessary glide logs.
2020-06-25 08:29:48 -04:00
Greyson Parrelli
dac6b5c992 Bump version to 4.64.7 2020-06-24 20:09:31 -04:00
Greyson Parrelli
7f8043777e Updated language translations. 2020-06-24 20:09:00 -04:00
Greyson Parrelli
854b3feb36 Reduce verbosity of job logs. 2020-06-24 20:00:42 -04:00
Greyson Parrelli
22447e6ddb Fix theming issue with snackbar. 2020-06-24 20:00:42 -04:00
Alan Evans
be2ec36e1f Fix clipping issues with archive icon.
Fixes #8344
2020-06-24 20:00:12 -04:00
Greyson Parrelli
98cf16479d Bump version to 4.64.6 2020-06-24 10:58:13 -04:00
Greyson Parrelli
584735cbd0 Updated language translations. 2020-06-24 10:57:45 -04:00
Alan Evans
3741493cb7 Remove frame rate reporter and unused FPS ringbuffer. 2020-06-24 11:44:35 -03:00
Greyson Parrelli
4ea861fe5c Improve 'mark all read' performance. 2020-06-24 10:34:52 -04:00
Jim Gustafson
cd3df4d3c1 Update to ringrtc v2.2.0 2020-06-24 09:50:43 -04:00
Alan Evans
881a1edccb Bump version to 4.64.5 2020-06-22 10:53:52 -03:00
Alan Evans
1b7b574289 Updated language translations. 2020-06-22 10:50:27 -03:00
Alan Evans
d1d7498447 Fix text colors when system theme doesn't match. 2020-06-22 10:02:18 -03:00
Greyson Parrelli
50c18727e7 Bump version to 4.64.4 2020-06-21 12:23:31 -04:00
Greyson Parrelli
e9bfde470a Updated language translations. 2020-06-21 12:23:10 -04:00
Greyson Parrelli
68f718a210 Fix issue with conversation list times not updating.
Just started calling notifyDataSetChanged() in onResume() to provide
some sort of time update regularity.
2020-06-21 12:20:18 -04:00
Greyson Parrelli
c3e528ad4b Bump version to 4.64.3 2020-06-19 19:17:16 -04:00
Greyson Parrelli
28af97c400 Updated language translations. 2020-06-19 19:17:16 -04:00
Jim Gustafson
c2e4c343ab Update to ringrtc v2.1.1 2020-06-19 19:12:59 -04:00
Cody Henthorne
8a78589c2f Fix light navigation buttons in conversation settings screens. 2020-06-19 16:53:38 -04:00
Alan Evans
841ee18435 Add default option to message vibrate for pre API26. 2020-06-19 13:08:54 -03:00
Greyson Parrelli
71f54701d2 Add additional safeguards around disappearing messages. 2020-06-19 10:17:23 -04:00
Alan Evans
1c99939dfa Bump version to 4.64.2 2020-06-18 17:30:38 -03:00
Alan Evans
50462cecd0 Updated language translations. 2020-06-18 17:29:20 -03:00
Cody Henthorne
aa6a32f023 Make conversation footer always show. 2020-06-18 16:14:38 -04:00
Alan Evans
c4dc9064e3 Handle Attachment Keyboard selection of a too large item. 2020-06-18 15:55:26 -03:00
Alan Evans
bc5be10a0e Respect emoji config on conversation banner title. 2020-06-18 15:39:02 -03:00
Alan Evans
98d9b57379 Add copy to bottom sheet for Note to Self. 2020-06-18 14:34:30 -03:00
Cody Henthorne
021a16050a Stop back transition jank from avatar viewer to settings. 2020-06-18 13:16:08 -04:00
Alan Evans
555104aff0 Make message button navigate back if launched from the conversation. 2020-06-18 14:00:06 -03:00
Alan Evans
95d63b78f4 Add call and message buttons to recipient bottom sheet.
And insecure call button for non-registered contacts.
2020-06-18 13:23:46 -03:00
Alan Evans
80f9e1f4f1 Fix not able to get to archived conversations when all archived. 2020-06-18 12:23:20 -03:00
Alan Evans
a77997a4de Fix margins for "No groups in common" & unregistered case. 2020-06-18 09:49:22 -03:00
Alan Evans
ec4eb8e2a9 Bump version to 4.64.1 2020-06-17 17:54:58 -03:00
Alan Evans
1bdeade71e Updated language translations. 2020-06-17 17:53:19 -03:00
Greyson Parrelli
629ba105cb Detect real age of call request by using server timestamps. 2020-06-17 17:53:18 -03:00
Alan Evans
891a1af995 Show Note to Self for local number recipient preferences. 2020-06-17 17:49:44 -03:00
Cody Henthorne
0fbc6ac151 Revert improperly removed code for Message Request footer. 2020-06-17 17:49:43 -03:00
Alan Evans
a6384d1b73 Add insecure call ability to recipient settings. 2020-06-17 17:49:43 -03:00
Alan Evans
2fb9514890 Respect emoji setting in profile/group name editing. 2020-06-17 17:49:43 -03:00
Alan Evans
fe89794505 Hide recipient subtitle if no name/username set. 2020-06-17 17:49:43 -03:00
Cody Henthorne
08800c9faf Make Message Details update views in more situations. 2020-06-17 17:49:43 -03:00
Cody Henthorne
469a4700d2 Fix improper tinting on screens when using FallbackPhoto. 2020-06-17 17:49:43 -03:00
Alan Evans
6707f974a5 Remove NewGroupUI FeatureFlag. 2020-06-17 17:49:43 -03:00
Alan Evans
c122cada2b Change call button shade. 2020-06-17 17:49:43 -03:00
Alan Evans
96f02d8c95 Hide some views for Note to Self conversation. 2020-06-17 17:49:43 -03:00
Greyson Parrelli
dd717b60b8 Bump version to 4.64.0 2020-06-16 23:47:15 -04:00
Greyson Parrelli
3c20c7f4b4 Updated language translations. 2020-06-16 23:46:41 -04:00
Cody Henthorne
1a09e70a04 Remove old Message Details. 2020-06-16 19:30:35 -04:00
Alan Evans
027453bbd2 Prevent IllegalStateException on recipient bottom sheet. 2020-06-16 19:30:35 -04:00
Greyson Parrelli
b621efa4a5 Don't prefetch views for the conversation list. 2020-06-16 19:30:35 -04:00
Cody Henthorne
2915e4698c Show registration rate limit error messaging. 2020-06-16 19:30:35 -04:00
Cody Henthorne
b687b1a4c5 Fix repeat alerts by using explicit reminder intent. 2020-06-16 19:30:35 -04:00
Alan Evans
b53827f32b Manage recipient activity. 2020-06-16 19:30:35 -04:00
Cody Henthorne
d9641128a8 Refresh Message Details screen. 2020-06-16 19:30:35 -04:00
Alan Evans
dfb5562142 Use group manager for MMS groups. 2020-06-16 19:30:35 -04:00
Jim Gustafson
d467c04749 Ensure speaker off at start of any call 2020-06-16 19:30:35 -04:00
Greyson Parrelli
3d7cffef2b Remove Message Requests feature flag. 2020-06-16 19:30:35 -04:00
Alex Hart
f2fe81d9b5 Fix conversation jumping when loading at last scroll position. 2020-06-16 19:30:35 -04:00
Greyson Parrelli
cf98a22269 Add placeholder support for ConversationListAdapter. 2020-06-16 19:30:35 -04:00
Alex Hart
49f75d7036 Migrate ConversationList to paging library and apply abstractions to conversation. 2020-06-16 19:30:35 -04:00
Greyson Parrelli
ce940235b0 Optimistically fetch profiles. 2020-06-16 19:30:35 -04:00
Alan Evans
f5626f678d Make CustomNotificationsDialogFragment work with recipients. 2020-06-16 19:30:35 -04:00
Alan Evans
b3a59c3946 Use recipient display name in recipient bottom sheet. 2020-06-16 19:30:35 -04:00
Fumiaki Yoshimatsu
93c390c4fc Don't send a read receipt when the recipient is blocked.
Fixes #9610
2020-06-16 19:30:35 -04:00
Cody Henthorne
941ab5a98f Prevent avatar from showing a start of outgoing video call. 2020-06-16 19:30:35 -04:00
Jim Gustafson
2ecdf803c0 Update to ringrtc v2.1.0 2020-06-16 19:30:35 -04:00
Cody Henthorne
5b2a399392 Return to previous scroll position when returning to a conversation. 2020-06-16 19:30:35 -04:00
Alex Hart
a9ea1d7606 Utilize DayNight theme when launching the app. 2020-06-12 11:36:15 -03:00
Greyson Parrelli
1ce8ac2de6 Light refactor of SignalStore. 2020-06-12 11:36:15 -03:00
Greyson Parrelli
e2019579fb Bump version to 4.63.3 2020-06-12 10:09:20 -04:00
Greyson Parrelli
fb3c6e56ee Updated language translations. 2020-06-12 10:08:51 -04:00
Greyson Parrelli
3fad007ae0 Cancel typing jobs when you send a group message. 2020-06-12 10:06:20 -04:00
Greyson Parrelli
8891b6c930 Properly throw UnregisteredUserException in SignalServicePipe. 2020-06-11 12:08:40 -04:00
Alan Evans
400c592acf Display 'Unknown group' for groups with no name. 2020-06-10 17:17:47 -03:00
Alex Hart
e13f3254ad Fix message jump-to-position. 2020-06-10 17:06:40 -03:00
1032 changed files with 42188 additions and 18594 deletions

View File

@@ -80,8 +80,8 @@ protobuf {
}
}
def canonicalVersionCode = 655
def canonicalVersionName = "4.63.2"
def canonicalVersionCode = 692
def canonicalVersionName = "4.69.2"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@@ -122,7 +122,7 @@ android {
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDS_MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
@@ -197,7 +197,7 @@ android {
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
@@ -304,7 +304,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.0.3'
implementation 'org.signal:ringrtc-android:2.4.1'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
@@ -358,11 +358,11 @@ dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'org.mockito:mockito-core:1.9.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'org.mockito:mockito-core:2.8.9'
testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.4'
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
testImplementation 'androidx.test:core:1.2.0'
testImplementation ('org.robolectric:robolectric:4.2') {
@@ -455,3 +455,13 @@ def getLastCommitTimestamp() {
return os.toString() + "000"
}
}
tasks.withType(Test) {
testLogging {
events "failed"
exceptionFormat "full"
showCauses true
showExceptions true
showStackTraces true
}
}

View File

@@ -1,15 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Wont pass lint or qa with a STOPSHIP in a comment -->
<issue id="StopShip" severity="fatal" />
<!-- L10N errors -->
<!-- This is a runtime crash so we don't want to ship with this. -->
<issue id="StringFormatMatches" severity="error" />
<!-- L10N warnings -->
<issue id="MissingTranslation" severity="warning" />
<issue id="MissingTranslation" severity="ignore" />
<issue id="MissingQuantity" severity="warning" />
<issue id="MissingDefaultResource" severity="error">
<ignore path="*/res/values-*/strings.xml" /> <!-- Ignore for non-English, excludeNonTranslatables task will remove these -->
</issue>
<issue id="ExtraTranslation" severity="warning" />
<issue id="ImpliedQuantity" severity="warning" />
<issue id="TypographyDashes" severity="error" >
<ignore path="*/res/values-*/strings.xml" /> <!-- Ignore for non-English -->
</issue>
<issue id="CanvasSize" severity="error" />
<issue id="HardcodedText" severity="error" />

View File

@@ -120,8 +120,15 @@
android:supportsPictureInPicture="true"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:taskAffinity=".calling"
android:launchMode="singleTask"/>
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
android:theme="@style/TextSecure.DarkNoActionBar"
android:screenOrientation="portrait"
android:noHistory="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".InviteActivity"
android:theme="@style/Signal.Light.NoActionBar.Invite"
android:windowSoftInputMode="stateHidden"
@@ -151,7 +158,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".sharing.ShareActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
@@ -184,7 +191,7 @@
</activity>
<activity android:name=".stickers.StickerPackPreviewActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
@@ -216,6 +223,14 @@
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="group.signal.org"/>
</intent-filter>
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher" />
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
@@ -243,24 +258,24 @@
android:theme="@style/TextSecure.LightTheme.Popup"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".MessageDetailsActivity"
<activity android:name=".messagedetails.MessageDetailsActivity"
android:label="@string/AndroidManifest__message_details"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".GroupCreateActivity"
android:windowSoftInputMode="stateVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
@@ -274,7 +289,7 @@
<activity android:name=".PassphraseCreateActivity"
android:label="@string/AndroidManifest__create_passphrase"
android:windowSoftInputMode="stateUnchanged"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -284,7 +299,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".NewConversationActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -294,7 +309,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".giph.ui.GiphyActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -359,7 +374,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediaoverview.MediaOverviewActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -410,10 +425,6 @@
</activity>
<activity android:name=".RecipientPreferenceActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.AvatarSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -452,6 +463,9 @@
</intent-filter>
</activity>
<activity android:name=".contacts.TurnOffContactJoinedNotificationsActivity"
android:theme="@style/Theme.AppCompat.Dialog.Alert" />
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
@@ -462,44 +476,47 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.ContactNameEditActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.SharedContactDetailsActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ShortcutLauncherActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity
android:name=".maps.PlacePickerActivity"
android:label="@string/PlacePickerActivity_title"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MainActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".pin.PinRestoreActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.addtogroup.AddToGroupsActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.addmembers.AddMembersActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
@@ -700,11 +717,7 @@
</intent-filter>
</receiver>
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.MessageNotifier.REMINDER_ACTION"/>
</intent-filter>
</receiver>
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver"/>
<receiver android:name=".notifications.DeleteNotificationReceiver">
<intent-filter>

View File

@@ -45,12 +45,12 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.messages.InitialMessageRetriever;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
@@ -60,7 +60,6 @@ import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
@@ -96,7 +95,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
private ViewOnceMessageManager viewOnceMessageManager;
private TypingStatusRepository typingStatusRepository;
private TypingStatusSender typingStatusSender;
private IncomingMessageObserver incomingMessageObserver;
private PersistentLogger persistentLogger;
private volatile boolean isAppVisible;
@@ -107,10 +105,11 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
@Override
public void onCreate() {
long startTime = System.currentTimeMillis();
super.onCreate();
Log.i(TAG, "onCreate()");
initializeSecurityProvider();
initializeLogging();
Log.i(TAG, "onCreate()");
initializeCrashHandling();
initializeAppDependencies();
initializeFirstEverAppLaunch();
@@ -133,7 +132,8 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync();
RegistrationUtil.markRegistrationPossiblyComplete();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
RegistrationUtil.maybeMarkRegistrationComplete(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
if (Build.VERSION.SDK_INT < 21) {
@@ -141,6 +141,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
}
ApplicationDependencies.getJobManager().beginJobLoop();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
}
@Override
@@ -153,7 +154,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
catchUpOnMessages();
}
@Override
@@ -230,7 +230,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
}
public void initializeMessageRetrieval() {
this.incomingMessageObserver = new IncomingMessageObserver(this);
ApplicationDependencies.getIncomingMessageObserver();
}
private void initializeAppDependencies() {
@@ -378,36 +378,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
});
}
private void catchUpOnMessages() {
InitialMessageRetriever retriever = ApplicationDependencies.getInitialMessageRetriever();
if (retriever.isCaughtUp()) {
return;
}
SignalExecutors.UNBOUNDED.execute(() -> {
long startTime = System.currentTimeMillis();
switch (retriever.begin(TimeUnit.SECONDS.toMillis(60))) {
case SUCCESS:
Log.i(TAG, "Successfully caught up on messages. " + (System.currentTimeMillis() - startTime) + " ms");
break;
case FAILURE_TIMEOUT:
Log.w(TAG, "Did not finish catching up due to a timeout. " + (System.currentTimeMillis() - startTime) + " ms");
break;
case FAILURE_ERROR:
Log.w(TAG, "Did not finish catching up due to an error. " + (System.currentTimeMillis() - startTime) + " ms");
break;
case SKIPPED_ALREADY_CAUGHT_UP:
Log.i(TAG, "Already caught up. " + (System.currentTimeMillis() - startTime) + " ms");
break;
case SKIPPED_ALREADY_RUNNING:
Log.i(TAG, "Already in the process of catching up. " + (System.currentTimeMillis() - startTime) + " ms");
break;
}
});
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));

View File

@@ -53,7 +53,7 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
*
*/
public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarActivity
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
implements SharedPreferences.OnSharedPreferenceChangeListener
{
@SuppressWarnings("unused")
@@ -272,12 +272,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(preference.getContext(), EditProfileActivity.class);
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
requireActivity().startActivity(intent);
requireActivity().startActivity(EditProfileActivity.getIntentForUserProfileEdit(preference.getContext()));
return true;
}
}

View File

@@ -1,11 +1,19 @@
package org.thoughtcrime.securesms;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.transition.TransitionInflater;
import android.view.DisplayCutout;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView;
@@ -14,12 +22,15 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
@@ -33,7 +44,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
/**
* Activity for displaying avatars full screen.
*/
public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActivity {
public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
private static final String TAG = Log.tag(AvatarPreviewActivity.class);
@@ -58,14 +69,26 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActi
setTheme(R.style.TextSecure_MediaPreview);
setContentView(R.layout.contact_photo_preview_activity);
Toolbar toolbar = findViewById(R.id.toolbar);
ImageView avatar = findViewById(R.id.avatar);
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));
}
Toolbar toolbar = findViewById(R.id.toolbar);
ImageView avatar = findViewById(R.id.avatar);
setSupportActionBar(toolbar);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
if (Build.VERSION.SDK_INT >= 28) {
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
toolbar.getViewTreeObserver().addOnGlobalLayoutListener(new DisplayCutoutAdjuster(toolbar, findViewById(R.id.toolbar_cutout_spacer)));
}
showSystemUI();
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -79,26 +102,42 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActi
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();
GlideApp.with(this).load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this))
.error(fallbackPhoto.asCallCard(this))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.addListener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
Log.w(TAG, "Unable to load avatar, or avatar removed, closing");
finish();
return false;
}
Resources resources = this.getResources();
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
return false;
}
})
.into(avatar);
GlideApp.with(this)
.asBitmap()
.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this))
.error(fallbackPhoto.asCallCard(this))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.addListener(new RequestListener<Bitmap>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
Log.w(TAG, "Unable to load avatar, or avatar removed, closing");
finish();
return false;
}
toolbar.setTitle(recipient.toShortString(context));
@Override
public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
return false;
}
})
.into(new CustomTarget<Bitmap>() {
@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();
}
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
toolbar.setTitle(recipient.getDisplayName(context));
});
avatar.setOnClickListener(v -> toggleUiVisibility());
@@ -149,4 +188,36 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActi
onBackPressed();
return true;
}
/**
* Adjust a spacer for the toolbar when a display cutout is detected. Runs within
* a layout listener because the activity delays view attachment due to the transitions
* and needs to update on device rotation.
*/
@TargetApi(28)
private static class DisplayCutoutAdjuster implements ViewTreeObserver.OnGlobalLayoutListener {
private final View view;
private final View spacer;
private DisplayCutoutAdjuster(@NonNull View view, @NonNull View spacer) {
this.view = view;
this.spacer = spacer;
}
@Override
public void onGlobalLayout() {
if (view.getRootWindowInsets() == null) {
return;
}
DisplayCutout cutout = view.getRootWindowInsets().getDisplayCutout();
if (cutout != null) {
ViewGroup.LayoutParams params = spacer.getLayoutParams();
params.height = cutout.getSafeInsetTop();
spacer.setLayoutParams(params);
spacer.setVisibility(View.VISIBLE);
}
}
}
}

View File

@@ -1,101 +0,0 @@
package org.thoughtcrime.securesms;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.appcompat.app.AppCompatActivity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import java.lang.reflect.Field;
public abstract class BaseActionBarActivity extends AppCompatActivity {
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
if (BaseActivity.isMenuWorkaroundRequired()) {
forceOverflowMenu();
}
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
initializeScreenshotSecurity();
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) {
openOptionsMenu();
return true;
}
return super.onKeyUp(keyCode, event);
}
private void initializeScreenshotSecurity() {
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
/**
* Modified from: http://stackoverflow.com/a/13098824
*/
private void forceOverflowMenu() {
try {
ViewConfiguration config = ViewConfiguration.get(this);
Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey");
if(menuKeyField != null) {
menuKeyField.setAccessible(true);
menuKeyField.setBoolean(config, false);
}
} catch (IllegalAccessException e) {
Log.w(TAG, "Failed to force overflow menu.");
} catch (NoSuchFieldException e) {
Log.w(TAG, "Failed to force overflow menu.");
}
}
protected void startActivitySceneTransition(Intent intent, View sharedView, String transitionName) {
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedView, transitionName)
.toBundle();
ActivityCompat.startActivity(this, intent, bundle);
}
@TargetApi(VERSION_CODES.LOLLIPOP)
protected void setStatusBarColor(int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(color);
}
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
}
}

View File

@@ -1,46 +1,90 @@
package org.thoughtcrime.securesms;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import android.view.KeyEvent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.view.WindowManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
public abstract class BaseActivity extends FragmentActivity {
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
}
/**
* Base class for all activities. The vast majority of activities shouldn't extend this directly.
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
* screen lock.
*/
public abstract class BaseActivity extends AppCompatActivity {
private static final String TAG = Log.tag(BaseActivity.class);
@Override
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) {
openOptionsMenu();
return true;
}
return super.onKeyUp(keyCode, event);
}
public static boolean isMenuWorkaroundRequired() {
return VERSION.SDK_INT < VERSION_CODES.KITKAT &&
VERSION.SDK_INT > VERSION_CODES.GINGERBREAD_MR1 &&
("LGE".equalsIgnoreCase(Build.MANUFACTURER) || "E6710".equalsIgnoreCase(Build.DEVICE));
protected void onCreate(Bundle savedInstanceState) {
logEvent("onCreate()");
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
initializeScreenshotSecurity();
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
}
@Override
protected void onStart() {
logEvent("onStart()");
super.onStart();
}
@Override
protected void onStop() {
logEvent("onStop()");
super.onStop();
}
@Override
protected void onDestroy() {
logEvent("onDestroy()");
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();
ActivityCompat.startActivity(this, intent, bundle);
}
@TargetApi(VERSION_CODES.LOLLIPOP)
protected void setStatusBarColor(int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(color);
}
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
}
private void logEvent(@NonNull String event) {
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
}
}

View File

@@ -1,13 +1,14 @@
package org.thoughtcrime.securesms;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.View;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mms.GlideRequests;
@@ -21,17 +22,17 @@ import java.util.Locale;
import java.util.Set;
public interface BindableConversationItem extends Unbindable {
void bind(@NonNull MessageRecord messageRecord,
void bind(@NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient recipients,
@Nullable String searchQuery,
boolean pulseHighlight);
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient recipients,
@Nullable String searchQuery,
boolean pulseMention);
MessageRecord getMessageRecord();
ConversationMessage getConversationMessage();
void setEventListener(@Nullable EventListener listener);
@@ -45,7 +46,11 @@ public interface BindableConversationItem extends Unbindable {
void onAddToContactsClicked(@NonNull Contact contact);
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
void onReactionClicked(long messageId, boolean isMms);
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);
}
}

View File

@@ -14,4 +14,7 @@ public interface BindableConversationListItem extends Unbindable {
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull Set<Long> selectedThreads, boolean batchMode);
void setBatchMode(boolean batchMode);
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
}

View File

@@ -4,7 +4,6 @@ import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
@@ -28,7 +27,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity {
public class BlockedContactsActivity extends PassphraseRequiredActivity {
private final DynamicTheme dynamicTheme = new DynamicTheme();

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.database.Cursor;
import android.os.AsyncTask;
import android.text.SpannableString;
import android.text.Spanned;
@@ -15,7 +14,6 @@ import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
@@ -51,7 +49,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
super(context);
Recipient recipient = Recipient.resolved(mismatch.getRecipientId(context));
String name = recipient.toShortString(context);
String name = recipient.getDisplayName(context);
String introduction = context.getString(R.string.ConfirmIdentityDialog_your_safety_number_with_s_has_changed, name, name);
SpannableString spannableString = new SpannableString(introduction + " " +
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_your_safety_number_with_this_contact));
@@ -105,7 +103,6 @@ public class ConfirmIdentityDialog extends AlertDialog {
}
processMessageRecord(messageRecord);
processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
return null;
}
@@ -115,26 +112,6 @@ public class ConfirmIdentityDialog extends AlertDialog {
else processIncomingMessageRecord(messageRecord);
}
private void processPendingMessageRecords(long threadId, IdentityKeyMismatch mismatch) {
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(getContext());
Cursor cursor = mmsSmsDatabase.getIdentityConflictMessagesForThread(threadId);
MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(cursor);
MessageRecord record;
try {
while ((record = reader.getNext()) != null) {
for (IdentityKeyMismatch recordMismatch : record.getIdentityKeyMismatches()) {
if (mismatch.equals(recordMismatch)) {
processMessageRecord(record);
}
}
}
} finally {
if (reader != null)
reader.close();
}
}
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
@@ -175,7 +152,9 @@ public class ConfirmIdentityDialog extends AlertDialog {
messageRecord.getDateSent(),
legacy ? Base64.decode(messageRecord.getBody()) : null,
!legacy ? Base64.decode(messageRecord.getBody()) : null,
0, null);
0,
0,
null);
long pushId = pushDatabase.insert(envelope);

View File

@@ -42,7 +42,7 @@ import java.lang.ref.WeakReference;
* @author Moxie Marlinspike
*
*/
public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity
public abstract class ContactSelectionActivity extends PassphraseRequiredActivity
implements SwipeRefreshLayout.OnRefreshListener,
ContactSelectionListFragment.OnContactSelectedListener,
ContactSelectionListFragment.ScrollCallback
@@ -113,7 +113,9 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
}
@Override
public void onContactSelected(Optional<RecipientId> recipientId, String number) {}
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
return true;
}
@Override
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}

View File

@@ -40,7 +40,6 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
@@ -93,7 +92,7 @@ import java.util.Set;
* @author Moxie Marlinspike
*
*/
public final class ContactSelectionListFragment extends Fragment
public final class ContactSelectionListFragment extends LoggingFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
@SuppressWarnings("unused")
@@ -271,12 +270,8 @@ public final class ContactSelectionListFragment extends Fragment
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
if (listCallback != null && FeatureFlags.newGroupUI()) {
if (FeatureFlags.groupsV2create() && FeatureFlags.groupsV2internalTest()) {
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback), createNewGroupsV1GroupItem(listCallback));
} else {
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
}
if (listCallback != null) {
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
headerAdapter.hide();
concatenateAdapter.addAdapter(headerAdapter);
}
@@ -317,13 +312,6 @@ public final class ContactSelectionListFragment extends Fragment
return view;
}
private View createNewGroupsV1GroupItem(@NonNull ListCallback listCallback) {
View view = LayoutInflater.from(requireContext())
.inflate(R.layout.contact_selection_new_group_v1_item, (ViewGroup) requireView(), false);
view.setOnClickListener(v -> listCallback.onNewGroup(true));
return view;
}
private void initializeNoContactsPermission() {
swipeRefresh.setVisibility(View.GONE);
@@ -463,6 +451,11 @@ public final class ContactSelectionListFragment extends Fragment
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
if (isMulti() && 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 (selectionLimitReached()) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
@@ -480,11 +473,15 @@ public final class ContactSelectionListFragment extends Fragment
if (uuid.isPresent()) {
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null);
if (onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null)) {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
new AlertDialog.Builder(requireContext())
@@ -495,11 +492,14 @@ public final class ContactSelectionListFragment extends Fragment
}
});
} else {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber());
if (onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber())) {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
}
} else {
@@ -518,7 +518,7 @@ public final class ContactSelectionListFragment extends Fragment
private void markContactSelected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
if (isMulti() && FeatureFlags.newGroupUI()) {
if (isMulti()) {
addChipForSelectedContact(selectedContact);
}
}
@@ -631,7 +631,8 @@ public final class ContactSelectionListFragment extends Fragment
}
public interface OnContactSelectedListener {
void onContactSelected(Optional<RecipientId> recipientId, String number);
/** @return True if the contact is allowed to be selected, otherwise false. */
boolean onContactSelected(Optional<RecipientId> recipientId, String number);
void onContactDeselected(Optional<RecipientId> recipientId, String number);
}

View File

@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
import org.thoughtcrime.securesms.service.ApplicationMigrationService.ImportState;
public class DatabaseMigrationActivity extends PassphraseRequiredActionBarActivity {
public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
private final ImportServiceConnection serviceConnection = new ImportServiceConnection();
private final ImportStateHandler importStateHandler = new ImportStateHandler();

View File

@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.DynamicLanguage;
@@ -42,7 +41,7 @@ import org.whispersystems.signalservice.internal.push.DeviceLimitExceededExcepti
import java.io.IOException;
public class DeviceActivity extends PassphraseRequiredActionBarActivity
public class DeviceActivity extends PassphraseRequiredActivity
implements Button.OnClickListener, ScanListener, DeviceLinkFragment.LinkClickedListener
{

View File

@@ -6,7 +6,7 @@ import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewAnimationUtils;
@@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.qr.ScanningThread;
import org.thoughtcrime.securesms.util.ViewUtil;
public class DeviceAddFragment extends Fragment {
public class DeviceAddFragment extends LoggingFragment {
private ViewGroup container;
private LinearLayout overlay;

View File

@@ -53,7 +53,7 @@ public class DeviceListFragment extends ListFragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA);
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
}
@Override

View File

@@ -5,7 +5,7 @@ import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import android.view.Window;
public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActivity {
public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
@SuppressWarnings("unused")
private static final String TAG = DeviceProvisioningActivity.class.getSimpleName();
@@ -26,7 +26,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv
startActivity(intent);
finish();
})
.setNegativeButton(R.string.DeviceProvisioningActivity_cancel, (dialog12, which) -> {
.setNegativeButton(android.R.string.cancel, (dialog12, which) -> {
dialog12.dismiss();
finish();
})

View File

@@ -1,14 +1,17 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import java.util.Arrays;
import cn.carbswang.android.numberpickerview.library.NumberPickerView;
public class ExpirationDialog extends AlertDialog {
@@ -36,7 +39,7 @@ public class ExpirationDialog extends AlertDialog {
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
listener.onClick(context.getResources().getIntArray(R.array.expiration_times)[selected]);
listener.onClick(getExpirationTimes(context, currentExpiration)[selected]);
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
@@ -47,7 +50,7 @@ public class ExpirationDialog extends AlertDialog {
final View view = inflater.inflate(R.layout.expiration_dialog, null);
final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
final TextView textView = view.findViewById(R.id.expiration_details);
final int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
final int[] expirationTimes = getExpirationTimes(context, currentExpiration);
final String[] expirationDisplayValues = new String[expirationTimes.length];
int selectedIndex = expirationTimes.length - 1;
@@ -80,6 +83,19 @@ public class ExpirationDialog extends AlertDialog {
return view;
}
private static int[] getExpirationTimes(Context context, int currentExpiration) {
int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
int location = Arrays.binarySearch(expirationTimes, currentExpiration);
if (location < 0) {
int[] temp = Arrays.copyOf(expirationTimes, expirationTimes.length + 1);
temp[temp.length - 1] = currentExpiration;
Arrays.sort(temp);
expirationTimes = temp;
}
return expirationTimes;
}
public interface OnClickListener {
public void onClick(int expirationTime);
}

View File

@@ -1,629 +0,0 @@
/*
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.components.PushRecipientsPanel;
import org.thoughtcrime.securesms.components.PushRecipientsPanel.RecipientsPanelChangedListener;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.RecipientsEditor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* Activity to create and update {@link GroupId.V1} groups
*
* @author Jake McGinty
*/
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
implements OnRecipientDeletedListener,
RecipientsPanelChangedListener
{
private final static String TAG = GroupCreateActivity.class.getSimpleName();
private static final String GROUP_ID_EXTRA = "group_id";
private static final String GROUP_THREAD_EXTRA = "group_thread";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private static final short REQUEST_CODE_SELECT_AVATAR = 26165;
private static final int PICK_CONTACT = 1;
private EditText groupName;
private ListView listView;
private ImageView avatar;
private TextView creatingText;
private Bitmap avatarBmp;
@NonNull private Optional<GroupData> groupToUpdate = Optional.absent();
public static Intent newEditGroupIntent(@NonNull Context context, @NonNull GroupId.V1 groupId) {
Intent intent = new Intent(context, GroupCreateActivity.class);
intent.putExtra(GroupCreateActivity.GROUP_ID_EXTRA, groupId.toString());
return intent;
}
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle state, boolean ready) {
setContentView(R.layout.group_create_activity);
//noinspection ConstantConditions
initializeAppBar();
initializeResources();
initializeExistingGroup();
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
updateViewState();
}
private boolean isSignalGroup() {
return TextSecurePreferences.isPushRegistered(this) && !getAdapter().hasNonPushMembers();
}
private void disableSignalGroupViews(int reasonResId) {
View pushDisabled = findViewById(R.id.push_disabled);
pushDisabled.setVisibility(View.VISIBLE);
((TextView) findViewById(R.id.push_disabled_reason)).setText(reasonResId);
avatar.setEnabled(false);
groupName.setEnabled(false);
}
private void enableSignalGroupViews() {
findViewById(R.id.push_disabled).setVisibility(View.GONE);
avatar.setEnabled(true);
groupName.setEnabled(true);
}
@SuppressWarnings("ConstantConditions")
private void updateViewState() {
if (!TextSecurePreferences.isPushRegistered(this)) {
disableSignalGroupViews(R.string.GroupCreateActivity_youre_not_registered_for_signal);
getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_mms_title);
} else if (getAdapter().hasNonPushMembers()) {
disableSignalGroupViews(R.string.GroupCreateActivity_contacts_dont_support_push);
getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_mms_title);
} else {
enableSignalGroupViews();
getSupportActionBar().setTitle(groupToUpdate.isPresent()
? R.string.GroupCreateActivity_actionbar_edit_title
: R.string.GroupCreateActivity_actionbar_title);
}
}
private static boolean isActiveInDirectory(Recipient recipient) {
return recipient.resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED;
}
private void addSelectedContacts(@NonNull Recipient... recipients) {
new AddMembersTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipients);
}
private void addSelectedContacts(@NonNull Collection<Recipient> recipients) {
addSelectedContacts(recipients.toArray(new Recipient[recipients.size()]));
}
private void initializeAppBar() {
Drawable upIcon = ContextCompat.getDrawable(this, R.drawable.ic_arrow_left_24);
getSupportActionBar().setHomeAsUpIndicator(upIcon);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
private void initializeResources() {
RecipientsEditor recipientsEditor = findViewById(R.id.recipients_text);
PushRecipientsPanel recipientsPanel = findViewById(R.id.recipients);
listView = findViewById(R.id.selected_contacts_list);
avatar = findViewById(R.id.avatar);
groupName = findViewById(R.id.group_name);
creatingText = findViewById(R.id.creating_group_text);
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(this);
adapter.setOnRecipientDeletedListener(this);
listView.setAdapter(adapter);
recipientsEditor.setHint(R.string.recipients_panel__add_members);
recipientsPanel.setPanelChangeListener(this);
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
avatar.setImageDrawable(getDefaultGroupAvatar());
avatar.setOnClickListener(view -> AvatarSelectionBottomSheetDialogFragment.create(avatarBmp != null, false, REQUEST_CODE_SELECT_AVATAR, true).show(getSupportFragmentManager(), null));
}
private Drawable getDefaultGroupAvatar() {
return new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this));
}
private void initializeExistingGroup() {
final GroupId groupId = GroupId.parseNullableOrThrow(getIntent().getStringExtra(GROUP_ID_EXTRA));
if (groupId != null) {
GroupId.V1 groupIdV1 = groupId.requireV1();
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupIdV1);
if (FeatureFlags.newGroupUI()) {
avatar.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(this, groupIdV1)));
}
}
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater();
menu.clear();
inflater.inflate(R.menu.group_create, menu);
super.onPrepareOptionsMenu(menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.menu_create_group:
if (groupToUpdate.isPresent()) handleGroupUpdate();
else handleGroupCreate();
return true;
}
return false;
}
@Override
public void onRecipientDeleted(Recipient recipient) {
getAdapter().remove(recipient);
updateViewState();
}
@Override
public void onRecipientsPanelUpdate(List<Recipient> recipients) {
if (recipients != null && !recipients.isEmpty()) addSelectedContacts(recipients);
}
private void handleGroupCreate() {
if (getAdapter().getCount() < 1) {
Log.i(TAG, getString(R.string.GroupCreateActivity_contacts_no_members));
Toast.makeText(getApplicationContext(), R.string.GroupCreateActivity_contacts_no_members, Toast.LENGTH_SHORT).show();
return;
}
if (isSignalGroup()) {
new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
new CreateMmsGroupTask(this, getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
private void handleGroupUpdate() {
new UpdateSignalGroupV1Task(this, groupToUpdate.get().id, avatarBmp,
getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void handleOpenConversation(long threadId, Recipient recipient) {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
startActivity(intent);
finish();
}
private SelectedRecipientsAdapter getAdapter() {
return (SelectedRecipientsAdapter) listView.getAdapter();
}
private @Nullable String getGroupName() {
return groupName.getText() != null ? groupName.getText().toString() : null;
}
@Override
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
super.onActivityResult(reqCode, resultCode, data);
if (data == null || resultCode != Activity.RESULT_OK)
return;
switch (reqCode) {
case PICK_CONTACT:
List<RecipientId> selected = data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS);
for (RecipientId contact : selected) {
Recipient recipient = Recipient.resolved(contact);
addSelectedContacts(recipient);
}
break;
case REQUEST_CODE_SELECT_AVATAR:
if (data.getBooleanExtra("delete", false)) {
avatarBmp = null;
avatar.setImageDrawable(getDefaultGroupAvatar());
return;
}
final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
final DecryptableUri decryptableUri = new DecryptableUri(result.getUri());
GlideApp.with(this)
.asBitmap()
.load(decryptableUri)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.centerCrop()
.override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
setAvatar(decryptableUri, resource);
}
});
}
}
private class AddRecipientButtonListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Intent intent = new Intent(GroupCreateActivity.this, PushContactSelectionActivity.class);
if (groupToUpdate.isPresent()) {
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_PUSH);
} else {
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_PUSH | DisplayMode.FLAG_SMS);
}
startActivityForResult(intent, PICK_CONTACT);
}
}
private static class CreateMmsGroupTask extends AsyncTask<Void,Void,GroupActionResult> {
private final GroupCreateActivity activity;
private final Set<Recipient> members;
public CreateMmsGroupTask(GroupCreateActivity activity, Set<Recipient> members) {
this.activity = activity;
this.members = members;
}
@Override
protected GroupActionResult doInBackground(Void... avoid) {
List<RecipientId> memberAddresses = new LinkedList<>();
for (Recipient recipient : members) {
memberAddresses.add(recipient.getId());
}
memberAddresses.add(Recipient.self().getId());
GroupId.Mms groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateMmsGroupForMembers(memberAddresses);
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
return new GroupActionResult(groupRecipient, threadId);
}
@Override
protected void onPostExecute(GroupActionResult result) {
activity.handleOpenConversation(result.getThreadId(), result.getGroupRecipient());
}
@Override
protected void onProgressUpdate(Void... values) {
super.onProgressUpdate(values);
}
}
private abstract static class SignalGroupTask extends AsyncTask<Void,Void,Optional<GroupActionResult>> {
protected GroupCreateActivity activity;
protected Bitmap avatar;
protected Set<Recipient> members;
protected String name;
public SignalGroupTask(GroupCreateActivity activity,
Bitmap avatar,
String name,
Set<Recipient> members)
{
this.activity = activity;
this.avatar = avatar;
this.name = name;
this.members = members;
}
@Override
protected void onPreExecute() {
activity.findViewById(R.id.group_details_layout).setVisibility(View.GONE);
activity.findViewById(R.id.creating_group_layout).setVisibility(View.VISIBLE);
activity.findViewById(R.id.menu_create_group).setVisibility(View.GONE);
final int titleResId = activity.groupToUpdate.isPresent()
? R.string.GroupCreateActivity_updating_group
: R.string.GroupCreateActivity_creating_group;
activity.creatingText.setText(activity.getString(titleResId, activity.getGroupName()));
}
@Override
protected void onPostExecute(Optional<GroupActionResult> groupActionResultOptional) {
if (activity.isFinishing()) return;
activity.findViewById(R.id.group_details_layout).setVisibility(View.VISIBLE);
activity.findViewById(R.id.creating_group_layout).setVisibility(View.GONE);
activity.findViewById(R.id.menu_create_group).setVisibility(View.VISIBLE);
}
}
private static class CreateSignalGroupTask extends SignalGroupTask {
public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set<Recipient> members) {
super(activity, avatar, name, members);
}
@Override
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
return Optional.of(GroupManager.createGroupV1(activity, members, BitmapUtil.toByteArray(avatar), name, false));
}
@Override
protected void onPostExecute(Optional<GroupActionResult> result) {
if (result.isPresent() && result.get().getThreadId() > -1) {
if (!activity.isFinishing()) {
activity.handleOpenConversation(result.get().getThreadId(), result.get().getGroupRecipient());
}
} else {
super.onPostExecute(result);
Toast.makeText(activity.getApplicationContext(),
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show();
}
}
}
private static class UpdateSignalGroupV1Task extends SignalGroupTask {
private final GroupId.V1 groupId;
UpdateSignalGroupV1Task(GroupCreateActivity activity, GroupId.V1 groupId,
Bitmap avatar, String name, Set<Recipient> members)
{
super(activity, avatar, name, members);
this.groupId = groupId;
}
@Override
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
return Optional.fromNullable(GroupManager.updateGroup(activity, groupId, members, BitmapUtil.toByteArray(avatar), name));
}
@Override
protected void onPostExecute(Optional<GroupActionResult> result) {
if (result.isPresent() && result.get().getThreadId() > -1) {
if (!activity.isFinishing()) {
Intent intent = activity.getIntent();
intent.putExtra(GROUP_THREAD_EXTRA, result.get().getThreadId());
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId().toString());
activity.setResult(RESULT_OK, intent);
activity.finish();
}
} else {
super.onPostExecute(result);
Toast.makeText(activity.getApplicationContext(),
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show();
}
}
}
private static class AddMembersTask extends AsyncTask<Recipient,Void,List<AddMembersTask.Result>> {
static class Result {
Optional<Recipient> recipient;
boolean isPush;
String reason;
public Result(@Nullable Recipient recipient, boolean isPush, @Nullable String reason) {
this.recipient = Optional.fromNullable(recipient);
this.isPush = isPush;
this.reason = reason;
}
}
private GroupCreateActivity activity;
private boolean failIfNotPush;
public AddMembersTask(@NonNull GroupCreateActivity activity) {
this.activity = activity;
this.failIfNotPush = activity.groupToUpdate.isPresent();
}
@Override
protected List<Result> doInBackground(Recipient... recipients) {
final List<Result> results = new LinkedList<>();
for (Recipient recipient : recipients) {
boolean isPush = isActiveInDirectory(recipient);
if (failIfNotPush && !isPush) {
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group,
recipient.toShortString(activity))));
} else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getE164().or(""))) {
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_youre_already_in_the_group)));
} else {
results.add(new Result(recipient, isPush, null));
}
}
return results;
}
@Override
protected void onPostExecute(List<Result> results) {
if (activity.isFinishing()) return;
for (Result result : results) {
if (result.recipient.isPresent()) {
activity.getAdapter().add(result.recipient.get(), result.isPush);
} else {
Toast.makeText(activity, result.reason, Toast.LENGTH_SHORT).show();
}
}
activity.updateViewState();
}
}
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<GroupId.V1, Void, Optional<GroupData>> {
private GroupCreateActivity activity;
public FillExistingGroupInfoAsyncTask(GroupCreateActivity activity) {
super(activity,
R.string.GroupCreateActivity_loading_group_details,
R.string.please_wait);
this.activity = activity;
}
@Override
protected Optional<GroupData> doInBackground(GroupId.V1... groupIds) {
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
final Set<Recipient> existingContacts = new HashSet<>(recipients.size());
existingContacts.addAll(recipients);
if (group.isPresent()) {
Bitmap avatar = null;
try {
avatar = BitmapFactory.decodeStream(AvatarHelper.getAvatar(getContext(), group.get().getRecipientId()));
} catch (IOException e) {
Log.w(TAG, "Failed to read avatar.");
}
return Optional.of(new GroupData(groupIds[0],
existingContacts,
avatar,
BitmapUtil.toByteArray(avatar),
group.get().getTitle()));
} else {
return Optional.absent();
}
}
@Override
protected void onPostExecute(Optional<GroupData> group) {
super.onPostExecute(group);
if (group.isPresent() && !activity.isFinishing()) {
activity.groupToUpdate = group;
activity.groupName.setText(group.get().name);
if (group.get().avatarBmp != null) {
activity.setAvatar(group.get().avatarBytes, group.get().avatarBmp);
}
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(activity, group.get().recipients);
adapter.setOnRecipientDeletedListener(activity);
activity.listView.setAdapter(adapter);
activity.updateViewState();
}
}
}
private <T> void setAvatar(T model, Bitmap bitmap) {
avatarBmp = bitmap;
GlideApp.with(this)
.load(model)
.circleCrop()
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(avatar);
}
private static class GroupData {
GroupId.V1 id;
Set<Recipient> recipients;
Bitmap avatarBmp;
byte[] avatarBytes;
String name;
GroupData(GroupId.V1 id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
this.id = id;
this.recipients = recipients;
this.avatarBmp = avatarBmp;
this.avatarBytes = avatarBytes;
this.name = name;
}
}
}

View File

@@ -43,7 +43,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
import java.util.concurrent.ExecutionException;
public class InviteActivity extends PassphraseRequiredActionBarActivity implements ContactSelectionListFragment.OnContactSelectedListener {
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
private ContactSelectionListFragment contactsFragment;
private EditText inviteText;
@@ -121,8 +121,9 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
}
@Override
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
updateSmsButtonText();
return true;
}
@Override

View File

@@ -0,0 +1,45 @@
package org.thoughtcrime.securesms;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.logging.Log;
/**
* Simply logs out lifecycle events.
*/
public abstract class LoggingFragment extends Fragment {
private static final String TAG = Log.tag(LoggingFragment.class);
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
logEvent("onCreate()");
super.onCreate(savedInstanceState);
}
@Override
public void onStart() {
logEvent("onStart()");
super.onStart();
}
@Override
public void onStop() {
logEvent("onStop()");
super.onStop();
}
@Override
public void onDestroy() {
logEvent("onDestroy()");
super.onDestroy();
}
private void logEvent(@NonNull String event) {
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
}
}

View File

@@ -1,13 +1,16 @@
package org.thoughtcrime.securesms;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class MainActivity extends PassphraseRequiredActionBarActivity {
public class MainActivity extends PassphraseRequiredActivity {
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final MainNavigator navigator = new MainNavigator(this);
@@ -18,6 +21,14 @@ public class MainActivity extends PassphraseRequiredActionBarActivity {
setContentView(R.layout.main_activity);
navigator.onCreate(savedInstanceState);
handleGroupLinkInIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleGroupLinkInIntent(intent);
}
@Override
@@ -42,4 +53,11 @@ public class MainActivity extends PassphraseRequiredActionBarActivity {
public @NonNull MainNavigator getNavigator() {
return navigator;
}
private void handleGroupLinkInIntent(Intent intent) {
Uri data = intent.getData();
if (data != null) {
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString());
}
}
}

View File

@@ -3,9 +3,8 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
public class MainFragment extends Fragment {
public class MainFragment extends LoggingFragment {
@Override
public void onAttach(@NonNull Context context) {

View File

@@ -20,10 +20,12 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -73,11 +75,12 @@ import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
/**
* Activity for displaying media attachments in-app
*/
public final class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
public final class MediaPreviewActivity extends PassphraseRequiredActivity
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
MediaRailAdapter.RailItemListener,
MediaPreviewFragment.Events
@@ -117,17 +120,20 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
private boolean showThread;
private MediaDatabase.Sorting sorting;
private @Nullable Cursor cursor = null;
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
@NonNull MediaRecord mediaRecord,
boolean leftIsRecent)
{
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId());
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, mediaRecord.getAttachment().getCaption());
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
intent.setDataAndType(attachment.getDataUri(), mediaRecord.getContentType());
return intent;
}
@@ -181,7 +187,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
private @NonNull String getTitleText(@NonNull MediaItem mediaItem) {
String from;
if (mediaItem.outgoing) from = getString(R.string.MediaPreviewActivity_you);
else if (mediaItem.recipient != null) from = mediaItem.recipient.toShortString(this);
else if (mediaItem.recipient != null) from = mediaItem.recipient.getDisplayName(this);
else from = "";
if (showThread) {
@@ -193,7 +199,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
if (threadRecipient.isLocalNumber()) {
from = getString(R.string.note_to_self);
} else {
to = threadRecipient.toShortString(this);
to = threadRecipient.getDisplayName(this);
}
} else {
to = getString(R.string.MediaPreviewActivity_you);
@@ -228,6 +234,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
restartItem = cleanupMedia();
}
@Override
protected void onDestroy() {
if (cursor != null) {
cursor.close();
cursor = null;
}
super.onDestroy();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
@@ -344,6 +359,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
mediaPager.removeAllViews();
mediaPager.setAdapter(null);
viewModel.setCursor(this, null, leftIsRecent);
return restartItem;
}
@@ -475,19 +491,46 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
@Override
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
if (data != null) {
@SuppressWarnings("ConstantConditions")
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(),this, data.first, data.second, leftIsRecent);
if (data.first == cursor) {
return;
}
if (cursor != null) {
cursor.close();
}
cursor = Objects.requireNonNull(data.first);
int mediaPosition = Objects.requireNonNull(data.second);
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(),this, cursor, mediaPosition, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
viewModel.setCursor(this, data.first, leftIsRecent);
viewModel.setCursor(this, cursor, leftIsRecent);
int item = restartItem >= 0 ? restartItem : data.second;
int item = restartItem >= 0 ? restartItem : mediaPosition;
mediaPager.setCurrentItem(item);
if (item == 0) {
viewPagerListener.onPageSelected(0);
}
cursor.registerContentObserver(new ContentObserver(new Handler(getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
onMediaChange();
}
});
} else {
mediaNotAvailable();
}
}
private void onMediaChange() {
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
if (adapter != null) {
adapter.checkMedia(mediaPager.getCurrentItem());
}
}
@@ -502,6 +545,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
return true;
}
@Override
public void mediaNotAvailable() {
Toast.makeText(this, R.string.MediaPreviewActivity_media_no_longer_available, Toast.LENGTH_LONG).show();
finish();
}
private void toggleUiVisibility() {
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
@@ -621,6 +670,11 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
public boolean hasFragmentFor(int position) {
return mediaPreviewFragment != null;
}
@Override
public void checkMedia(int currentItem) {
}
}
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
@@ -712,7 +766,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
cursor.moveToPosition(cursorPosition);
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(context, cursor);
DatabaseAttachment attachment = mediaRecord.getAttachment();
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
MediaPreviewFragment fragment = MediaPreviewFragment.newInstance(attachment, autoPlay);
mediaFragments.put(position, fragment);
@@ -734,16 +788,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
public MediaItem getMediaItemFor(int position) {
cursor.moveToPosition(getCursorPosition(position));
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
RecipientId recipientId = mediaRecord.getRecipientId();
RecipientId threadRecipientId = mediaRecord.getThreadRecipientId();
if (mediaRecord.getAttachment().getDataUri() == null) throw new AssertionError();
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
RecipientId recipientId = mediaRecord.getRecipientId();
RecipientId threadRecipientId = mediaRecord.getThreadRecipientId();
return new MediaItem(Recipient.live(recipientId).get(),
Recipient.live(threadRecipientId).get(),
mediaRecord.getAttachment(),
mediaRecord.getAttachment().getDataUri(),
attachment,
Objects.requireNonNull(attachment.getDataUri()),
mediaRecord.getContentType(),
mediaRecord.getDate(),
mediaRecord.isOutgoing());
@@ -767,6 +820,14 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
return mediaFragments.containsKey(position);
}
@Override
public void checkMedia(int position) {
MediaPreviewFragment fragment = mediaFragments.get(position);
if (fragment != null) {
fragment.checkMediaStillAvailable();
}
}
private int getCursorPosition(int position) {
if (leftIsRecent) return position;
else return cursor.getCount() - 1 - position;
@@ -805,5 +866,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
void pause(int position);
@Nullable View getPlaybackControls(int position);
boolean hasFragmentFor(int position);
void checkMedia(int currentItem);
}
}

View File

@@ -1,451 +0,0 @@
/*
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.database.Cursor;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.lang.ref.WeakReference;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
/**
* @author Jake McGinty
*/
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor> {
private final static String TAG = MessageDetailsActivity.class.getSimpleName();
public static final String MESSAGE_ID_EXTRA = "message_id";
public static final String THREAD_ID_EXTRA = "thread_id";
public static final String IS_PUSH_GROUP_EXTRA = "is_push_group";
public static final String TYPE_EXTRA = "type";
public static final String RECIPIENT_EXTRA = "recipient_id";
private GlideRequests glideRequests;
private long threadId;
private boolean isPushGroup;
private ConversationItem conversationItem;
private ViewGroup itemParent;
private View metadataContainer;
private View expiresContainer;
private TextView errorText;
private View resendButton;
private TextView sentDate;
private TextView receivedDate;
private TextView expiresInText;
private View receivedContainer;
private TextView transport;
private TextView toFrom;
private ListView recipientsList;
private LayoutInflater inflater;
private DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
private boolean running;
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
public void onCreate(Bundle bundle, boolean ready) {
setContentView(R.layout.message_details_activity);
running = true;
initializeResources();
initializeActionBar();
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
assert getSupportActionBar() != null;
getSupportActionBar().setTitle(R.string.AndroidManifest__message_details);
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
}
@Override
protected void onPause() {
super.onPause();
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
}
@Override
protected void onDestroy() {
super.onDestroy();
running = false;
}
private void initializeActionBar() {
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
LiveRecipient recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
recipient.observe(this, r -> setActionBarColor(r.getColor()));
setActionBarColor(recipient.get().getColor());
}
private void setActionBarColor(MaterialColor color) {
assert getSupportActionBar() != null;
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(color.toStatusBarColor(this));
}
}
private void initializeResources() {
inflater = LayoutInflater.from(this);
View header = inflater.inflate(R.layout.message_details_header, recipientsList, false);
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
isPushGroup = getIntent().getBooleanExtra(IS_PUSH_GROUP_EXTRA, false);
glideRequests = GlideApp.with(this);
itemParent = header.findViewById(R.id.item_container);
recipientsList = findViewById(R.id.recipients_list);
metadataContainer = header.findViewById(R.id.metadata_container);
errorText = header.findViewById(R.id.error_text);
resendButton = header.findViewById(R.id.resend_button);
sentDate = header.findViewById(R.id.sent_time);
receivedContainer = header.findViewById(R.id.received_container);
receivedDate = header.findViewById(R.id.received_time);
transport = header.findViewById(R.id.transport);
toFrom = header.findViewById(R.id.tofrom);
expiresContainer = header.findViewById(R.id.expires_container);
expiresInText = header.findViewById(R.id.expires_in);
recipientsList.setHeaderDividersEnabled(false);
recipientsList.addHeaderView(header, null, false);
}
private void updateTransport(MessageRecord messageRecord) {
final String transportText;
if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
transportText = "-";
} else if (messageRecord.isPending()) {
transportText = getString(R.string.ConversationFragment_pending);
} else if (messageRecord.isPush()) {
transportText = getString(R.string.ConversationFragment_push);
} else if (messageRecord.isMms()) {
transportText = getString(R.string.ConversationFragment_mms);
} else {
transportText = getString(R.string.ConversationFragment_sms);
}
transport.setText(transportText);
}
private void updateTime(MessageRecord messageRecord) {
sentDate.setOnLongClickListener(null);
receivedDate.setOnLongClickListener(null);
if (messageRecord.isPending() || messageRecord.isFailed()) {
sentDate.setText("-");
receivedContainer.setVisibility(View.GONE);
} else {
Locale dateLocale = dynamicLanguage.getCurrentLocale();
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this, dateLocale);
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
sentDate.setOnLongClickListener(v -> {
copyToClipboard(String.valueOf(messageRecord.getDateSent()));
return true;
});
if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived())));
receivedDate.setOnLongClickListener(v -> {
copyToClipboard(String.valueOf(messageRecord.getDateReceived()));
return true;
});
receivedContainer.setVisibility(View.VISIBLE);
} else {
receivedContainer.setVisibility(View.GONE);
}
}
}
private void updateExpirationTime(final MessageRecord messageRecord) {
if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
expiresContainer.setVisibility(View.GONE);
return;
}
expiresContainer.setVisibility(View.VISIBLE);
Util.runOnMain(new Runnable() {
@Override
public void run() {
long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted();
long remaining = messageRecord.getExpiresIn() - elapsed;
String duration = ExpirationUtil.getExpirationDisplayValue(MessageDetailsActivity.this, Math.max((int)(remaining / 1000), 1));
expiresInText.setText(duration);
if (running) {
Util.runOnMainDelayed(this, 500);
}
}
});
}
private void updateRecipients(MessageRecord messageRecord, Recipient recipient, List<RecipientDeliveryStatus> recipients) {
final int toFromRes;
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__with;
} else if (messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__to;
} else {
toFromRes = R.string.message_details_header__from;
}
toFrom.setText(toFromRes);
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, null, false);
Parcelable state = recipientsList.onSaveInstanceState();
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
recipientsList.onRestoreInstanceState(state);
}
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
if (conversationItem == null) {
if (messageRecord.isGroupAction()) {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_update, itemParent, false);
} else if (messageRecord.isOutgoing()) {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent_multimedia, itemParent, false);
} else {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received_multimedia, itemParent, false);
}
itemParent.addView(conversationItem);
}
}
private @Nullable MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
switch (type) {
case MmsSmsDatabase.SMS_TRANSPORT:
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
SmsDatabase.Reader reader = smsDatabase.readerFor(cursor);
return reader.getNext();
case MmsSmsDatabase.MMS_TRANSPORT:
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
MmsDatabase.Reader mmsReader = mmsDatabase.readerFor(cursor);
return mmsReader.getNext();
default:
throw new AssertionError("no valid message type specified");
}
}
private void copyToClipboard(@NonNull String text) {
((ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new MessageDetailsLoader(this, getIntent().getStringExtra(TYPE_EXTRA),
getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1));
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
if (messageRecord == null) {
finish();
} else {
new MessageRecipientAsyncTask(this, messageRecord).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
recipientsList.setAdapter(null);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
}
return false;
}
@SuppressLint("StaticFieldLeak")
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<RecipientDeliveryStatus>> {
private final WeakReference<Context> weakContext;
private final MessageRecord messageRecord;
MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
this.weakContext = new WeakReference<>(context);
this.messageRecord = messageRecord;
}
protected Context getContext() {
return weakContext.get();
}
@Override
public List<RecipientDeliveryStatus> doInBackground(Void... voids) {
Context context = getContext();
if (context == null) {
Log.w(TAG, "associated context is destroyed, finishing early");
return null;
}
List<RecipientDeliveryStatus> recipients = new LinkedList<>();
if (!messageRecord.getRecipient().isGroup()) {
recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), messageRecord.isUnidentified(), -1));
} else {
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
if (receiptInfoList.isEmpty()) {
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
for (Recipient recipient : group) {
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
}
} else {
for (GroupReceiptInfo info : receiptInfoList) {
recipients.add(new RecipientDeliveryStatus(Recipient.resolved(info.getRecipientId()),
getStatusFor(info.getStatus(), messageRecord.isPending(), messageRecord.isFailed()),
info.isUnidentified(),
info.getTimestamp()));
}
}
}
return recipients;
}
@Override
public void onPostExecute(List<RecipientDeliveryStatus> recipients) {
if (getContext() == null) {
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
return;
}
inflateMessageViewIfAbsent(messageRecord);
updateRecipients(messageRecord, messageRecord.getRecipient(), recipients);
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty();
if (isGroupNetworkFailure || isIndividualNetworkFailure) {
errorText.setVisibility(View.VISIBLE);
resendButton.setVisibility(View.VISIBLE);
resendButton.setOnClickListener(this::onResendClicked);
metadataContainer.setVisibility(View.GONE);
} else if (messageRecord.isFailed()) {
errorText.setVisibility(View.VISIBLE);
resendButton.setVisibility(View.GONE);
resendButton.setOnClickListener(null);
metadataContainer.setVisibility(View.GONE);
} else {
updateTransport(messageRecord);
updateTime(messageRecord);
updateExpirationTime(messageRecord);
errorText.setVisibility(View.GONE);
resendButton.setVisibility(View.GONE);
resendButton.setOnClickListener(null);
metadataContainer.setVisibility(View.VISIBLE);
}
}
private RecipientDeliveryStatus.Status getStatusFor(int deliveryReceiptCount, int readReceiptCount, boolean pending) {
if (readReceiptCount > 0) return RecipientDeliveryStatus.Status.READ;
else if (deliveryReceiptCount > 0) return RecipientDeliveryStatus.Status.DELIVERED;
else if (!pending) return RecipientDeliveryStatus.Status.SENT;
else return RecipientDeliveryStatus.Status.PENDING;
}
private RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending, boolean failed) {
if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ;
else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && failed) return RecipientDeliveryStatus.Status.UNKNOWN;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN;
throw new AssertionError();
}
private void onResendClicked(View v) {
resendButton.setVisibility(View.GONE);
SignalExecutors.BOUNDED.execute(() -> MessageSender.resend(MessageDetailsActivity.this, messageRecord));
}
}
}

View File

@@ -1,112 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
private final Context context;
private final GlideRequests glideRequests;
private final MessageRecord record;
private final List<RecipientDeliveryStatus> members;
private final boolean isPushGroup;
private final StableIdGenerator<RecipientId> idGenerator;
MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
@NonNull MessageRecord record, @NonNull List<RecipientDeliveryStatus> members,
boolean isPushGroup)
{
this.context = context;
this.glideRequests = glideRequests;
this.record = record;
this.isPushGroup = isPushGroup;
this.members = members;
this.idGenerator = new StableIdGenerator<>();
}
@Override
public int getCount() {
return members.size();
}
@Override
public Object getItem(int position) {
return members.get(position);
}
@Override
public long getItemId(int position) {
return idGenerator.getId(members.get(position).recipient.getId());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.message_recipient_list_item, parent, false);
}
RecipientDeliveryStatus member = members.get(position);
((MessageRecipientListItem)convertView).set(glideRequests, record, member, isPushGroup);
return convertView;
}
@Override
public void onMovedToScrapHeap(View view) {
((MessageRecipientListItem)view).unbind();
}
static class RecipientDeliveryStatus {
enum Status {
UNKNOWN, PENDING, SENT, DELIVERED, READ
}
private final Recipient recipient;
private final Status deliveryStatus;
private final boolean isUnidentified;
private final long timestamp;
RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, boolean isUnidentified, long timestamp) {
this.recipient = recipient;
this.deliveryStatus = deliveryStatus;
this.isUnidentified = isUnidentified;
this.timestamp = timestamp;
}
Status getDeliveryStatus() {
return deliveryStatus;
}
boolean isUnidentified() {
return isUnidentified;
}
public long getTimestamp() {
return timestamp;
}
public Recipient getRecipient() {
return recipient;
}
}
}

View File

@@ -1,200 +0,0 @@
/*
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
* A simple view to show the recipients of a message
*
* @author Jake McGinty
*/
public class MessageRecipientListItem extends RelativeLayout
implements RecipientForeverObserver
{
@SuppressWarnings("unused")
private final static String TAG = MessageRecipientListItem.class.getSimpleName();
private RecipientDeliveryStatus member;
private GlideRequests glideRequests;
private FromTextView fromView;
private TextView errorDescription;
private TextView actionDescription;
private Button conflictButton;
private AvatarImageView contactPhotoImage;
private ImageView unidentifiedDeliveryIcon;
private DeliveryStatusView deliveryStatusView;
public MessageRecipientListItem(Context context) {
super(context);
}
public MessageRecipientListItem(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
this.fromView = findViewById(R.id.from);
this.errorDescription = findViewById(R.id.error_description);
this.actionDescription = findViewById(R.id.action_description);
this.contactPhotoImage = findViewById(R.id.contact_photo_image);
this.conflictButton = findViewById(R.id.conflict_button);
this.unidentifiedDeliveryIcon = findViewById(R.id.ud_indicator);
this.deliveryStatusView = findViewById(R.id.delivery_status);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
observeMember();
}
@Override
protected void onDetachedFromWindow() {
unsubscribeFromMember();
super.onDetachedFromWindow();
}
public void set(final GlideRequests glideRequests,
final MessageRecord record,
final RecipientDeliveryStatus member,
final boolean isPushGroup)
{
unsubscribeFromMember();
this.glideRequests = glideRequests;
this.member = member;
observeMember();
fromView.setText(member.getRecipient());
contactPhotoImage.setAvatar(glideRequests, member.getRecipient(), false);
setIssueIndicators(record, isPushGroup);
unidentifiedDeliveryIcon.setVisibility(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()) && member.isUnidentified() ? VISIBLE : GONE);
}
private void observeMember() {
if (isAttachedToWindow() && member != null && member.getRecipient() != null) {
member.getRecipient().live().observeForever(this);
}
}
private void unsubscribeFromMember() {
if (member != null && member.getRecipient() != null) member.getRecipient().live().removeForeverObserver(this);
}
private void setIssueIndicators(final MessageRecord record,
final boolean isPushGroup)
{
final NetworkFailure networkFailure = getNetworkFailure(record);
final IdentityKeyMismatch keyMismatch = networkFailure == null ? getKeyMismatch(record) : null;
String errorText = "";
if (keyMismatch != null) {
conflictButton.setVisibility(View.VISIBLE);
errorText = getContext().getString(R.string.MessageDetailsRecipient_new_safety_number);
conflictButton.setOnClickListener(v -> new ConfirmIdentityDialog(getContext(), record, keyMismatch).show());
} else if ((networkFailure != null && !record.isPending()) || (!isPushGroup && record.isFailed())) {
conflictButton.setVisibility(View.GONE);
errorText = getContext().getString(R.string.MessageDetailsRecipient_failed_to_send);
} else {
if (record.isOutgoing()) {
if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.PENDING || member.getDeliveryStatus() == RecipientDeliveryStatus.Status.UNKNOWN) {
deliveryStatusView.setVisibility(View.GONE);
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.READ) {
deliveryStatusView.setRead();
deliveryStatusView.setVisibility(View.VISIBLE);
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.DELIVERED) {
deliveryStatusView.setDelivered();
deliveryStatusView.setVisibility(View.VISIBLE);
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.SENT) {
deliveryStatusView.setSent();
deliveryStatusView.setVisibility(View.VISIBLE);
}
} else {
deliveryStatusView.setVisibility(View.GONE);
}
conflictButton.setVisibility(View.GONE);
}
errorDescription.setText(errorText);
errorDescription.setVisibility(TextUtils.isEmpty(errorText) ? View.GONE : View.VISIBLE);
}
private NetworkFailure getNetworkFailure(final MessageRecord record) {
if (record.hasNetworkFailures()) {
for (final NetworkFailure failure : record.getNetworkFailures()) {
if (failure.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
return failure;
}
}
}
return null;
}
private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) {
if (record.isIdentityMismatchFailure()) {
for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) {
if (mismatch.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
return mismatch;
}
}
}
return null;
}
public void unbind() {
unsubscribeFromMember();
}
@Override
public void onRecipientChanged(@NonNull Recipient recipient) {
if (this.member != null && this.member.getRecipient().equals(recipient)) {
Log.d(TAG, "onRecipientChanged -- valid");
fromView.setText(recipient);
contactPhotoImage.setAvatar(glideRequests, recipient, false);
} else {
Log.d(TAG, "onRecipientChanged -- invalid");
}
}
}

View File

@@ -21,15 +21,24 @@ import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
/**
* Activity container for starting a new conversation.
*
@@ -51,15 +60,40 @@ public class NewConversationActivity extends ContactSelectionActivity
}
@Override
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
Recipient recipient;
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
if (recipientId.isPresent()) {
recipient = Recipient.resolved(recipientId.get());
launch(Recipient.resolved(recipientId.get()));
} else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
recipient = Recipient.external(this, number);
if (FeatureFlags.cds() && NetworkConstraint.isMet(this)) {
Log.i(TAG, "[onContactSelected] CDS enabled. Doing contact refresh.");
AlertDialog progress = SimpleProgressDialog.show(this);
SimpleTask.run(getLifecycle(), () -> {
Recipient resolved = Recipient.external(this, number);
if (!resolved.isRegistered()) {
Log.i(TAG, "[onContactSelected] Not registered. Doing a directory refresh.");
try {
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
resolved = Recipient.resolved(resolved.getId());
} catch (IOException e) {
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
}
}
return resolved;
}, resolved -> {
progress.dismiss();
launch(resolved);
});
} else {
launch(Recipient.external(this, number));
}
}
launch(recipient);
return true;
}
private void launch(Recipient recipient) {

View File

@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
*
* @author Moxie Marlinspike
*/
public abstract class PassphraseActivity extends BaseActionBarActivity {
public abstract class PassphraseActivity extends BaseActivity {
private static final String TAG = PassphraseActivity.class.getSimpleName();

View File

@@ -29,8 +29,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
public abstract class PassphraseRequiredActivity extends BaseActivity implements MasterSecretListener {
private static final String TAG = PassphraseRequiredActivity.class.getSimpleName();
public static final String LOCALE_EXTRA = "locale_extra";
public static final String NEXT_INTENT_EXTRA = "next_intent";
@@ -49,7 +49,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
@Override
protected final void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "[" + Log.tag(getClass()) + "] onCreate()");
this.networkAccess = new SignalServiceNetworkAccess(this);
onPreCreate();
@@ -69,7 +68,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
@Override
protected void onResume() {
Log.d(TAG, "[" + Log.tag(getClass()) + "] onResume()");
super.onResume();
if (networkAccess.isCensored(this)) {
@@ -77,27 +75,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
}
}
@Override
protected void onStart() {
Log.d(TAG, "[" + Log.tag(getClass()) + "] onStart()");
super.onStart();
}
@Override
protected void onPause() {
Log.d(TAG, "[" + Log.tag(getClass()) + "] onPause()");
super.onPause();
}
@Override
protected void onStop() {
Log.d(TAG, "[" + Log.tag(getClass()) + "] onStop()");
super.onStop();
}
@Override
protected void onDestroy() {
Log.d(TAG, "[" + Log.tag(getClass()) + "] onDestroy()");
super.onDestroy();
removeClearKeyReceiver(this);
}
@@ -185,7 +164,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
}
private boolean userMustCreateSignalPin() {
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().lastPinCreateFailed();
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().lastPinCreateFailed() && !SignalStore.kbsValues().hasOptedOut();
}
private boolean userMustSetProfileName() {

View File

@@ -6,7 +6,7 @@ import android.widget.Button;
import org.thoughtcrime.securesms.preferences.MmsPreferencesActivity;
public class PromptMmsActivity extends PassphraseRequiredActionBarActivity {
public class PromptMmsActivity extends PassphraseRequiredActivity {
@Override
protected void onCreate(Bundle bundle, boolean ready) {

View File

@@ -1,770 +0,0 @@
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.telephony.PhoneNumberUtils;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.preference.CheckBoxPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.color.MaterialColors;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.database.loaders.RecipientMediaLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreference;
import org.thoughtcrime.securesms.preferences.widgets.ContactPreference;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.ExecutionException;
@SuppressLint("StaticFieldLeak")
public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements LoaderManager.LoaderCallbacks<Cursor>
{
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
public static final String RECIPIENT_ID = "recipient";
private static final String PREFERENCE_MUTED = "pref_key_recipient_mute";
private static final String PREFERENCE_MESSAGE_TONE = "pref_key_recipient_ringtone";
private static final String PREFERENCE_CALL_TONE = "pref_key_recipient_call_ringtone";
private static final String PREFERENCE_MESSAGE_VIBRATE = "pref_key_recipient_vibrate";
private static final String PREFERENCE_CALL_VIBRATE = "pref_key_recipient_call_vibrate";
private static final String PREFERENCE_BLOCK = "pref_key_recipient_block";
private static final String PREFERENCE_COLOR = "pref_key_recipient_color";
private static final String PREFERENCE_IDENTITY = "pref_key_recipient_identity";
private static final String PREFERENCE_ABOUT = "pref_key_number";
private static final String PREFERENCE_CUSTOM_NOTIFICATIONS = "pref_key_recipient_custom_notifications";
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
private ImageView avatar;
private GlideRequests glideRequests;
private RecipientId recipientId;
private TextView threadPhotoRailLabel;
private ThreadPhotoRailView threadPhotoRailView;
private CollapsingToolbarLayout toolbarLayout;
public static @NonNull Intent getLaunchIntent(@NonNull Context context, @NonNull RecipientId id) {
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, id);
return intent;
}
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
public void onCreate(Bundle instanceState, boolean ready) {
setContentView(R.layout.recipient_preference_activity);
this.glideRequests = GlideApp.with(this);
this.recipientId = getIntent().getParcelableExtra(RECIPIENT_ID);
LiveRecipient recipient = Recipient.live(recipientId);
initializeToolbar();
setHeader(recipient.get());
recipient.observe(this, this::setHeader);
LoaderManager.getInstance(this).initLoader(0, null, this);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.preference_fragment);
fragment.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
}
return false;
}
private void initializeToolbar() {
this.toolbarLayout = findViewById(R.id.collapsing_toolbar);
this.avatar = findViewById(R.id.avatar);
this.threadPhotoRailView = findViewById(R.id.recent_photos);
this.threadPhotoRailLabel = findViewById(R.id.rail_label);
this.toolbarLayout.setExpandedTitleColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
this.toolbarLayout.setCollapsedTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
this.threadPhotoRailView.setListener(mediaRecord ->
startActivity(MediaPreviewActivity.intentFromMediaRecord(RecipientPreferenceActivity.this,
mediaRecord,
ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR)));
SimpleTask.run(
() -> DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipientId),
(threadId) -> {
if (threadId == null) {
Log.i(TAG, "No thread id for recipient.");
} else {
this.threadPhotoRailLabel.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(this, threadId)));
}
}
);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setLogo(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
getWindow().setStatusBarColor(Color.TRANSPARENT);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recipient_preference_root), (v, insets) -> {
ViewUtil.setTopMargin(toolbar, insets.getSystemWindowInsetTop());
return insets;
});
}
}
private void setHeader(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();
glideRequests.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this))
.error(fallbackPhoto.asCallCard(this))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.addListener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
avatar.setOnClickListener(null);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
avatar.setOnClickListener(v -> startActivity(AvatarPreviewActivity.intentFromRecipientId(RecipientPreferenceActivity.this, recipient.getId()),
AvatarPreviewActivity.createTransitionBundle(RecipientPreferenceActivity.this, avatar)));
return false;
}
})
.into(this.avatar);
if (contactPhoto == null) this.avatar.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
else this.avatar.setScaleType(ImageView.ScaleType.CENTER_CROP);
this.avatar.setBackgroundColor(recipient.getColor().toActionBarColor(this));
this.toolbarLayout.setTitle(recipient.toShortString(this));
this.toolbarLayout.setContentScrimColor(recipient.getColor().toActionBarColor(this));
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new RecipientMediaLoader(this, recipientId, RecipientMediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
if (data != null && data.getCount() > 0) {
this.threadPhotoRailLabel.setVisibility(View.VISIBLE);
this.threadPhotoRailView.setVisibility(View.VISIBLE);
} else {
this.threadPhotoRailLabel.setVisibility(View.GONE);
this.threadPhotoRailView.setVisibility(View.GONE);
}
this.threadPhotoRailView.setCursor(glideRequests, data);
Bundle bundle = new Bundle();
bundle.putParcelable(RECIPIENT_ID, recipientId);
initFragment(R.id.preference_fragment, new RecipientPreferenceFragment(), null, bundle);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
this.threadPhotoRailView.setCursor(glideRequests, null);
}
public static class RecipientPreferenceFragment extends CorrectedPreferenceFragment {
private LiveRecipient recipient;
private boolean canHaveSafetyNumber;
@Override
public void onCreate(Bundle icicle) {
Log.i(TAG, "onCreate (fragment)");
super.onCreate(icicle);
initializeRecipients();
this.canHaveSafetyNumber = recipient.get().isRegistered() && !recipient.get().isLocalNumber();
Preference customNotificationsPref = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
if (NotificationChannels.supported()) {
((SwitchPreferenceCompat) customNotificationsPref).setChecked(recipient.get().getNotificationChannel() != null);
customNotificationsPref.setOnPreferenceChangeListener(new CustomNotificationsChangedListener());
this.findPreference(PREFERENCE_MESSAGE_TONE).setDependency(PREFERENCE_CUSTOM_NOTIFICATIONS);
this.findPreference(PREFERENCE_MESSAGE_VIBRATE).setDependency(PREFERENCE_CUSTOM_NOTIFICATIONS);
if (recipient.get().getNotificationChannel() != null) {
final Context context = requireContext();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(getContext());
db.setMessageRingtone(recipient.getId(), NotificationChannels.getMessageRingtone(context, recipient.get()));
db.setMessageVibrate(recipient.getId(), NotificationChannels.getMessageVibrate(context, recipient.get()) ? VibrateState.ENABLED : VibrateState.DISABLED);
NotificationChannels.ensureCustomChannelConsistency(context);
return null;
}
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
}
} else {
customNotificationsPref.setVisible(false);
}
this.findPreference(PREFERENCE_MESSAGE_TONE)
.setOnPreferenceChangeListener(new RingtoneChangeListener(false));
this.findPreference(PREFERENCE_MESSAGE_TONE)
.setOnPreferenceClickListener(new RingtoneClickedListener(false));
this.findPreference(PREFERENCE_CALL_TONE)
.setOnPreferenceChangeListener(new RingtoneChangeListener(true));
this.findPreference(PREFERENCE_CALL_TONE)
.setOnPreferenceClickListener(new RingtoneClickedListener(true));
this.findPreference(PREFERENCE_MESSAGE_VIBRATE)
.setOnPreferenceChangeListener(new VibrateChangeListener(false));
this.findPreference(PREFERENCE_CALL_VIBRATE)
.setOnPreferenceChangeListener(new VibrateChangeListener(true));
this.findPreference(PREFERENCE_MUTED)
.setOnPreferenceClickListener(new MuteClickedListener());
this.findPreference(PREFERENCE_BLOCK)
.setOnPreferenceClickListener(new BlockClickedListener());
this.findPreference(PREFERENCE_COLOR)
.setOnPreferenceChangeListener(new ColorChangeListener());
((ContactPreference)this.findPreference(PREFERENCE_ABOUT))
.setListener(new AboutNumberClickedListener());
}
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
Log.i(TAG, "onCreatePreferences...");
addPreferencesFromResource(R.xml.recipient_preferences);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onResume() {
super.onResume();
setSummaries(recipient.get());
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 1 && resultCode == RESULT_OK && data != null) {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
findPreference(PREFERENCE_MESSAGE_TONE).getOnPreferenceChangeListener().onPreferenceChange(findPreference(PREFERENCE_MESSAGE_TONE), uri);
} else if (requestCode == 2 && resultCode == RESULT_OK && data != null) {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
findPreference(PREFERENCE_CALL_TONE).getOnPreferenceChangeListener().onPreferenceChange(findPreference(PREFERENCE_CALL_TONE), uri);
}
}
@Override
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
recyclerView.setItemAnimator(null);
recyclerView.setLayoutAnimation(null);
return recyclerView;
}
private void initializeRecipients() {
this.recipient = Recipient.live(getArguments().getParcelable(RECIPIENT_ID));
this.recipient.observe(this, this::setSummaries);
}
private void setSummaries(Recipient recipient) {
CheckBoxPreference mutePreference = (CheckBoxPreference) this.findPreference(PREFERENCE_MUTED);
Preference customPreference = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
Preference ringtoneMessagePreference = this.findPreference(PREFERENCE_MESSAGE_TONE);
Preference ringtoneCallPreference = this.findPreference(PREFERENCE_CALL_TONE);
ListPreference vibrateMessagePreference = (ListPreference) this.findPreference(PREFERENCE_MESSAGE_VIBRATE);
ListPreference vibrateCallPreference = (ListPreference) this.findPreference(PREFERENCE_CALL_VIBRATE);
ColorPickerPreference colorPreference = (ColorPickerPreference) this.findPreference(PREFERENCE_COLOR);
Preference blockPreference = this.findPreference(PREFERENCE_BLOCK);
Preference identityPreference = this.findPreference(PREFERENCE_IDENTITY);
PreferenceCategory callCategory = (PreferenceCategory)this.findPreference("call_settings");
PreferenceCategory aboutCategory = (PreferenceCategory)this.findPreference("about");
PreferenceCategory aboutDivider = (PreferenceCategory)this.findPreference("about_divider");
ContactPreference aboutPreference = (ContactPreference)this.findPreference(PREFERENCE_ABOUT);
PreferenceCategory privacyCategory = (PreferenceCategory) this.findPreference("privacy_settings");
PreferenceCategory divider = (PreferenceCategory) this.findPreference("divider");
mutePreference.setChecked(recipient.isMuted());
ringtoneMessagePreference.setSummary(ringtoneMessagePreference.isEnabled() ? getRingtoneSummary(getContext(), recipient.getMessageRingtone()) : "");
ringtoneCallPreference.setSummary(getRingtoneSummary(getContext(), recipient.getCallRingtone()));
Pair<String, Integer> vibrateMessageSummary = getVibrateSummary(getContext(), recipient.getMessageVibrate());
Pair<String, Integer> vibrateCallSummary = getVibrateSummary(getContext(), recipient.getCallVibrate());
vibrateMessagePreference.setSummary(vibrateMessagePreference.isEnabled() ? vibrateMessageSummary.first : "");
vibrateMessagePreference.setValueIndex(vibrateMessageSummary.second);
vibrateCallPreference.setSummary(vibrateCallSummary.first);
vibrateCallPreference.setValueIndex(vibrateCallSummary.second);
blockPreference.setVisible(RecipientUtil.isBlockable(recipient));
if (recipient.isBlocked()) blockPreference.setTitle(R.string.RecipientPreferenceActivity_unblock);
else blockPreference.setTitle(R.string.RecipientPreferenceActivity_block);
if (recipient.isLocalNumber()) {
mutePreference.setVisible(false);
customPreference.setVisible(false);
ringtoneMessagePreference.setVisible(false);
vibrateMessagePreference.setVisible(false);
if (identityPreference != null) identityPreference.setVisible(false);
if (aboutCategory != null) aboutCategory.setVisible(false);
if (aboutDivider != null) aboutDivider.setVisible(false);
if (privacyCategory != null) privacyCategory.setVisible(false);
if (divider != null) divider.setVisible(false);
if (callCategory != null) callCategory.setVisible(false);
}
if (recipient.isGroup()) {
if (colorPreference != null) colorPreference.setVisible(false);
if (identityPreference != null) identityPreference.setVisible(false);
if (callCategory != null) callCategory.setVisible(false);
if (aboutCategory != null) aboutCategory.setVisible(false);
if (aboutDivider != null) aboutDivider.setVisible(false);
if (divider != null) divider.setVisible(false);
} else {
colorPreference.setColors(MaterialColors.CONVERSATION_PALETTE.asConversationColorArray(requireActivity()));
colorPreference.setColor(recipient.getColor().toActionBarColor(requireActivity()));
if (FeatureFlags.profileDisplay()) {
aboutPreference.setTitle(recipient.getDisplayName(requireContext()));
aboutPreference.setSummary(recipient.resolve().getE164().or(""));
} else {
aboutPreference.setTitle(formatRecipient(recipient));
aboutPreference.setSummary(recipient.getCustomLabel());
}
aboutPreference.setState(recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED, recipient.isBlocked());
IdentityUtil.getRemoteIdentityKey(getActivity(), recipient).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
@Override
public void onSuccess(Optional<IdentityRecord> result) {
if (result.isPresent()) {
if (identityPreference != null) identityPreference.setOnPreferenceClickListener(new IdentityClickedListener(result.get()));
if (identityPreference != null) identityPreference.setEnabled(true);
} else if (canHaveSafetyNumber) {
if (identityPreference != null) identityPreference.setSummary(R.string.RecipientPreferenceActivity_available_once_a_message_has_been_sent_or_received);
if (identityPreference != null) identityPreference.setEnabled(false);
} else {
if (identityPreference != null) getPreferenceScreen().removePreference(identityPreference);
}
}
@Override
public void onFailure(ExecutionException e) {
if (identityPreference != null) getPreferenceScreen().removePreference(identityPreference);
}
});
}
if (recipient.isMmsGroup() && privacyCategory != null) {
privacyCategory.setVisible(false);
}
}
private @NonNull String formatRecipient(@NonNull Recipient recipient) {
if (recipient.getE164().isPresent()) return PhoneNumberUtils.formatNumber(recipient.requireE164());
else if (recipient.getEmail().isPresent()) return recipient.requireEmail();
else return "";
}
private @NonNull String getRingtoneSummary(@NonNull Context context, @Nullable Uri ringtone) {
if (ringtone == null) {
return context.getString(R.string.preferences__default);
} else if (ringtone.toString().isEmpty()) {
return context.getString(R.string.preferences__silent);
} else {
Ringtone tone = RingtoneManager.getRingtone(getActivity(), ringtone);
if (tone != null) {
return tone.getTitle(context);
}
}
return context.getString(R.string.preferences__default);
}
private @NonNull Pair<String, Integer> getVibrateSummary(@NonNull Context context, @NonNull VibrateState vibrateState) {
if (vibrateState == VibrateState.DEFAULT) {
return new Pair<>(context.getString(R.string.preferences__default), 0);
} else if (vibrateState == VibrateState.ENABLED) {
return new Pair<>(context.getString(R.string.RecipientPreferenceActivity_enabled), 1);
} else {
return new Pair<>(context.getString(R.string.RecipientPreferenceActivity_disabled), 2);
}
}
private class RingtoneChangeListener implements Preference.OnPreferenceChangeListener {
private final boolean calls;
RingtoneChangeListener(boolean calls) {
this.calls = calls;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final Context context = preference.getContext();
Uri value = (Uri)newValue;
Uri defaultValue;
if (calls) defaultValue = TextSecurePreferences.getCallNotificationRingtone(context);
else defaultValue = TextSecurePreferences.getNotificationRingtone(context);
if (defaultValue.equals(value)) value = null;
else if (value == null) value = Uri.EMPTY;
new AsyncTask<Uri, Void, Void>() {
@Override
protected Void doInBackground(Uri... params) {
if (calls) {
DatabaseFactory.getRecipientDatabase(context).setCallRingtone(recipient.getId(), params[0]);
} else {
DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(recipient.getId(), params[0]);
NotificationChannels.updateMessageRingtone(context, recipient.get(), params[0]);
}
return null;
}
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, value);
return false;
}
}
private class RingtoneClickedListener implements Preference.OnPreferenceClickListener {
private final boolean calls;
RingtoneClickedListener(boolean calls) {
this.calls = calls;
}
@Override
public boolean onPreferenceClick(Preference preference) {
Uri current;
Uri defaultUri;
if (calls) {
current = recipient.get().getCallRingtone();
defaultUri = TextSecurePreferences.getCallNotificationRingtone(getContext());
} else {
current = recipient.get().getMessageRingtone();
defaultUri = TextSecurePreferences.getNotificationRingtone(getContext());
}
if (current == null) current = Settings.System.DEFAULT_NOTIFICATION_URI;
else if (current.toString().isEmpty()) current = null;
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultUri);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, calls ? RingtoneManager.TYPE_RINGTONE : RingtoneManager.TYPE_NOTIFICATION);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
startActivityForResult(intent, calls ? 2 : 1);
return true;
}
}
private class VibrateChangeListener implements Preference.OnPreferenceChangeListener {
private final boolean call;
VibrateChangeListener(boolean call) {
this.call = call;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int value = Integer.parseInt((String) newValue);
final VibrateState vibrateState = VibrateState.fromId(value);
final Context context = preference.getContext();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (call) {
DatabaseFactory.getRecipientDatabase(context).setCallVibrate(recipient.getId(), vibrateState);
}
else {
DatabaseFactory.getRecipientDatabase(context).setMessageVibrate(recipient.getId(), vibrateState);
NotificationChannels.updateMessageVibrate(context, recipient.get(), vibrateState);
}
return null;
}
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
return false;
}
}
private class ColorChangeListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final Context context = getContext();
if (context == null) return true;
final int value = (Integer) newValue;
final MaterialColor selectedColor = MaterialColors.CONVERSATION_PALETTE.getByColor(context, value);
final MaterialColor currentColor = recipient.get().getColor();
if (selectedColor == null) return true;
if (preference.isEnabled() && !currentColor.equals(selectedColor)) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientDatabase(context).setColor(recipient.getId(), selectedColor);
if (recipient.get().resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(recipient.getId()));
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
return true;
}
}
private class MuteClickedListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
if (recipient.get().isMuted()) handleUnmute(preference.getContext());
else handleMute(preference.getContext());
return true;
}
private void handleMute(@NonNull Context context) {
MuteDialog.show(context, until -> setMuted(context, recipient.get(), until));
setSummaries(recipient.get());
}
private void handleUnmute(@NonNull Context context) {
setMuted(context, recipient.get(), 0);
}
private void setMuted(@NonNull final Context context, final Recipient recipient, final long until) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientDatabase(context)
.setMuted(recipient.getId(), until);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
private class IdentityClickedListener implements Preference.OnPreferenceClickListener {
private final IdentityRecord identityKey;
private IdentityClickedListener(IdentityRecord identityKey) {
Log.i(TAG, "Identity record: " + identityKey);
this.identityKey = identityKey;
}
@Override
public boolean onPreferenceClick(Preference preference) {
startActivity(VerifyIdentityActivity.newIntent(preference.getContext(), identityKey));
return true;
}
}
private class BlockClickedListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
Context context = preference.getContext();
if (recipient.get().isBlocked()) {
BlockUnblockDialog.showUnblockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.unblock(context, recipient.get()));
} else {
BlockUnblockDialog.showBlockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.block(context, recipient.get()));
}
return true;
}
}
private class AboutNumberClickedListener implements ContactPreference.Listener {
@Override
public void onMessageClicked() {
CommunicationActions.startConversation(getContext(), recipient.get(), null);
}
@Override
public void onSecureCallClicked() {
CommunicationActions.startVoiceCall(getActivity(), recipient.get());
}
@Override
public void onSecureVideoClicked() {
CommunicationActions.startVideoCall(getActivity(), recipient.get());
}
@Override
public void onInSecureCallClicked() {
CommunicationActions.startInsecureCall(requireActivity(), recipient.get());
}
@Override
public void onLongClick() {
if (recipient.get().hasE164()) {
Util.copyToClipboard(requireContext(), recipient.get().requireE164());
ServiceUtil.getVibrator(requireContext()).vibrate(250);
Toast.makeText(requireContext(), R.string.RecipientBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}
}
private class CustomNotificationsChangedListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final Context context = preference.getContext();
final boolean enabled = (boolean) newValue;
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (enabled) {
String channel = NotificationChannels.createChannelFor(context, recipient.get());
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), channel);
} else {
NotificationChannels.deleteChannelFor(context, recipient.get());
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), null);
}
return null;
}
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
return true;
}
}
}
}

View File

@@ -104,7 +104,7 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
* @author Moxie Marlinspike
*/
@SuppressLint("StaticFieldLeak")
public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements ScanListener, View.OnClickListener {
public class VerifyIdentityActivity extends PassphraseRequiredActivity implements ScanListener, View.OnClickListener {
private static final String TAG = Log.tag(VerifyIdentityActivity.class);
@@ -307,7 +307,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
byte[] localId;
byte[] remoteId;
if (FeatureFlags.uuids() && recipient.resolve().getUuid().isPresent()) {
if (FeatureFlags.verifyV2() && recipient.resolve().getUuid().isPresent()) {
Log.i(TAG, "Using UUID (version 2).");
version = 2;
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
@@ -486,7 +486,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
}
private void setRecipientText(Recipient recipient) {
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.toShortString(getContext()))));
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
description.setMovementMethod(LinkMovementMethod.getInstance());
}

View File

@@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
@@ -155,14 +156,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
@Override
protected void onUserLeaveHint() {
if (deviceSupportsPipMode()) {
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(16, 9))
.build();
setPictureInPictureParams(params);
enterPipModeIfPossible();
}
//noinspection deprecation
enterPictureInPictureMode();
@Override
public void onBackPressed() {
if (!enterPipModeIfPossible()) {
super.onBackPressed();
}
}
@@ -171,8 +171,19 @@ public class WebRtcCallActivity extends AppCompatActivity {
viewModel.setIsInPipMode(isInPictureInPictureMode);
}
private boolean enterPipModeIfPossible() {
if (isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(9, 16))
.build();
enterPictureInPictureMode(params);
return true;
}
return false;
}
private boolean isInPipMode() {
return deviceSupportsPipMode() && isInPictureInPictureMode();
return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode();
}
private void processIntent(@NonNull Intent intent) {
@@ -391,6 +402,9 @@ public class WebRtcCallActivity extends AppCompatActivity {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
if (hangupType == HangupMessage.Type.NEED_PERMISSION) {
startActivity(CalleeMustAcceptMessageRequestActivity.createIntent(this, recipient.getId()));
}
delayedFinish();
}
@@ -489,9 +503,8 @@ public class WebRtcCallActivity extends AppCompatActivity {
.show();
}
private boolean deviceSupportsPipMode() {
private boolean isSystemPipEnabledAndAvailable() {
return Build.VERSION.SDK_INT >= 26 &&
FeatureFlags.callingPip() &&
getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
}
@@ -510,27 +523,30 @@ public class WebRtcCallActivity extends AppCompatActivity {
viewModel.setRecipient(event.getRecipient());
switch (event.getState()) {
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(event); break;
case CALL_RINGING: handleCallRinging(event); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
case CALL_INCOMING: handleIncomingCall(event); break;
case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(event); break;
case CALL_RINGING: handleCallRinging(event); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
case CALL_INCOMING: handleIncomingCall(event); break;
case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
}
callScreen.setLocalRenderer(event.getLocalRenderer());
callScreen.setRemoteRenderer(event.getRemoteRenderer());
viewModel.updateFromWebRtcViewModel(event);
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
if (event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable) {
viewModel.updateFromWebRtcViewModel(event, enableVideo);
if (enableVideo) {
enableVideoIfAvailable = false;
handleSetMuteVideo(false);
}

View File

@@ -0,0 +1,83 @@
package org.thoughtcrime.securesms.animation.transitions;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.graphics.drawable.Drawable;
import android.transition.Transition;
import android.transition.TransitionValues;
import android.util.Property;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
@TargetApi(21)
abstract class CircleSquareImageViewTransition extends Transition {
private static final String CIRCLE_RATIO = "CIRCLE_RATIO";
private final boolean toCircle;
CircleSquareImageViewTransition(boolean toCircle) {
this.toCircle = toCircle;
}
@Override
public void captureStartValues(TransitionValues transitionValues) {
View view = transitionValues.view;
if (view instanceof ImageView) {
transitionValues.values.put(CIRCLE_RATIO, toCircle ? 0f : 1f);
}
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
View view = transitionValues.view;
if (view instanceof ImageView) {
transitionValues.values.put(CIRCLE_RATIO, toCircle ? 1f : 0f);
}
}
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
ImageView endImageView = (ImageView) endValues.view;
float start = (float) startValues.values.get(CIRCLE_RATIO);
float end = (float) endValues.values.get(CIRCLE_RATIO);
return ObjectAnimator.ofFloat(endImageView, new RadiusRatioProperty(), start, end);
}
static final class RadiusRatioProperty extends Property<ImageView, Float> {
private float ratio;
RadiusRatioProperty() {
super(Float.class, "circle_ratio");
}
@Override
final public void set(ImageView imageView, Float ratio) {
this.ratio = ratio;
Drawable imageViewDrawable = imageView.getDrawable();
if (imageViewDrawable instanceof RoundedBitmapDrawable) {
RoundedBitmapDrawable drawable = (RoundedBitmapDrawable) imageViewDrawable;
if (ratio > 0.95) {
drawable.setCircular(true);
} else {
drawable.setCornerRadius(Math.min(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()) * ratio * 0.5f);
}
}
}
@Override
public Float get(ImageView object) {
return ratio;
}
}
}

View File

@@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.animation.transitions;
import android.annotation.TargetApi;
import android.content.Context;
import android.util.AttributeSet;
/**
* Will only transition {@link android.widget.ImageView}s that contain a {@link androidx.core.graphics.drawable.RoundedBitmapDrawable}.
*/
@TargetApi(21)
public final class CircleToSquareImageViewTransition extends CircleSquareImageViewTransition {
public CircleToSquareImageViewTransition(Context context, AttributeSet attrs) {
super(false);
}
}

View File

@@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.animation.transitions;
import android.annotation.TargetApi;
import android.content.Context;
import android.util.AttributeSet;
/**
* Will only transition {@link android.widget.ImageView}s that contain a {@link androidx.core.graphics.drawable.RoundedBitmapDrawable}.
*/
@TargetApi(21)
public final class SquareToCircleImageViewTransition extends CircleSquareImageViewTransition {
public SquareToCircleImageViewTransition(Context context, AttributeSet attrs) {
super(true);
}
}

View File

@@ -39,6 +39,7 @@ public abstract class Attachment {
private final String fastPreflightId;
private final boolean voiceNote;
private final boolean borderless;
private final int width;
private final int height;
private final boolean quote;
@@ -59,11 +60,26 @@ public abstract class Attachment {
@NonNull
private final TransformProperties transformProperties;
public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay,
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
public Attachment(@NonNull String contentType,
int transferState,
long size,
@Nullable String fileName,
int cdnNumber,
@Nullable String location,
@Nullable String key,
@Nullable String relay,
@Nullable byte[] digest,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
int width,
int height,
boolean quote,
long uploadTimestamp,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
this.contentType = contentType;
@@ -77,6 +93,7 @@ public abstract class Attachment {
this.digest = digest;
this.fastPreflightId = fastPreflightId;
this.voiceNote = voiceNote;
this.borderless = borderless;
this.width = width;
this.height = height;
this.quote = quote;
@@ -150,6 +167,10 @@ public abstract class Attachment {
return voiceNote;
}
public boolean isBorderless() {
return borderless;
}
public int getWidth() {
return width;
}

View File

@@ -20,17 +20,34 @@ public class DatabaseAttachment extends Attachment {
private final boolean hasThumbnail;
private final int displayOrder;
public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
boolean hasData, boolean hasThumbnail,
String contentType, int transferProgress, long size,
String fileName, int cdnNumber, String location, String key, String relay,
byte[] digest, String fastPreflightId, boolean voiceNote,
int width, int height, boolean quote, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties, int displayOrder,
public DatabaseAttachment(AttachmentId attachmentId,
long mmsId,
boolean hasData,
boolean hasThumbnail,
String contentType,
int transferProgress,
long size,
String fileName,
int cdnNumber,
String location,
String key,
String relay,
byte[] digest,
String fastPreflightId,
boolean voiceNote,
boolean borderless,
int width,
int height,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties,
int displayOrder,
long uploadTimestamp)
{
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.attachmentId = attachmentId;
this.hasData = hasData;
this.hasThumbnail = hasThumbnail;

View File

@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
public class MmsNotificationAttachment extends Attachment {
public MmsNotificationAttachment(int status, long size) {
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null, null);
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, 0, 0, false, 0, null, null, null, null, null);
}
@Nullable

View File

@@ -18,14 +18,26 @@ import java.util.List;
public class PointerAttachment extends Attachment {
private PointerAttachment(@NonNull String contentType, int transferState, long size,
@Nullable String fileName, int cdnNumber, @NonNull String location,
@Nullable String key, @Nullable String relay,
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator,
private PointerAttachment(@NonNull String contentType,
int transferState,
long size,
@Nullable String fileName,
int cdnNumber,
@NonNull String location,
@Nullable String key,
@Nullable String relay,
@Nullable byte[] digest,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
int width,
int height,
long uploadTimestamp,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash)
{
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
}
@Nullable
@@ -91,21 +103,22 @@ public class PointerAttachment extends Attachment {
}
return Optional.of(new PointerAttachment(pointer.get().getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().or(0),
pointer.get().asPointer().getFileName().orNull(),
pointer.get().asPointer().getCdnNumber(),
pointer.get().asPointer().getRemoteId().toString(),
encodedKey, null,
pointer.get().asPointer().getDigest().orNull(),
fastPreflightId,
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().getWidth(),
pointer.get().asPointer().getHeight(),
pointer.get().asPointer().getUploadTimestamp(),
pointer.get().asPointer().getCaption().orNull(),
stickerLocator,
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().or(0),
pointer.get().asPointer().getFileName().orNull(),
pointer.get().asPointer().getCdnNumber(),
pointer.get().asPointer().getRemoteId().toString(),
encodedKey, null,
pointer.get().asPointer().getDigest().orNull(),
fastPreflightId,
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().isBorderless(),
pointer.get().asPointer().getWidth(),
pointer.get().asPointer().getHeight(),
pointer.get().asPointer().getUploadTimestamp(),
pointer.get().asPointer().getCaption().orNull(),
stickerLocator,
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
}
@@ -123,6 +136,7 @@ public class PointerAttachment extends Attachment {
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
null,
false,
false,
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,

View File

@@ -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, 0, 0, quote, 0, null, null, null, null, null);
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, 0, 0, quote, 0, null, null, null, null, null);
}
@Override

View File

@@ -15,20 +15,42 @@ public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri;
private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
@Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
public UriAttachment(@NonNull Uri uri,
@NonNull String contentType,
int transferState,
long size,
@Nullable String fileName,
boolean voiceNote,
boolean borderless,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
}
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
@NonNull String contentType, int transferState, long size, int width, int height,
@Nullable String fileName, @Nullable String fastPreflightId,
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
public UriAttachment(@NonNull Uri dataUri,
@Nullable Uri thumbnailUri,
@NonNull String contentType,
int transferState,
long size,
int width,
int height,
@Nullable String fileName,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.dataUri = dataUri;
this.thumbnailUri = thumbnailUri;
}

View File

@@ -18,6 +18,7 @@ import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
import org.thoughtcrime.securesms.logging.Log;
@@ -89,7 +90,11 @@ public final class AudioWaveForm {
AudioHash audioHash = attachment.getAudioHash();
if (audioHash != null) {
AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
if (audioFileInfo.waveForm.length != BAR_COUNT) {
if (audioFileInfo.waveForm.length == 0) {
Log.w(TAG, "Recovering from a wave form generation error " + cacheKey);
Util.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);
@@ -100,13 +105,19 @@ public final class AudioWaveForm {
}
try {
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
long startTime = System.currentTimeMillis();
AudioFileInfo fileInfo = generateWaveForm(uri);
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
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));
DatabaseFactory.getAttachmentDatabase(context).writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
Util.runOnMain(() -> onSuccess.accept(fileInfo));

View File

@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
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.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
@@ -40,7 +41,6 @@ import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -96,7 +96,9 @@ public class FullBackupExporter extends FullBackupBase {
for (String table : tables) {
if (table.equals(MmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMessage, null, count);
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count);
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
@@ -283,11 +285,15 @@ public class FullBackupExporter extends FullBackupBase {
return result;
}
private static boolean isNonExpiringMessage(@NonNull Cursor cursor) {
private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) {
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 &&
cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
}
private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) {
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0;
}
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
String where = MmsDatabase.ID + " = ?";

View File

@@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.color;
import android.content.Context;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -52,7 +54,7 @@ public class MaterialColors {
return null;
}
public int[] asConversationColorArray(@NonNull Context context) {
public @ColorInt int[] asConversationColorArray(@NonNull Context context) {
int[] results = new int[colors.size()];
int index = 0;

View File

@@ -16,7 +16,6 @@ import androidx.fragment.app.FragmentActivity;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RecipientPreferenceActivity;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
@@ -26,8 +25,8 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.Objects;
@@ -113,6 +112,9 @@ public final class AvatarImageView extends AppCompatImageView {
this.fallbackPhotoProvider = fallbackPhotoProvider;
}
/**
* Shows self as the actual profile picture.
*/
public void setRecipient(@NonNull Recipient recipient) {
if (recipient.isLocalNumber()) {
setAvatar(GlideApp.with(this), null, false);
@@ -122,6 +124,13 @@ public final class AvatarImageView extends AppCompatImageView {
}
}
/**
* Shows self as the note to self icon.
*/
public void setAvatar(@Nullable Recipient recipient) {
setAvatar(GlideApp.with(this), recipient, false);
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
if (recipient != null) {
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
@@ -165,7 +174,7 @@ public final class AvatarImageView extends AppCompatImageView {
if (quickContactEnabled) {
super.setOnClickListener(v -> {
Context context = getContext();
if (FeatureFlags.newGroupUI() && recipient.isPushGroup()) {
if (recipient.isPushGroup()) {
context.startActivity(ManageGroupActivity.newIntent(context, recipient.requireGroupId().requirePush()),
ManageGroupActivity.createTransitionBundle(context, this));
} else {
@@ -173,7 +182,8 @@ public final class AvatarImageView extends AppCompatImageView {
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
.show(((FragmentActivity) context).getSupportFragmentManager(), "BOTTOM");
} else {
context.startActivity(RecipientPreferenceActivity.getLaunchIntent(context, recipient.getId()));
context.startActivity(ManageRecipientActivity.newIntent(context, recipient.getId()),
ManageRecipientActivity.createTransitionBundle(context, this));
}
}
});

View File

@@ -7,23 +7,26 @@ import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.CenterInside;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
public class StickerView extends FrameLayout {
public class BorderlessImageView extends FrameLayout {
private ThumbnailView image;
private View missingShade;
public StickerView(@NonNull Context context) {
public BorderlessImageView(@NonNull Context context) {
super(context);
init();
}
public StickerView(@NonNull Context context, @Nullable AttributeSet attrs) {
public BorderlessImageView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
@@ -50,10 +53,17 @@ public class StickerView extends FrameLayout {
image.setOnLongClickListener(l);
}
public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) {
boolean showControls = stickerSlide.asAttachment().getDataUri() == null;
public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
boolean showControls = slide.asAttachment().getDataUri() == null;
if (slide.hasSticker()) {
image.setFit(new CenterInside());
image.setImageResource(glideRequests, slide, showControls, false);
} else {
image.setFit(new CenterCrop());
image.setImageResource(glideRequests, slide, showControls, false, slide.asAttachment().getWidth(), slide.asAttachment().getHeight());
}
image.setImageResource(glideRequests, stickerSlide, showControls, false);
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
}

View File

@@ -2,40 +2,57 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.core.os.BuildCompat;
import android.text.Annotation;
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import org.thoughtcrime.securesms.logging.Log;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.mention.MentionDeleter;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.List;
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
public class ComposeText extends EmojiEditText {
private CharSequence hint;
private SpannableString subHint;
private CharSequence combinedHint;
private MentionRendererDelegate mentionRendererDelegate;
private MentionValidatorWatcher mentionValidatorWatcher;
@Nullable private InputPanel.MediaListener mediaListener;
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
@Nullable private MentionQueryChangedListener mentionQueryChangedListener;
public ComposeText(Context context) {
super(context);
@@ -52,34 +69,72 @@ public class ComposeText extends EmojiEditText {
initialize();
}
public String getTextTrimmed(){
return getText().toString().trim();
/**
* Trims and returns text while preserving potential spans like {@link MentionAnnotation}.
*/
public @NonNull CharSequence getTextTrimmed() {
Editable text = getText();
if (text == null) {
return "";
}
return StringUtil.trimSequence(text);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!TextUtils.isEmpty(hint)) {
if (!TextUtils.isEmpty(subHint)) {
setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint))
.append("\n")
.append(ellipsizeToWidth(subHint)));
} else {
setHint(ellipsizeToWidth(hint));
}
if (!TextUtils.isEmpty(combinedHint)) {
setHint(combinedHint);
}
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
protected void onSelectionChanged(int selectionStart, int selectionEnd) {
super.onSelectionChanged(selectionStart, selectionEnd);
if (FeatureFlags.mentions() && getText() != null) {
boolean selectionChanged = changeSelectionForPartialMentions(getText(), selectionStart, selectionEnd);
if (selectionChanged) {
return;
}
if (selectionStart == selectionEnd) {
doAfterCursorChange(getText());
} else {
updateQuery(null);
}
}
if (cursorPositionChangedListener != null) {
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
cursorPositionChangedListener.onCursorPositionChanged(selectionStart, selectionEnd);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (getText() != null && getLayout() != null) {
int checkpoint = canvas.save();
// Clip using same logic as TextView drawing
int maxScrollY = getLayout().getHeight() - getBottom() - getTop() - getCompoundPaddingBottom() - getCompoundPaddingTop();
float clipLeft = getCompoundPaddingLeft() + getScrollX();
float clipTop = (getScrollY() == 0) ? 0 : getExtendedPaddingTop() + getScrollY();
float clipRight = getRight() - getLeft() - getCompoundPaddingRight() + getScrollX();
float clipBottom = getBottom() - getTop() + getScrollY() - ((getScrollY() == maxScrollY) ? 0 : getExtendedPaddingBottom());
canvas.clipRect(clipLeft - 10, clipTop, clipRight + 10, clipBottom);
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
try {
mentionRendererDelegate.draw(canvas, getText(), getLayout());
} finally {
canvas.restoreToCount(checkpoint);
}
}
super.onDraw(canvas);
}
private CharSequence ellipsizeToWidth(CharSequence text) {
return TextUtils.ellipsize(text,
getPaint(),
@@ -88,25 +143,25 @@ public class ComposeText extends EmojiEditText {
}
public void setHint(@NonNull String hint, @Nullable CharSequence subHint) {
this.hint = hint;
if (subHint != null) {
this.subHint = new SpannableString(subHint);
this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
Spannable subHintSpannable = new SpannableString(subHint);
subHintSpannable.setSpan(new RelativeSizeSpan(0.5f), 0, subHintSpannable.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
combinedHint = new SpannableStringBuilder().append(ellipsizeToWidth(hint))
.append("\n")
.append(ellipsizeToWidth(subHintSpannable));
} else {
this.subHint = null;
combinedHint = ellipsizeToWidth(hint);
}
if (this.subHint != null) {
super.setHint(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint))
.append("\n")
.append(ellipsizeToWidth(this.subHint)));
} else {
super.setHint(ellipsizeToWidth(this.hint));
}
super.setHint(combinedHint);
}
public void appendInvite(String invite) {
if (getText() == null) {
return;
}
if (!TextUtils.isEmpty(getText()) && !getText().toString().equals(" ")) {
append(" ");
}
@@ -119,13 +174,22 @@ public class ComposeText extends EmojiEditText {
this.cursorPositionChangedListener = listener;
}
public void setMentionQueryChangedListener(@Nullable MentionQueryChangedListener listener) {
this.mentionQueryChangedListener = listener;
}
public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
if (FeatureFlags.mentions()) {
mentionValidatorWatcher.setMentionValidator(mentionValidator);
}
}
private boolean isLandscape() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
public void setTransport(TransportOption transport) {
final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext());
final boolean isIncognito = TextSecurePreferences.isIncognitoKeyboardEnabled(getContext());
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
int inputType = getInputType();
@@ -137,12 +201,12 @@ public class ComposeText extends EmojiEditText {
inputType = (inputType & ~InputType.TYPE_MASK_VARIATION) | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE;
}
setInputType(inputType);
setImeOptions(imeOptions);
setHint(transport.getComposeHint(),
transport.getSimName().isPresent()
? getContext().getString(R.string.conversation_activity__from_sim_name, transport.getSimName().get())
: null);
setInputType(inputType);
}
@Override
@@ -165,13 +229,131 @@ public class ComposeText extends EmojiEditText {
this.mediaListener = mediaListener;
}
public boolean hasMentions() {
Editable text = getText();
if (text != null) {
return !MentionAnnotation.getMentionAnnotations(text).isEmpty();
}
return false;
}
public @NonNull List<Mention> getMentions() {
return MentionAnnotation.getMentionsFromAnnotations(getText());
}
private void initialize() {
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
setImeOptions(getImeOptions() | 16777216);
}
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ThemeUtil.getThemedColor(getContext(), R.attr.conversation_mention_background_color));
if (FeatureFlags.mentions()) {
addTextChangedListener(new MentionDeleter());
mentionValidatorWatcher = new MentionValidatorWatcher();
addTextChangedListener(mentionValidatorWatcher);
}
}
private boolean changeSelectionForPartialMentions(@NonNull Spanned spanned, int selectionStart, int selectionEnd) {
Annotation[] annotations = spanned.getSpans(0, spanned.length(), Annotation.class);
for (Annotation annotation : annotations) {
if (MentionAnnotation.isMentionAnnotation(annotation)) {
int spanStart = spanned.getSpanStart(annotation);
int spanEnd = spanned.getSpanEnd(annotation);
boolean startInMention = selectionStart > spanStart && selectionStart < spanEnd;
boolean endInMention = selectionEnd > spanStart && selectionEnd < spanEnd;
if (startInMention || endInMention) {
if (selectionStart == selectionEnd) {
setSelection(spanEnd, spanEnd);
} else {
int newStart = startInMention ? spanStart : selectionStart;
int newEnd = endInMention ? spanEnd : selectionEnd;
setSelection(newStart, newEnd);
}
return true;
}
}
}
return false;
}
private void doAfterCursorChange(@NonNull Editable text) {
if (enoughToFilter(text)) {
performFiltering(text);
} else {
updateQuery(null);
}
}
private void performFiltering(@NonNull Editable text) {
int end = getSelectionEnd();
int start = findQueryStart(text, end);
CharSequence query = text.subSequence(start, end);
updateQuery(query.toString());
}
private void updateQuery(@Nullable String query) {
if (mentionQueryChangedListener != null) {
mentionQueryChangedListener.onQueryChanged(query);
}
}
private boolean enoughToFilter(@NonNull Editable text) {
int end = getSelectionEnd();
if (end < 0) {
return false;
}
return findQueryStart(text, end) != -1;
}
public void replaceTextWithMention(@NonNull String displayName, @NonNull RecipientId recipientId) {
Editable text = getText();
if (text == null) {
return;
}
clearComposingText();
int end = getSelectionEnd();
int start = findQueryStart(text, end) - 1;
text.replace(start, end, createReplacementToken(displayName, recipientId));
}
private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull RecipientId recipientId) {
SpannableStringBuilder builder = new SpannableStringBuilder().append(MENTION_STARTER);
if (text instanceof Spanned) {
SpannableString spannableString = new SpannableString(text + " ");
TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, spannableString, 0);
builder.append(spannableString);
} else {
builder.append(text).append(" ");
}
builder.setSpan(MentionAnnotation.mentionAnnotationForRecipientId(recipientId), 0, builder.length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return builder;
}
private int findQueryStart(@NonNull CharSequence text, int inputCursorPosition) {
if (inputCursorPosition == 0) {
return -1;
}
int delimiterSearchIndex = inputCursorPosition - 1;
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != MENTION_STARTER && text.charAt(delimiterSearchIndex) != ' ')) {
delimiterSearchIndex--;
}
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == MENTION_STARTER) {
return delimiterSearchIndex + 1;
}
return -1;
}
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR2)
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
private static final String TAG = CommitContentListener.class.getSimpleName();
@@ -184,7 +366,7 @@ public class ComposeText extends EmojiEditText {
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
if (BuildCompat.isAtLeastNMR1() && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
if (Build.VERSION.SDK_INT >= 25 && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
} catch (Exception e) {
@@ -207,4 +389,8 @@ public class ComposeText extends EmojiEditText {
public interface CursorPositionChangedListener {
void onCursorPositionChanged(int start, int end);
}
public interface MentionQueryChangedListener {
void onQueryChanged(@Nullable String query);
}
}

View File

@@ -6,14 +6,15 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -94,9 +95,17 @@ public class ConversationItemFooter extends LinearLayout {
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
dateView.forceLayout();
if (messageRecord.isFailed()) {
dateView.setText(R.string.ConversationItem_error_not_delivered);
int errorMsg;
if (messageRecord.hasFailedWithNetworkFailures()) {
errorMsg = R.string.ConversationItem_error_network_not_delivered;
} else if (messageRecord.getRecipient().isPushGroup() && messageRecord.isIdentityMismatchFailure()) {
errorMsg = R.string.ConversationItem_error_partially_not_delivered;
} else {
errorMsg = R.string.ConversationItem_error_not_sent_tap_for_details;
}
dateView.setText(errorMsg);
} else if (messageRecord.isPendingInsecureSmsFallback()) {
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
} else {

View File

@@ -29,6 +29,7 @@ public class ConversationItemThumbnail extends FrameLayout {
private ConversationItemFooter footer;
private CornerMask cornerMask;
private Outliner outliner;
private Outliner pulseOutliner;
private boolean borderless;
public ConversationItemThumbnail(Context context) {
@@ -80,6 +81,14 @@ public class ConversationItemThumbnail extends FrameLayout {
outliner.draw(canvas);
}
}
if (pulseOutliner != null) {
pulseOutliner.draw(canvas);
}
}
public void setPulseOutliner(@NonNull Outliner outliner) {
this.pulseOutliner = outliner;
}
@Override

View File

@@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
public final class ConversationScrollToView extends FrameLayout {
private final TextView unreadCount;
private final ImageView scrollButton;
public ConversationScrollToView(@NonNull Context context) {
this(context, null);
}
public ConversationScrollToView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ConversationScrollToView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.conversation_scroll_to, this);
unreadCount = findViewById(R.id.conversation_scroll_to_count);
scrollButton = findViewById(R.id.conversation_scroll_to_button);
if (attrs != null) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ConversationScrollToView);
Drawable src = array.getDrawable(R.styleable.ConversationScrollToView_cstv_scroll_button_src);
scrollButton.setImageDrawable(src);
array.recycle();
}
}
@Override
public void setOnClickListener(@Nullable OnClickListener l) {
scrollButton.setOnClickListener(l);
}
public void setUnreadCount(int unreadCount) {
this.unreadCount.setText(formatUnreadCount(unreadCount));
this.unreadCount.setVisibility(unreadCount > 0 ? VISIBLE : GONE);
}
private @NonNull CharSequence formatUnreadCount(int unreadCount) {
return unreadCount > 999 ? "999+" : String.valueOf(unreadCount);
}
}

View File

@@ -42,7 +42,7 @@ public class FromTextView extends EmojiTextView {
}
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
String fromString = recipient.toShortString(getContext());
String fromString = recipient.getDisplayName(getContext());
int typeface;
@@ -61,19 +61,6 @@ public class FromTextView extends EmojiTextView {
if (recipient.isLocalNumber()) {
builder.append(getContext().getString(R.string.note_to_self));
} else if (!FeatureFlags.profileDisplay() && recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName().toString() + ") ");
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
profileName.setSpan(new TypefaceSpan("sans-serif-light"), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
profileName.setSpan(new ForegroundColorSpan(ResUtil.getColor(getContext(), R.attr.conversation_list_item_subject_color)), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL){
builder.append(profileName);
builder.append(fromSpan);
} else {
builder.append(fromSpan);
builder.append(profileName);
}
} else {
builder.append(fromSpan);
}

View File

@@ -2,10 +2,8 @@ package org.thoughtcrime.securesms.components;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
@@ -94,7 +92,6 @@ public class InputPanel extends LinearLayout
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@@ -160,7 +157,7 @@ public class InputPanel extends LinearLayout
public void setQuote(@NonNull GlideRequests glideRequests,
long id,
@NonNull Recipient author,
@NonNull String body,
@NonNull CharSequence body,
@NonNull SlideDeck attachments)
{
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
@@ -228,7 +225,7 @@ public class InputPanel extends LinearLayout
public Optional<QuoteModel> getQuote() {
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody(), false, quoteView.getAttachments()));
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody().toString(), false, quoteView.getAttachments(), quoteView.getMentions()));
} else {
return Optional.absent();
}
@@ -239,6 +236,11 @@ public class InputPanel extends LinearLayout
this.linkPreview.setLoading();
}
public void setLinkPreviewNoPreview() {
this.linkPreview.setVisibility(View.VISIBLE);
this.linkPreview.setNoPreview();
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull Optional<LinkPreview> preview) {
if (preview.isPresent()) {
this.linkPreview.setVisibility(View.VISIBLE);

View File

@@ -21,6 +21,9 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
import okhttp3.HttpUrl;
/**
* The view shown in the compose box that represents the state of the link preview.
*/
public class LinkPreviewView extends FrameLayout {
private static final int TYPE_CONVERSATION = 0;
@@ -33,6 +36,7 @@ public class LinkPreviewView extends FrameLayout {
private View divider;
private View closeButton;
private View spinner;
private View noPreview;
private int type;
private int defaultRadius;
@@ -60,6 +64,7 @@ public class LinkPreviewView extends FrameLayout {
divider = findViewById(R.id.linkpreview_divider);
spinner = findViewById(R.id.linkpreview_progress_wheel);
closeButton = findViewById(R.id.linkpreview_close);
noPreview = findViewById(R.id.linkpreview_no_preview);
defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
cornerMask = new CornerMask(this);
outliner = new Outliner();
@@ -102,6 +107,15 @@ public class LinkPreviewView extends FrameLayout {
site.setVisibility(GONE);
thumbnail.setVisibility(GONE);
spinner.setVisibility(VISIBLE);
noPreview.setVisibility(INVISIBLE);
}
public void setNoPreview() {
title.setVisibility(GONE);
site.setVisibility(GONE);
thumbnail.setVisibility(GONE);
spinner.setVisibility(GONE);
noPreview.setVisibility(VISIBLE);
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
@@ -109,6 +123,7 @@ public class LinkPreviewView extends FrameLayout {
site.setVisibility(VISIBLE);
thumbnail.setVisibility(VISIBLE);
spinner.setVisibility(GONE);
noPreview.setVisibility(GONE);
title.setText(linkPreview.getTitle());

View File

@@ -79,7 +79,6 @@ public class MaskView extends View {
target.getDrawingRect(drawingRect);
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
drawingRect.bottom = Math.min(drawingRect.bottom, getBottom() - getPaddingBottom());
drawingRect.top += targetParentTranslationY;
drawingRect.bottom += targetParentTranslationY;
@@ -88,6 +87,7 @@ public class MaskView extends View {
target.draw(maskCanvas);
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
canvas.drawBitmap(mask, 0, drawingRect.top, maskPaint);
mask.recycle();

View File

@@ -25,6 +25,14 @@ public class Outliner {
outlinePaint.setColor(color);
}
public void setStrokeWidth(float pixels) {
outlinePaint.setStrokeWidth(pixels);
}
public void setAlpha(int alpha) {
outlinePaint.setAlpha(alpha);
}
public void draw(Canvas canvas) {
draw(canvas, 0, canvas.getWidth(), canvas.getHeight(), 0);
}

View File

@@ -23,6 +23,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
@@ -55,7 +57,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
private long id;
private LiveRecipient author;
private String body;
private CharSequence body;
private TextView mediaDescriptionText;
private TextView missingLinkText;
private SlideDeck attachments;
@@ -147,7 +149,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
public void setQuote(GlideRequests glideRequests,
long id,
@NonNull Recipient author,
@Nullable String body,
@Nullable CharSequence body,
boolean originalMissing,
@NonNull SlideDeck attachments)
{
@@ -189,14 +191,14 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
authorView.setText(author.isLocalNumber() ? getContext().getString(R.string.QuoteView_you)
: author.toShortString(getContext()));
: author.getDisplayName(getContext()));
// We use the raw color resource because Android 4.x was struggling with tints here
quoteBarView.setImageResource(author.getColor().toQuoteBarColorResource(getContext(), outgoing));
mainView.setBackgroundColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
}
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments) {
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
bodyView.setVisibility(VISIBLE);
bodyView.setText(body == null ? "" : body);
@@ -280,11 +282,15 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
return author.get();
}
public String getBody() {
public CharSequence getBody() {
return body;
}
public List<Attachment> getAttachments() {
return attachments.asAttachments();
}
public @NonNull List<Mention> getMentions() {
return MentionAnnotation.getMentionsFromAnnotations(body);
}
}

View File

@@ -398,6 +398,10 @@ public class ThumbnailView extends FrameLayout {
getTransferControls().showProgressSpinner();
}
public void setFit(@NonNull BitmapTransformation fit) {
this.fit = fit;
}
protected void setRadius(int radius) {
this.radius = radius;
}

View File

@@ -205,6 +205,10 @@ public final class TransferControlView extends FrameLayout {
}
private void display(@Nullable final View view) {
if (current == view) {
return;
}
if (current != null) {
current.setVisibility(GONE);
}

View File

@@ -120,6 +120,10 @@ public final class WaveFormSeekBarView extends AppCompatSeekBar {
canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
canvas.scale(-1, 1, usableWidth / 2f, usableHeight / 2f);
}
for (int bar = 0; bar < data.length; bar++) {
float x = bar * (barWidth + barGap) + barWidth / 2f;
float y = data[bar] * maxHeight;

View File

@@ -37,6 +37,12 @@ public class ZoomingImageView extends FrameLayout {
private static final String TAG = ZoomingImageView.class.getSimpleName();
private static final int ZOOM_TRANSITION_DURATION = 300;
private static final float ZOOM_LEVEL_MIN = 1.0f;
private static final float ZOOM_LEVEL_MID = 1.5f;
private static final float ZOOM_LEVEL_MAX = 2.0f;
private final PhotoView photoView;
private final SubsamplingScaleImageView subsamplingImageView;
@@ -58,6 +64,12 @@ public class ZoomingImageView extends FrameLayout {
this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF);
this.photoView.setZoomTransitionDuration(ZOOM_TRANSITION_DURATION);
this.photoView.setScaleLevels(ZOOM_LEVEL_MIN, ZOOM_LEVEL_MID, ZOOM_LEVEL_MAX);
this.subsamplingImageView.setDoubleTapZoomDuration(ZOOM_TRANSITION_DURATION);
this.subsamplingImageView.setDoubleTapZoomScale(ZOOM_LEVEL_MID);
this.photoView.setOnClickListener(v -> ZoomingImageView.this.callOnClick());
this.subsamplingImageView.setOnClickListener(v -> ZoomingImageView.this.callOnClick());
}

View File

@@ -12,6 +12,7 @@ import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.ResUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
@@ -48,6 +49,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
@Override
public void onEmojiSelected(String emoji) {
recentModel.onCodePointSelected(emoji);
SignalStore.emojiValues().setPreferredVariation(emoji);
if (emojiEventListener != null) {
emojiEventListener.onEmojiSelected(emoji);

View File

@@ -2,23 +2,33 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.widget.TextViewCompat;
import androidx.appcompat.widget.AppCompatTextView;
import android.text.Annotation;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
import androidx.core.widget.TextViewCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
public class EmojiTextView extends AppCompatTextView {
@@ -35,6 +45,9 @@ public class EmojiTextView extends AppCompatTextView {
private int maxLength;
private CharSequence overflowText;
private CharSequence previousOverflowText;
private boolean renderMentions;
private MentionRendererDelegate mentionRendererDelegate;
public EmojiTextView(Context context) {
this(context, null);
@@ -48,14 +61,33 @@ public class EmojiTextView extends AppCompatTextView {
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true);
a.recycle();
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
originalFontSize = a.getDimensionPixelSize(0, 0);
a.recycle();
if (renderMentions) {
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.transparent_black_20));
}
}
@Override
protected void onDraw(Canvas canvas) {
if (renderMentions && getText() instanceof Spanned && getLayout() != null) {
int checkpoint = canvas.save();
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
try {
mentionRendererDelegate.draw(canvas, (Spanned) getText(), getLayout());
} finally {
canvas.restoreToCount(checkpoint);
}
}
super.onDraw(canvas);
}
@Override public void setText(@Nullable CharSequence text, BufferType type) {
@@ -115,7 +147,19 @@ public class EmojiTextView extends AppCompatTextView {
private void ellipsizeAnyTextForMaxLength() {
if (maxLength > 0 && getText().length() > maxLength + 1) {
SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, maxLength)).append(ELLIPSIS).append(Optional.fromNullable(overflowText).or(""));
CharSequence shortenedText = getText().subSequence(0, maxLength);
if (shortenedText instanceof Spanned) {
Spanned spanned = (Spanned) shortenedText;
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(spanned, maxLength - 1, maxLength);
if (!mentionAnnotations.isEmpty()) {
shortenedText = shortenedText.subSequence(0, spanned.getSpanStart(mentionAnnotations.get(0)));
}
}
newContent.append(shortenedText)
.append(ELLIPSIS)
.append(Util.emptyIfNull(overflowText));
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
@@ -198,4 +242,10 @@ public class EmojiTextView extends AppCompatTextView {
this.originalFontSize = TypedValue.applyDimension(unit, size, getResources().getDisplayMetrics());
super.setTextSize(unit, size);
}
public void setMentionBackgroundTint(@ColorInt int mentionBackgroundTint) {
if (renderMentions) {
mentionRendererDelegate.setTint(mentionBackgroundTint);
}
}
}

View File

@@ -10,6 +10,7 @@ import android.widget.PopupWindow;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.List;
@@ -27,7 +28,7 @@ public class EmojiVariationSelectorPopup extends PopupWindow {
this.listener = listener;
this.list = (ViewGroup) getContentView().findViewById(R.id.emoji_variation_container);
setBackgroundDrawable(null);
setBackgroundDrawable(ThemeUtil.getThemedDrawable(context, R.attr.emoji_variation_selector_background));
setOutsideTouchable(true);
if (Build.VERSION.SDK_INT >= 21) {

View File

@@ -4,6 +4,8 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
@@ -13,6 +15,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.io.IOException;
import java.util.ArrayList;
@@ -73,6 +76,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
return true;
}
@MainThread
public void onCodePointSelected(String emoji) {
recentlyUsed.remove(emoji);
recentlyUsed.add(emoji);
@@ -84,22 +88,16 @@ public class RecentEmojiPageModel implements EmojiPageModel {
}
final LinkedHashSet<String> latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try {
String serialized = JsonUtils.toJson(latestRecentlyUsed);
prefs.edit()
.putString(preferenceName, serialized)
.apply();
} catch (IOException e) {
Log.w(TAG, e);
}
return null;
SignalExecutors.BOUNDED.execute(() -> {
try {
String serialized = JsonUtils.toJson(latestRecentlyUsed);
prefs.edit()
.putString(preferenceName, serialized)
.apply();
} catch (IOException e) {
Log.w(TAG, e);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
}
private String[] toReversePrimitiveArray(@NonNull LinkedHashSet<String> emojiSet) {

View File

@@ -0,0 +1,74 @@
package org.thoughtcrime.securesms.components.mention;
import android.text.Annotation;
import android.text.Spannable;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Collections;
import java.util.List;
/**
* This wraps an Android standard {@link Annotation} so it can leverage the built in
* span parceling for copy/paste. The annotation span contains the mentioned recipient's
* id (in numerical form).
*
* Note: Do not extend Annotation or the parceling behavior will be lost.
*/
public final class MentionAnnotation {
public static final String MENTION_ANNOTATION = "mention";
private MentionAnnotation() {
}
public static Annotation mentionAnnotationForRecipientId(@NonNull RecipientId id) {
return new Annotation(MENTION_ANNOTATION, idToMentionAnnotationValue(id));
}
public static String idToMentionAnnotationValue(@NonNull RecipientId id) {
return String.valueOf(id.toLong());
}
public static boolean isMentionAnnotation(@NonNull Annotation annotation) {
return MENTION_ANNOTATION.equals(annotation.getKey());
}
public static void setMentionAnnotations(Spannable body, List<Mention> mentions) {
for (Mention mention : mentions) {
body.setSpan(MentionAnnotation.mentionAnnotationForRecipientId(mention.getRecipientId()), mention.getStart(), mention.getStart() + mention.getLength(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static @NonNull List<Mention> getMentionsFromAnnotations(@Nullable CharSequence text) {
if (text instanceof Spanned) {
Spanned spanned = (Spanned) text;
return Stream.of(getMentionAnnotations(spanned))
.map(annotation -> {
int spanStart = spanned.getSpanStart(annotation);
int spanLength = spanned.getSpanEnd(annotation) - spanStart;
return new Mention(RecipientId.from(annotation.getValue()), spanStart, spanLength);
})
.toList();
}
return Collections.emptyList();
}
public static @NonNull List<Annotation> getMentionAnnotations(@NonNull Spanned spanned) {
return getMentionAnnotations(spanned, 0, spanned.length());
}
public static @NonNull List<Annotation> getMentionAnnotations(@NonNull Spanned spanned, int start, int end) {
return Stream.of(spanned.getSpans(start, end, Annotation.class))
.filter(MentionAnnotation::isMentionAnnotation)
.toList();
}
}

View File

@@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.components.mention;
import android.text.Annotation;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import androidx.annotation.Nullable;
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
/**
* Detects if some part of the mention is being deleted, and if so, deletes the entire mention and
* span from the text view.
*/
public class MentionDeleter implements TextWatcher {
@Nullable private Annotation toDelete;
@Override
public void beforeTextChanged(CharSequence sequence, int start, int count, int after) {
if (count > 0 && sequence instanceof Spanned) {
Spanned text = (Spanned) sequence;
for (Annotation annotation : MentionAnnotation.getMentionAnnotations(text, start, start + count)) {
if (text.getSpanStart(annotation) < start && text.getSpanEnd(annotation) > start) {
toDelete = annotation;
return;
}
}
}
}
@Override
public void afterTextChanged(Editable editable) {
if (toDelete == null) {
return;
}
int toDeleteStart = editable.getSpanStart(toDelete);
int toDeleteEnd = editable.getSpanEnd(toDelete);
editable.removeSpan(toDelete);
toDelete = null;
editable.replace(toDeleteStart, toDeleteEnd, String.valueOf(MENTION_STARTER));
}
@Override
public void onTextChanged(CharSequence sequence, int start, int before, int count) { }
}

View File

@@ -0,0 +1,133 @@
package org.thoughtcrime.securesms.components.mention;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.text.Layout;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.LayoutUtil;
/**
* Handles actually drawing the mention backgrounds for a TextView.
* <p>
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
*/
public abstract class MentionRenderer {
protected final int horizontalPadding;
protected final int verticalPadding;
public MentionRenderer(int horizontalPadding, int verticalPadding) {
this.horizontalPadding = horizontalPadding;
this.verticalPadding = verticalPadding;
}
public abstract void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset);
protected int getLineTop(@NonNull Layout layout, int line) {
return LayoutUtil.getLineTopWithoutPadding(layout, line) - verticalPadding;
}
protected int getLineBottom(@NonNull Layout layout, int line) {
return LayoutUtil.getLineBottomWithoutPadding(layout, line) + verticalPadding;
}
public static final class SingleLineMentionRenderer extends MentionRenderer {
private final Drawable drawable;
public SingleLineMentionRenderer(int horizontalPadding, int verticalPadding, @NonNull Drawable drawable) {
super(horizontalPadding, verticalPadding);
this.drawable = drawable;
}
@Override
public void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset) {
int lineTop = getLineTop(layout, startLine);
int lineBottom = getLineBottom(layout, startLine);
int left = Math.min(startOffset, endOffset);
int right = Math.max(startOffset, endOffset);
drawable.setBounds(left, lineTop, right, lineBottom);
drawable.draw(canvas);
}
}
public static final class MultiLineMentionRenderer extends MentionRenderer {
private final Drawable drawableLeft;
private final Drawable drawableMid;
private final Drawable drawableRight;
public MultiLineMentionRenderer(int horizontalPadding, int verticalPadding,
@NonNull Drawable drawableLeft,
@NonNull Drawable drawableMid,
@NonNull Drawable drawableRight)
{
super(horizontalPadding, verticalPadding);
this.drawableLeft = drawableLeft;
this.drawableMid = drawableMid;
this.drawableRight = drawableRight;
}
@Override
public void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset) {
int paragraphDirection = layout.getParagraphDirection(startLine);
float lineEndOffset;
if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) {
lineEndOffset = layout.getLineLeft(startLine) - horizontalPadding;
} else {
lineEndOffset = layout.getLineRight(startLine) + horizontalPadding;
}
int lineBottom = getLineBottom(layout, startLine);
int lineTop = getLineTop(layout, startLine);
drawStart(canvas, startOffset, lineTop, (int) lineEndOffset, lineBottom);
for (int line = startLine + 1; line < endLine; line++) {
int left = (int) layout.getLineLeft(line) - horizontalPadding;
int right = (int) layout.getLineRight(line) + horizontalPadding;
lineTop = getLineTop(layout, line);
lineBottom = getLineBottom(layout, line);
drawableMid.setBounds(left, lineTop, right, lineBottom);
drawableMid.draw(canvas);
}
float lineStartOffset;
if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) {
lineStartOffset = layout.getLineRight(startLine) + horizontalPadding;
} else {
lineStartOffset = layout.getLineLeft(startLine) - horizontalPadding;
}
lineBottom = getLineBottom(layout, endLine);
lineTop = getLineTop(layout, endLine);
drawEnd(canvas, (int) lineStartOffset, lineTop, endOffset, lineBottom);
}
private void drawStart(@NonNull Canvas canvas, int start, int top, int end, int bottom) {
if (start > end) {
drawableRight.setBounds(end, top, start, bottom);
drawableRight.draw(canvas);
} else {
drawableLeft.setBounds(start, top, end, bottom);
drawableLeft.draw(canvas);
}
}
private void drawEnd(@NonNull Canvas canvas, int start, int top, int end, int bottom) {
if (start > end) {
drawableLeft.setBounds(end, top, start, bottom);
drawableLeft.draw(canvas);
} else {
drawableRight.setBounds(start, top, end, bottom);
drawableRight.draw(canvas);
}
}
}
}

View File

@@ -0,0 +1,78 @@
package org.thoughtcrime.securesms.components.mention;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.text.Annotation;
import android.text.Layout;
import android.text.Spanned;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.graphics.drawable.DrawableCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
/**
* Encapsulates the logic for determining the type of mention rendering needed (single vs multi-line) and then
* passing that information to the appropriate {@link MentionRenderer}.
* <p></p>
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
*/
public class MentionRendererDelegate {
private final MentionRenderer single;
private final MentionRenderer multi;
private final int horizontalPadding;
private final Drawable drawable;
private final Drawable drawableLeft;
private final Drawable drawableMid;
private final Drawable drawableEnd;
public MentionRendererDelegate(@NonNull Context context, @ColorInt int tint) {
this.horizontalPadding = ViewUtil.dpToPx(2);
drawable = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg), tint);
drawableLeft = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg_left), tint);
drawableMid = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg_mid), tint);
drawableEnd = DrawableUtil.tint(ContextUtil.requireDrawable(context, R.drawable.mention_text_bg_right), tint);
single = new MentionRenderer.SingleLineMentionRenderer(horizontalPadding,
0,
drawable);
multi = new MentionRenderer.MultiLineMentionRenderer(horizontalPadding,
0,
drawableLeft,
drawableMid,
drawableEnd);
}
public void draw(@NonNull Canvas canvas, @NonNull Spanned text, @NonNull Layout layout) {
Annotation[] annotations = text.getSpans(0, text.length(), Annotation.class);
for (Annotation annotation : annotations) {
if (MentionAnnotation.isMentionAnnotation(annotation)) {
int spanStart = text.getSpanStart(annotation);
int spanEnd = text.getSpanEnd(annotation);
int startLine = layout.getLineForOffset(spanStart);
int endLine = layout.getLineForOffset(spanEnd);
int startOffset = (int) (layout.getPrimaryHorizontal(spanStart) + -1 * layout.getParagraphDirection(startLine) * horizontalPadding);
int endOffset = (int) (layout.getPrimaryHorizontal(spanEnd) + layout.getParagraphDirection(endLine) * horizontalPadding);
MentionRenderer renderer = (startLine == endLine) ? single : multi;
renderer.draw(canvas, layout, startLine, endLine, startOffset, endOffset);
}
}
}
public void setTint(@ColorInt int tint) {
DrawableCompat.setTint(drawable, tint);
DrawableCompat.setTint(drawableLeft, tint);
DrawableCompat.setTint(drawableMid, tint);
DrawableCompat.setTint(drawableEnd, tint);
}
}

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.components.mention;
import android.text.Annotation;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import androidx.annotation.Nullable;
import java.util.List;
/**
* Provides a mechanism to validate mention annotations set on an edit text. This enables
* removing invalid mentions if the user mentioned isn't in the group.
*/
public class MentionValidatorWatcher implements TextWatcher {
@Nullable private List<Annotation> invalidMentionAnnotations;
@Nullable private MentionValidator mentionValidator;
@Override
public void onTextChanged(CharSequence sequence, int start, int before, int count) {
if (count > 1 && mentionValidator != null && sequence instanceof Spanned) {
Spanned span = (Spanned) sequence;
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(span, start, start + count);
if (mentionAnnotations.size() > 0) {
invalidMentionAnnotations = mentionValidator.getInvalidMentionAnnotations(mentionAnnotations);
}
}
}
@Override
public void afterTextChanged(Editable editable) {
if (invalidMentionAnnotations == null) {
return;
}
List<Annotation> invalidMentions = invalidMentionAnnotations;
invalidMentionAnnotations = null;
for (Annotation annotation : invalidMentions) {
editable.removeSpan(annotation);
}
}
public void setMentionValidator(@Nullable MentionValidator mentionValidator) {
this.mentionValidator = mentionValidator;
}
@Override
public void beforeTextChanged(CharSequence sequence, int start, int count, int after) { }
public interface MentionValidator {
List<Annotation> getInvalidMentionAnnotations(List<Annotation> mentionAnnotations);
}
}

View File

@@ -1,33 +1,17 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import org.thoughtcrime.securesms.logging.Log;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.Util;
public class ExpiredBuildReminder extends Reminder {
@SuppressWarnings("unused")
private static final String TAG = ExpiredBuildReminder.class.getSimpleName();
public ExpiredBuildReminder(final Context context) {
super(context.getString(R.string.reminder_header_expired_build),
context.getString(R.string.reminder_header_expired_build_details));
setOkListener(v -> {
try {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName())));
} catch (android.content.ActivityNotFoundException anfe) {
try {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + context.getPackageName())));
} catch (android.content.ActivityNotFoundException anfe2) {
Log.w(TAG, anfe2);
Toast.makeText(context, R.string.OutdatedBuildReminder_no_web_browser_installed, Toast.LENGTH_SHORT).show();
}
}
});
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
}
@Override

View File

@@ -1,35 +1,17 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import org.thoughtcrime.securesms.logging.Log;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.Util;
public class OutdatedBuildReminder extends Reminder {
private static final String TAG = OutdatedBuildReminder.class.getSimpleName();
public OutdatedBuildReminder(final Context context) {
super(context.getString(R.string.reminder_header_outdated_build),
getPluralsText(context));
setOkListener(v -> {
try {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName())));
} catch (ActivityNotFoundException anfe) {
try {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + context.getPackageName())));
} catch (ActivityNotFoundException anfe2) {
Log.w(TAG, anfe2);
Toast.makeText(context, R.string.OutdatedBuildReminder_no_web_browser_installed, Toast.LENGTH_LONG).show();
}
}
});
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
}
private static CharSequence getPluralsText(final Context context) {

View File

@@ -286,6 +286,7 @@ public class WebRtcCallView extends FrameLayout {
public void setStatusFromHangupType(@NonNull HangupMessage.Type hangupType) {
switch (hangupType) {
case NORMAL:
case NEED_PERMISSION:
status.setText(R.string.RedPhone_ending_call);
break;
case ACCEPTED:
@@ -306,7 +307,10 @@ public class WebRtcCallView extends FrameLayout {
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
visibleViewSet.clear();
visibleViewSet.addAll(topViews);
if (webRtcControls.displayTopViews()) {
visibleViewSet.addAll(topViews);
}
if (webRtcControls.displayIncomingCallButtons()) {
visibleViewSet.addAll(incomingCallViews);

View File

@@ -35,6 +35,7 @@ public class WebRtcCallViewModel extends ViewModel {
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
private boolean showVideoForOutgoing = false;
private long callConnectedTime = -1;
private Handler ellapsedTimeHandler = new Handler(Looper.getMainLooper());
private boolean answerWithVideoAvailable = false;
@@ -97,7 +98,7 @@ public class WebRtcCallViewModel extends ViewModel {
}
@MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel) {
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled());
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
@@ -106,6 +107,13 @@ public class WebRtcCallViewModel extends ViewModel {
}
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled());
if (enableVideo) {
showVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
showVideoForOutgoing = false;
}
updateLocalRenderState(webRtcViewModel.getState());
updateWebRtcControls(webRtcViewModel.getState(),
webRtcViewModel.getLocalCameraState().isEnabled(),
@@ -172,18 +180,19 @@ public class WebRtcCallViewModel extends ViewModel {
isRemoteVideoEnabled || isRemoteVideoOffer,
isMoreThanOneCameraAvailable,
isBluetoothAvailable,
isInPipMode.getValue() == Boolean.TRUE,
callState,
audioOutput));
}
private @NonNull WebRtcLocalRenderState getRealLocalRenderState(boolean shouldDisplayLocalVideo, @NonNull WebRtcLocalRenderState state) {
if (shouldDisplayLocalVideo) return state;
else return WebRtcLocalRenderState.GONE;
if (shouldDisplayLocalVideo || showVideoForOutgoing) return state;
else return WebRtcLocalRenderState.GONE;
}
private @NonNull WebRtcControls getRealWebRtcControls(boolean neverDisplayControls, @NonNull WebRtcControls controls) {
if (neverDisplayControls) return WebRtcControls.NONE;
else return controls;
private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
if (isInPipMode) return WebRtcControls.PIP;
else return controls;
}
private void startTimer() {

View File

@@ -5,22 +5,25 @@ import androidx.annotation.NonNull;
public final class WebRtcControls {
public static final WebRtcControls NONE = new WebRtcControls();
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, CallState.NONE, WebRtcAudioOutput.HANDSET);
private final boolean isRemoteVideoEnabled;
private final boolean isLocalVideoEnabled;
private final boolean isMoreThanOneCameraAvailable;
private final boolean isBluetoothAvailable;
private final boolean isInPipMode;
private final CallState callState;
private final WebRtcAudioOutput audioOutput;
private WebRtcControls() {
this(false, false, false, false, CallState.NONE, WebRtcAudioOutput.HANDSET);
this(false, false, false, false, false, CallState.NONE, WebRtcAudioOutput.HANDSET);
}
WebRtcControls(boolean isLocalVideoEnabled,
boolean isRemoteVideoEnabled,
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
boolean isInPipMode,
@NonNull CallState callState,
@NonNull WebRtcAudioOutput audioOutput)
{
@@ -28,6 +31,7 @@ public final class WebRtcControls {
this.isRemoteVideoEnabled = isRemoteVideoEnabled;
this.isBluetoothAvailable = isBluetoothAvailable;
this.isMoreThanOneCameraAvailable = isMoreThanOneCameraAvailable;
this.isInPipMode = isInPipMode;
this.callState = callState;
this.audioOutput = audioOutput;
}
@@ -80,6 +84,10 @@ public final class WebRtcControls {
return isOngoing() && !(displayAudioToggle() && displayCameraToggle());
}
boolean displayTopViews() {
return !isInPipMode;
}
WebRtcAudioOutput getAudioOutput() {
return audioOutput;
}

View File

@@ -274,7 +274,7 @@ public class ContactsCursorLoader extends CursorLoader {
String stringId = recipient.isGroup() ? recipient.requireGroupId().toString() : recipient.getE164().or(recipient.getEmail()).or("");
recentConversations.addRow(new Object[] { recipient.getId().serialize(),
recipient.toShortString(getContext()),
recipient.getDisplayName(getContext()),
stringId,
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",

View File

@@ -0,0 +1,69 @@
package org.thoughtcrime.securesms.contacts;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.util.List;
/**
* Activity which displays a dialog to confirm whether to turn off "Contact Joined Signal" notifications.
*/
public class TurnOffContactJoinedNotificationsActivity extends AppCompatActivity {
private final static String EXTRA_THREAD_ID = "thread_id";
public static Intent newIntent(@NonNull Context context, long threadId) {
Intent intent = new Intent(context, TurnOffContactJoinedNotificationsActivity.class);
intent.putExtra(EXTRA_THREAD_ID, threadId);
return intent;
}
@Override
protected void onResume() {
super.onResume();
new AlertDialog.Builder(this)
.setMessage(R.string.TurnOffContactJoinedNotificationsActivity__turn_off_contact_joined_signal)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
handlePositiveAction(dialog);
})
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> {
dialog.dismiss();
finish();
}))
.show();
}
private void handlePositiveAction(@NonNull DialogInterface dialog) {
SimpleTask.run(getLifecycle(), () -> {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this);
List<MessagingDatabase.MarkedMessageInfo> marked = threadDatabase.setRead(getIntent().getLongExtra(EXTRA_THREAD_ID, -1), false);
MarkReadReceiver.process(this, marked);
TextSecurePreferences.setNewContactsNotificationEnabled(this, false);
ApplicationDependencies.getMessageNotifier().updateNotification(this);
return null;
}, unused -> {
dialog.dismiss();
finish();
});
}
}

View File

@@ -2,13 +2,11 @@ package org.thoughtcrime.securesms.contacts.avatars;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import org.thoughtcrime.securesms.R;
@@ -18,7 +16,16 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
public final class GroupFallbackPhoto80 implements FallbackContactPhoto {
public final class FallbackPhoto80dp implements FallbackContactPhoto {
@DrawableRes private final int drawable80dp;
private final MaterialColor backgroundColor;
public FallbackPhoto80dp(@DrawableRes int drawable80dp, @NonNull MaterialColor backgroundColor) {
this.drawable80dp = drawable80dp;
this.backgroundColor = backgroundColor;
}
@Override
public Drawable asDrawable(Context context, int color) {
return buildDrawable(context);
@@ -40,13 +47,13 @@ public final class GroupFallbackPhoto80 implements FallbackContactPhoto {
}
private @NonNull Drawable buildDrawable(@NonNull Context context) {
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable)));
Drawable foreground = AppCompatResources.getDrawable(context, R.drawable.ic_group_80);
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate();
Drawable foreground = AppCompatResources.getDrawable(context, drawable80dp);
Drawable gradient = ThemeUtil.getThemedDrawable(context, R.attr.resource_placeholder_gradient);
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
int foregroundInset = ViewUtil.dpToPx(24);
DrawableCompat.setTint(background, MaterialColor.ULTRAMARINE.toAvatarColor(context));
DrawableCompat.setTint(background, backgroundColor.toAvatarColor(context));
drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset);

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.contacts.sync;
import androidx.annotation.NonNull;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.SetUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.UUID;
class ContactDiscoveryV1 {
private static final String TAG = ContactDiscoveryV1.class.getSimpleName();
static @NonNull DirectoryResult getDirectoryResult(@NonNull Set<String> databaseNumbers,
@NonNull Set<String> systemNumbers)
throws IOException
{
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
List<ContactTokenDetails> activeTokens = getTokens(inputResult.getNumbers());
Set<String> activeNumbers = Stream.of(activeTokens).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(activeNumbers, inputResult);
HashMap<String, UUID> uuids = new HashMap<>();
for (String number : outputResult.getNumbers()) {
uuids.put(number, null);
}
return new DirectoryResult(uuids, outputResult.getRewrites());
}
static @NonNull DirectoryResult getDirectoryResult(@NonNull String number) throws IOException {
return getDirectoryResult(Collections.singleton(number), Collections.singleton(number));
}
private static @NonNull List<ContactTokenDetails> getTokens(@NonNull Set<String> numbers) throws IOException {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
if (numbers.size() == 1) {
Optional<ContactTokenDetails> details = accountManager.getContact(numbers.iterator().next());
return details.isPresent() ? Collections.singletonList(details.get()) : Collections.emptyList();
} else {
return accountManager.getContacts(numbers);
}
}
}

View File

@@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.push.IasTrustStore;
import org.thoughtcrime.securesms.util.SetUtil;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
class ContactDiscoveryV2 {
private static final String TAG = Log.tag(ContactDiscoveryV2.class);
@WorkerThread
static DirectoryResult getDirectoryResult(@NonNull Context context,
@NonNull Set<String> databaseNumbers,
@NonNull Set<String> systemNumbers)
throws IOException
{
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
Set<String> sanitizedNumbers = sanitizeNumbers(inputResult.getNumbers());
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
KeyStore iasKeyStore = getIasKeyStore(context);
try {
Map<String, UUID> results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE);
FuzzyPhoneNumberHelper.OutputResultV2 outputResult = FuzzyPhoneNumberHelper.generateOutputV2(results, inputResult);
return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites());
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException e) {
Log.w(TAG, "Attestation error.", e);
throw new IOException(e);
}
}
static @NonNull DirectoryResult getDirectoryResult(@NonNull Context context, @NonNull String number) throws IOException {
return getDirectoryResult(context, Collections.singleton(number), Collections.singleton(number));
}
private static Set<String> sanitizeNumbers(@NonNull Set<String> numbers) {
return Stream.of(numbers).filter(number -> {
try {
return number.startsWith("+") && number.length() > 1 && number.charAt(1) != '0' && Long.parseLong(number.substring(1)) > 0;
} catch (NumberFormatException e) {
return false;
}
}).collect(Collectors.toSet());
}
private static KeyStore getIasKeyStore(@NonNull Context context) {
try {
TrustStore contactTrustStore = new IasTrustStore(context);
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(contactTrustStore.getKeyStoreInputStream(), contactTrustStore.getKeyStorePassword().toCharArray());
return keyStore;
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -1,53 +1,419 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Manages all the stuff around determining if a user is registered or not.
*/
public class DirectoryHelper {
private static final String TAG = Log.tag(DirectoryHelper.class);
@WorkerThread
public static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
if (FeatureFlags.uuids()) {
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) {
Log.w(TAG, "Have not yet set our own local number. Skipping.");
return;
}
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
Log.w(TAG, "No contact permissions. Skipping.");
return;
}
if (!SignalStore.registrationValues().isRegistrationComplete()) {
Log.w(TAG, "Registration is not yet complete. Skipping, but running a routine to possibly mark it complete.");
RegistrationUtil.maybeMarkRegistrationComplete(context);
return;
}
Stopwatch stopwatch = new Stopwatch("full");
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Set<String> databaseNumbers = sanitizeNumbers(recipientDatabase.getAllPhoneNumbers());
Set<String> systemNumbers = sanitizeNumbers(ContactAccessor.getInstance().getAllContactsWithNumbers(context));
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
} else {
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
}
stopwatch.split("network");
if (result.getNumberRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
}
Map<RecipientId, String> uuidMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers());
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
Set<RecipientId> activeIds = uuidMap.keySet();
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
.filterNot(activeNumbers::contains)
.filterNot(n -> result.getNumberRewrites().containsKey(n))
.map(recipientDatabase::getOrInsertFromE164)
.collect(Collectors.toSet());
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
newlyActiveIds.removeAll(existingSignalIds);
newlyActiveIds.retainAll(existingSystemIds);
notifyNewUsers(context, newlyActiveIds);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
}
StorageSyncHelper.scheduleSyncForDataChange();
stopwatch.split("disk");
stopwatch.stop(TAG);
}
@WorkerThread
public static RegisteredState refreshDirectoryFor(@NonNull Context context, @NonNull Recipient recipient, boolean notifyOfNewUsers) throws IOException {
RegisteredState originalRegisteredState = recipient.resolve().getRegistered();
RegisteredState newRegisteredState = null;
Stopwatch stopwatch = new Stopwatch("single");
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
RegisteredState originalRegisteredState = recipient.resolve().getRegistered();
RegisteredState newRegisteredState = null;
if (FeatureFlags.uuids()) {
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
newRegisteredState = DirectoryHelperV1.refreshDirectoryFor(context, recipient, notifyOfNewUsers);
} else {
newRegisteredState = DirectoryHelperV1.refreshDirectoryFor(context, recipient, notifyOfNewUsers);
if (recipient.hasUuid() && !recipient.hasE164()) {
boolean isRegistered = isUuidRegistered(context, recipient);
stopwatch.split("uuid-network");
if (isRegistered) {
boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), recipient.getUuid().get());
if (idChanged) {
Log.w(TAG, "ID changed during refresh by UUID.");
}
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
stopwatch.split("uuid-disk");
stopwatch.stop(TAG);
return isRegistered ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
}
if (!recipient.getE164().isPresent()) {
Log.w(TAG, "No UUID or E164?");
return RegisteredState.NOT_REGISTERED;
}
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
} else {
result = ContactDiscoveryV1.getDirectoryResult(recipient.getE164().get());
}
stopwatch.split("e164-network");
if (result.getNumberRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
}
if (result.getRegisteredNumbers().size() > 0) {
UUID uuid = result.getRegisteredNumbers().values().iterator().next();
if (uuid != null) {
boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), uuid);
if (idChanged) {
recipient = Recipient.resolved(recipientDatabase.getByUuid(uuid).get());
}
} else {
recipientDatabase.markRegistered(recipient.getId());
}
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
updateContactsDatabase(context, Collections.singletonList(recipient.getId()), false, result.getNumberRewrites());
}
newRegisteredState = result.getRegisteredNumbers().size() > 0 ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
if (newRegisteredState != originalRegisteredState) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
if (notifyOfNewUsers && newRegisteredState == RegisteredState.REGISTERED && recipient.resolve().isSystemContact()) {
notifyNewUsers(context, Collections.singletonList(recipient.getId()));
}
StorageSyncHelper.scheduleSyncForDataChange();
}
stopwatch.split("e164-disk");
stopwatch.stop(TAG);
return newRegisteredState;
}
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
return true;
} catch (ExecutionException e) {
if (e.getCause() instanceof NotFoundException) {
return false;
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new IOException(e);
}
}
private static void updateContactsDatabase(@NonNull Context context,
@NonNull Collection<RecipientId> activeIds,
boolean removeMissing,
@NonNull Map<String, String> rewrites)
{
AccountHolder account = getOrCreateSystemAccount(context);
if (account == null) {
Log.w(TAG, "Failed to create an account!");
return;
}
try {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
ContactsDatabase contactsDatabase = DatabaseFactory.getContactsDatabase(context);
List<String> activeAddresses = Stream.of(activeIds)
.map(Recipient::resolved)
.filter(Recipient::hasE164)
.map(Recipient::requireE164)
.toList();
contactsDatabase.removeDeletedRawContacts(account.getAccount());
contactsDatabase.setRegisteredUsers(account.getAccount(), activeAddresses, removeMissing);
Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context);
BulkOperationsHandle handle = recipientDatabase.beginBulkSystemContactUpdate();
try {
while (cursor != null && cursor.moveToNext()) {
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
if (isValidContactNumber(number)) {
String formattedNumber = PhoneNumberFormatter.get(context).format(number);
String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber);
RecipientId recipientId = Recipient.externalContact(context, realNumber).getId();
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));
String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
int phoneType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)));
handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, phoneType, contactUri.toString());
}
}
} finally {
handle.finish();
}
if (NotificationChannels.supported()) {
try (RecipientDatabase.RecipientReader recipients = DatabaseFactory.getRecipientDatabase(context).getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = recipients.getNext()) != null) {
NotificationChannels.updateContactChannelName(context, recipient);
}
}
}
} catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, "Failed to update contacts.", e);
}
}
private static boolean isValidContactNumber(@Nullable String number) {
return !TextUtils.isEmpty(number) && !UuidUtil.isUuid(number);
}
private static @Nullable AccountHolder getOrCreateSystemAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account[] accounts = accountManager.getAccountsByType(BuildConfig.APPLICATION_ID);
AccountHolder account;
if (accounts.length == 0) {
account = createAccount(context);
} else {
account = new AccountHolder(accounts[0], false);
}
if (account != null && !ContentResolver.getSyncAutomatically(account.getAccount(), ContactsContract.AUTHORITY)) {
ContentResolver.setSyncAutomatically(account.getAccount(), ContactsContract.AUTHORITY, true);
}
return account;
}
private static @Nullable AccountHolder createAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account account = new Account(context.getString(R.string.app_name), BuildConfig.APPLICATION_ID);
if (accountManager.addAccountExplicitly(account, null, null)) {
Log.i(TAG, "Created new account...");
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
return new AccountHolder(account, true);
} else {
Log.w(TAG, "Failed to create account!");
return null;
}
}
private static void notifyNewUsers(@NonNull Context context,
@NonNull Collection<RecipientId> newUsers)
{
if (!TextSecurePreferences.isNewContactsNotificationEnabled(context)) return;
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isLocalNumber()) {
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
if (insertResult.isPresent()) {
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour >= 9 && hour < 23) {
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), true);
} else {
Log.i(TAG, "Not notifying of a new user due to the time of day. (Hour: " + hour + ")");
}
}
}
}
}
private static Set<String> sanitizeNumbers(@NonNull Set<String> numbers) {
return Stream.of(numbers).filter(number -> {
try {
return number.startsWith("+") && number.length() > 1 && number.charAt(1) != '0' && Long.parseLong(number.substring(1)) > 0;
} catch (NumberFormatException e) {
return false;
}
}).collect(Collectors.toSet());
}
static class DirectoryResult {
private final Map<String, UUID> registeredNumbers;
private final Map<String, String> numberRewrites;
DirectoryResult(@NonNull Map<String, UUID> registeredNumbers,
@NonNull Map<String, String> numberRewrites)
{
this.registeredNumbers = registeredNumbers;
this.numberRewrites = numberRewrites;
}
@NonNull Map<String, UUID> getRegisteredNumbers() {
return registeredNumbers;
}
@NonNull Map<String, String> getNumberRewrites() {
return numberRewrites;
}
}
private static class AccountHolder {
private final boolean fresh;
private final Account account;
private AccountHolder(Account account, boolean fresh) {
this.fresh = fresh;
this.account = account;
}
@SuppressWarnings("unused")
public boolean isFresh() {
return fresh;
}
public Account getAccount() {
return account;
}
}
}

View File

@@ -1,401 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
class DirectoryHelperV1 {
private static final String TAG = DirectoryHelperV1.class.getSimpleName();
@WorkerThread
static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) return;
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) return;
List<RecipientId> newlyActiveUsers = refreshDirectory(context, ApplicationDependencies.getSignalServiceAccountManager());
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (notifyOfNewUsers) notifyNewUsers(context, newlyActiveUsers);
}
@SuppressLint("CheckResult")
private static @NonNull List<RecipientId> refreshDirectory(@NonNull Context context, @NonNull SignalServiceAccountManager accountManager) throws IOException {
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) {
return Collections.emptyList();
}
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
return Collections.emptyList();
}
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Set<String> allRecipientNumbers = recipientDatabase.getAllPhoneNumbers();
Stream<String> eligibleRecipientDatabaseContactNumbers = Stream.of(allRecipientNumbers);
Stream<String> eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context));
Set<String> eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet());
Set<String> storedNumbers = Stream.of(allRecipientNumbers).collect(Collectors.toSet());
DirectoryResult directoryResult = getDirectoryResult(context, accountManager, recipientDatabase, storedNumbers, eligibleContactNumbers);
return directoryResult.getNewlyActiveRecipients();
}
@WorkerThread
static RegisteredState refreshDirectoryFor(@NonNull Context context, @NonNull Recipient recipient, boolean notifyOfNewUsers) throws IOException {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
if (recipient.getUuid().isPresent() && !recipient.getE164().isPresent()) {
boolean isRegistered = isUuidRegistered(context, recipient);
if (isRegistered) {
recipientDatabase.markRegistered(recipient.getId(), recipient.getUuid().get());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
return isRegistered ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
}
return getRegisteredState(context, ApplicationDependencies.getSignalServiceAccountManager(), recipientDatabase, recipient);
}
private static void updateContactsDatabase(@NonNull Context context, @NonNull List<RecipientId> activeIds, boolean removeMissing, Map<String, String> rewrites) {
Optional<AccountHolder> account = getOrCreateAccount(context);
if (account.isPresent()) {
try {
List<String> activeAddresses = Stream.of(activeIds).map(Recipient::resolved).filter(Recipient::hasE164).map(Recipient::requireE164).toList();
DatabaseFactory.getContactsDatabase(context).removeDeletedRawContacts(account.get().getAccount());
DatabaseFactory.getContactsDatabase(context).setRegisteredUsers(account.get().getAccount(), activeAddresses, removeMissing);
Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context);
RecipientDatabase.BulkOperationsHandle handle = DatabaseFactory.getRecipientDatabase(context).beginBulkSystemContactUpdate();
try {
while (cursor != null && cursor.moveToNext()) {
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
if (isValidContactNumber(number)) {
String formattedNumber = PhoneNumberFormatter.get(context).format(number);
String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber);
RecipientId recipientId = Recipient.externalContact(context, realNumber).getId();
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));
String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
int phoneType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)));
handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, phoneType, contactUri.toString());
}
}
} finally {
handle.finish();
}
if (NotificationChannels.supported()) {
try (RecipientDatabase.RecipientReader recipients = DatabaseFactory.getRecipientDatabase(context).getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = recipients.getNext()) != null) {
NotificationChannels.updateContactChannelName(context, recipient);
}
}
}
} catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, "Failed to update contacts.", e);
}
}
}
private static void notifyNewUsers(@NonNull Context context,
@NonNull List<RecipientId> newUsers)
{
if (!TextSecurePreferences.isNewContactsNotificationEnabled(context)) return;
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isLocalNumber()) {
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
if (insertResult.isPresent()) {
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour >= 9 && hour < 23) {
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), true);
} else {
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), false);
}
}
}
}
}
private static Optional<AccountHolder> getOrCreateAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account[] accounts = accountManager.getAccountsByType("org.thoughtcrime.securesms");
Optional<AccountHolder> account;
if (accounts.length == 0) account = createAccount(context);
else account = Optional.of(new AccountHolder(accounts[0], false));
if (account.isPresent() && !ContentResolver.getSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY)) {
ContentResolver.setSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY, true);
}
return account;
}
private static Optional<AccountHolder> createAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account account = new Account(context.getString(R.string.app_name), "org.thoughtcrime.securesms");
if (accountManager.addAccountExplicitly(account, null, null)) {
Log.i(TAG, "Created new account...");
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
return Optional.of(new AccountHolder(account, true));
} else {
Log.w(TAG, "Failed to create account!");
return Optional.absent();
}
}
private static DirectoryResult getDirectoryResult(@NonNull Context context,
@NonNull SignalServiceAccountManager accountManager,
@NonNull RecipientDatabase recipientDatabase,
@NonNull Set<String> locallyStoredNumbers,
@NonNull Set<String> eligibleContactNumbers)
throws IOException
{
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(eligibleContactNumbers, locallyStoredNumbers);
List<ContactTokenDetails> activeTokens = accountManager.getContacts(inputResult.getNumbers());
Set<String> activeNumbers = Stream.of(activeTokens).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(activeNumbers, inputResult);
if (inputResult.getFuzzies().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Got a fuzzy number result.");
}
if (outputResult.getRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
}
recipientDatabase.updatePhoneNumbers(outputResult.getRewrites());
List<RecipientId> activeIds = new LinkedList<>();
List<RecipientId> inactiveIds = new LinkedList<>();
Set<String> inactiveContactNumbers = new HashSet<>(inputResult.getNumbers());
inactiveContactNumbers.removeAll(outputResult.getRewrites().keySet());
for (String number : outputResult.getNumbers()) {
activeIds.add(recipientDatabase.getOrInsertFromE164(number));
inactiveContactNumbers.remove(number);
}
for (String inactiveContactNumber : inactiveContactNumbers) {
inactiveIds.add(recipientDatabase.getOrInsertFromE164(inactiveContactNumber));
}
Set<RecipientId> currentActiveIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> contactIds = new HashSet<>(recipientDatabase.getSystemContacts());
List<RecipientId> newlyActiveIds = Stream.of(activeIds)
.filter(id -> !currentActiveIds.contains(id))
.filter(contactIds::contains)
.toList();
recipientDatabase.setRegistered(activeIds, inactiveIds);
updateContactsDatabase(context, activeIds, true, outputResult.getRewrites());
Set<String> activeContactNumbers = Stream.of(activeIds).map(Recipient::resolved).filter(Recipient::hasSmsAddress).map(Recipient::requireSmsAddress).collect(Collectors.toSet());
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) {
return new DirectoryResult(activeContactNumbers, newlyActiveIds);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
return new DirectoryResult(activeContactNumbers);
}
}
private static RegisteredState getRegisteredState(@NonNull Context context,
@NonNull SignalServiceAccountManager accountManager,
@NonNull RecipientDatabase recipientDatabase,
@NonNull Recipient recipient)
throws IOException
{
boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED;
boolean systemContact = recipient.isSystemContact();
Optional<ContactTokenDetails> details = Optional.absent();
Map<String, String> rewrites = new HashMap<>();
if (recipient.hasE164()) {
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(Collections.singletonList(recipient.requireE164()), recipientDatabase.getAllPhoneNumbers());
if (inputResult.getNumbers().size() > 1) {
Log.i(TAG, "[getRegisteredState] Got a fuzzy number result.");
List<ContactTokenDetails> detailList = accountManager.getContacts(inputResult.getNumbers());
Collection<String> registered = Stream.of(detailList).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(registered, inputResult);
String finalNumber = recipient.requireE164();
ContactTokenDetails detail = new ContactTokenDetails();
if (outputResult.getRewrites().size() > 0 && outputResult.getRewrites().containsKey(finalNumber)) {
Log.i(TAG, "[getRegisteredState] Need to rewrite a number.");
finalNumber = outputResult.getRewrites().get(finalNumber);
rewrites = outputResult.getRewrites();
}
detail.setNumber(finalNumber);
details = Optional.of(detail);
recipientDatabase.updatePhoneNumbers(outputResult.getRewrites());
} else {
details = accountManager.getContact(recipient.requireE164());
}
}
if (details.isPresent()) {
recipientDatabase.setRegistered(recipient.getId(), RegisteredState.REGISTERED);
if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
updateContactsDatabase(context, Util.asList(recipient.getId()), false, rewrites);
}
if (!activeUser && TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (!activeUser && systemContact && !TextSecurePreferences.getNeedsSqlCipherMigration(context)) {
notifyNewUsers(context, Collections.singletonList(recipient.getId()));
}
return RegisteredState.REGISTERED;
} else {
recipientDatabase.setRegistered(recipient.getId(), RegisteredState.NOT_REGISTERED);
return RegisteredState.NOT_REGISTERED;
}
}
private static boolean isValidContactNumber(@Nullable String number) {
return !TextUtils.isEmpty(number) && !UuidUtil.isUuid(number);
}
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
return true;
} catch (ExecutionException e) {
if (e.getCause() instanceof NotFoundException) {
return false;
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new IOException(e);
}
}
private static class DirectoryResult {
private final Set<String> numbers;
private final List<RecipientId> newlyActiveRecipients;
DirectoryResult(@NonNull Set<String> numbers) {
this(numbers, Collections.emptyList());
}
DirectoryResult(@NonNull Set<String> numbers, @NonNull List<RecipientId> newlyActiveRecipients) {
this.numbers = numbers;
this.newlyActiveRecipients = newlyActiveRecipients;
}
Set<String> getNumbers() {
return numbers;
}
List<RecipientId> getNewlyActiveRecipients() {
return newlyActiveRecipients;
}
}
private static class AccountHolder {
private final boolean fresh;
private final Account account;
private AccountHolder(Account account, boolean fresh) {
this.fresh = fresh;
this.account = account;
}
@SuppressWarnings("unused")
public boolean isFresh() {
return fresh;
}
public Account getAccount() {
return account;
}
}
}

View File

@@ -8,6 +8,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* A helper class to match a single number with multiple possible registered numbers. An example is
@@ -67,6 +68,32 @@ class FuzzyPhoneNumberHelper {
return new OutputResult(allNumbers, rewrites);
}
/**
* This should be run on the list of numbers we find out are registered with the server. Based on
* these results and our initial input set, we can decide if we need to rewrite which number we
* have stored locally.
*/
static @NonNull OutputResultV2 generateOutputV2(@NonNull Map<String, UUID> registeredNumbers, @NonNull InputResult inputResult) {
Map<String, UUID> allNumbers = new HashMap<>(registeredNumbers);
Map<String, String> rewrites = new HashMap<>();
for (Map.Entry<String, String> entry : inputResult.getFuzzies().entrySet()) {
if (registeredNumbers.containsKey(entry.getKey()) && registeredNumbers.containsKey(entry.getValue())) {
if (mxHas1(entry.getKey())) {
rewrites.put(entry.getKey(), entry.getValue());
allNumbers.remove(entry.getKey());
} else {
allNumbers.remove(entry.getValue());
}
} else if (registeredNumbers.containsKey(entry.getValue())) {
rewrites.put(entry.getKey(), entry.getValue());
allNumbers.remove(entry.getKey());
}
}
return new OutputResultV2(allNumbers, rewrites);
}
private static boolean mx(@NonNull String number) {
return number.startsWith("+52") && (number.length() == 13 || number.length() == 14);
@@ -127,4 +154,22 @@ class FuzzyPhoneNumberHelper {
return rewrites;
}
}
public static class OutputResultV2 {
private final Map<String, UUID> numbers;
private final Map<String, String> rewrites;
private OutputResultV2(@NonNull Map<String, UUID> numbers, @NonNull Map<String, String> rewrites) {
this.numbers = numbers;
this.rewrites = rewrites;
}
public @NonNull Map<String, UUID> getNumbers() {
return numbers;
}
public @NonNull Map<String, String> getRewrites() {
return rewrites;
}
}
}

View File

@@ -643,7 +643,7 @@ public class Contact implements Parcelable {
private static Attachment attachmentFromUri(@Nullable Uri uri) {
if (uri == null) return null;
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null, null, null, null, null);
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, false, null, null, null, null, null);
}
@Override

View File

@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import android.widget.TextView;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
import static org.thoughtcrime.securesms.contactshare.Contact.*;
public class ContactNameEditActivity extends PassphraseRequiredActionBarActivity {
public class ContactNameEditActivity extends PassphraseRequiredActivity {
public static final String KEY_NAME = "name";
public static final String KEY_CONTACT_INDEX = "contact_index";

View File

@@ -14,7 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.Toast;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.mms.GlideApp;
@@ -27,7 +27,7 @@ import java.util.List;
import static org.thoughtcrime.securesms.contactshare.Contact.*;
import static org.thoughtcrime.securesms.contactshare.ContactShareEditViewModel.*;
public class ContactShareEditActivity extends PassphraseRequiredActionBarActivity implements ContactShareEditAdapter.EventListener {
public class ContactShareEditActivity extends PassphraseRequiredActivity implements ContactShareEditAdapter.EventListener {
public static final String KEY_CONTACTS = "contacts";
private static final String KEY_CONTACT_URIS = "contact_uris";

View File

@@ -20,8 +20,7 @@ import android.widget.TextView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -44,7 +43,7 @@ import java.util.Map;
import static org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.*;
public class SharedContactDetailsActivity extends PassphraseRequiredActionBarActivity {
public class SharedContactDetailsActivity extends PassphraseRequiredActivity {
private static final int CODE_ADD_EDIT_CONTACT = 2323;
private static final String KEY_CONTACT = "contact";

View File

@@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.Util;
@@ -65,8 +64,8 @@ import java.util.Set;
* manager, so position 0 is at the bottom of the screen. That's why the "header" is at the bottom,
* the "footer" is at the top, and we refer to the "next" record as having a lower index.
*/
public class ConversationAdapter<V extends View & BindableConversationItem>
extends PagedListAdapter<MessageRecord, RecyclerView.ViewHolder>
public class ConversationAdapter
extends PagedListAdapter<ConversationMessage, RecyclerView.ViewHolder>
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationAdapter.StickyHeaderViewHolder>
{
@@ -89,16 +88,16 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
private final Locale locale;
private final Recipient recipient;
private final Set<MessageRecord> selected;
private final List<MessageRecord> fastRecords;
private final Set<Long> releasedFastRecords;
private final Calendar calendar;
private final MessageDigest digest;
private final Set<ConversationMessage> selected;
private final List<ConversationMessage> fastRecords;
private final Set<Long> releasedFastRecords;
private final Calendar calendar;
private final MessageDigest digest;
private String searchQuery;
private MessageRecord recordToPulseHighlight;
private View headerView;
private View footerView;
private String searchQuery;
private ConversationMessage recordToPulse;
private View headerView;
private View footerView;
ConversationAdapter(@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@@ -130,7 +129,8 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
return MESSAGE_TYPE_FOOTER;
}
MessageRecord messageRecord = getItem(position);
ConversationMessage conversationMessage = getItem(position);
MessageRecord messageRecord = (conversationMessage != null) ? conversationMessage.getMessageRecord() : null;
if (messageRecord == null) {
return MESSAGE_TYPE_PLACEHOLDER;
@@ -153,16 +153,13 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
return FOOTER_ID;
}
MessageRecord record = getItem(position);
ConversationMessage message = getItem(position);
if (record == null) {
if (message == null) {
return -1;
}
String unique = (record.isMms() ? "MMS::" : "SMS::") + record.getId();
byte[] bytes = digest.digest(unique.getBytes());
return Conversions.byteArrayToLong(bytes);
return message.getUniqueId(digest);
}
@Override
@@ -175,22 +172,23 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
case MESSAGE_TYPE_UPDATE:
long start = System.currentTimeMillis();
V itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
BindableConversationItem bindable = (BindableConversationItem) itemView;
itemView.setOnClickListener(view -> {
if (clickListener != null) {
clickListener.onItemClick(itemView.getMessageRecord());
clickListener.onItemClick(bindable.getConversationMessage());
}
});
itemView.setOnLongClickListener(view -> {
if (clickListener != null) {
clickListener.onItemLongClick(itemView, itemView.getMessageRecord());
clickListener.onItemLongClick(itemView, bindable.getConversationMessage());
}
return true;
});
itemView.setEventListener(clickListener);
bindable.setEventListener(clickListener);
Log.d(TAG, String.format(Locale.US, "Inflate time: %d ms for View type: %d", System.currentTimeMillis() - start, viewType));
return new ConversationViewHolder(itemView);
@@ -215,24 +213,24 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
case MESSAGE_TYPE_UPDATE:
ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder;
MessageRecord messageRecord = Objects.requireNonNull(getItem(position));
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
int adapterPosition = holder.getAdapterPosition();
MessageRecord previousRecord = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
MessageRecord nextRecord = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
conversationViewHolder.getView().bind(messageRecord,
Optional.fromNullable(previousRecord),
Optional.fromNullable(nextRecord),
glideRequests,
locale,
selected,
recipient,
searchQuery,
messageRecord == recordToPulseHighlight);
conversationViewHolder.getBindable().bind(conversationMessage,
Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null),
Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null),
glideRequests,
locale,
selected,
recipient,
searchQuery,
conversationMessage == recordToPulse);
if (messageRecord == recordToPulseHighlight) {
recordToPulseHighlight = null;
if (conversationMessage == recordToPulse) {
recordToPulse = null;
}
break;
case MESSAGE_TYPE_HEADER:
@@ -245,16 +243,18 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
}
@Override
public void submitList(@Nullable PagedList<MessageRecord> pagedList) {
public void submitList(@Nullable PagedList<ConversationMessage> pagedList) {
cleanFastRecords();
super.submitList(pagedList);
}
@Override
protected @Nullable MessageRecord getItem(int position) {
protected @Nullable ConversationMessage getItem(int position) {
position = hasHeader() ? position - 1 : position;
if (position < fastRecords.size()) {
if (position == -1) {
return null;
} else if (position < fastRecords.size()) {
return fastRecords.get(position);
} else {
int correctedPosition = position - fastRecords.size();
@@ -272,7 +272,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
if (holder instanceof ConversationViewHolder) {
((ConversationViewHolder) holder).getView().unbind();
((ConversationViewHolder) holder).getBindable().unbind();
} else if (holder instanceof HeaderFooterViewHolder) {
((HeaderFooterViewHolder) holder).unbind();
}
@@ -285,11 +285,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
if (position >= getItemCount()) return -1;
if (position < 0) return -1;
MessageRecord record = getItem(position);
ConversationMessage conversationMessage = getItem(position);
if (record == null) return -1;
if (conversationMessage == null) return -1;
calendar.setTime(new Date(record.getDateSent()));
calendar.setTime(new Date(conversationMessage.getMessageRecord().getDateSent()));
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
}
@@ -300,14 +300,18 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
@Override
public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position) {
MessageRecord messageRecord = Objects.requireNonNull(getItem(position));
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, messageRecord.getDateReceived()));
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateReceived()));
}
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
}
boolean hasNoConversationMessages() {
return super.getItemCount() + fastRecords.size() == 0;
}
/**
* The presence of a header may throw off the position you'd like to jump to. This will return
* an adjusted message position based on adapter state.
@@ -328,12 +332,12 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
if (position >= getItemCount()) return 0;
if (position < 0) return 0;
MessageRecord messageRecord = getItem(position);
ConversationMessage conversationMessage = getItem(position);
if (messageRecord == null || messageRecord.isOutgoing()) {
if (conversationMessage == null || conversationMessage.getMessageRecord().isOutgoing()) {
return 0;
} else {
return messageRecord.getDateReceived();
return conversationMessage.getMessageRecord().getDateReceived();
}
}
@@ -379,13 +383,13 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
}
/**
* Momentarily highlights a row at the requested position.
* Momentarily highlights a mention at the requested position.
*/
void pulseHighlightItem(int position) {
void pulseAtPosition(int position) {
if (position >= 0 && position < getItemCount()) {
int correctedPosition = isHeaderPosition(position) ? position + 1 : position;
recordToPulseHighlight = getItem(correctedPosition);
recordToPulse = getItem(correctedPosition);
notifyItemChanged(correctedPosition);
}
}
@@ -403,8 +407,8 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
* for a database change.
*/
@MainThread
void addFastRecord(MessageRecord record) {
fastRecords.add(0, record);
void addFastRecord(ConversationMessage conversationMessage) {
fastRecords.add(0, conversationMessage);
notifyDataSetChanged();
}
@@ -422,7 +426,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
/**
* Returns set of records that are selected in multi-select mode.
*/
Set<MessageRecord> getSelectedItems() {
Set<ConversationMessage> getSelectedItems() {
return new HashSet<>(selected);
}
@@ -436,11 +440,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
/**
* Toggles the selected state of a record in multi-select mode.
*/
void toggleSelection(MessageRecord record) {
if (selected.contains(record)) {
selected.remove(record);
void toggleSelection(ConversationMessage conversationMessage) {
if (selected.contains(conversationMessage)) {
selected.remove(conversationMessage);
} else {
selected.add(record);
selected.add(conversationMessage);
}
}
@@ -464,11 +468,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
Util.assertMainThread();
synchronized (releasedFastRecords) {
Iterator<MessageRecord> recordIterator = fastRecords.iterator();
while (recordIterator.hasNext()) {
long id = recordIterator.next().getId();
Iterator<ConversationMessage> messageIterator = fastRecords.iterator();
while (messageIterator.hasNext()) {
long id = messageIterator.next().getMessageRecord().getId();
if (releasedFastRecords.contains(id)) {
recordIterator.remove();
messageIterator.remove();
releasedFastRecords.remove(id);
}
}
@@ -479,7 +483,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
return headerView != null;
}
private boolean hasFooter() {
public boolean hasFooter() {
return footerView != null;
}
@@ -510,14 +514,17 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
}
}
public @Nullable ConversationMessage getLastVisibleConversationMessage(int position) {
return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0));
}
static class ConversationViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ConversationViewHolder(final @NonNull V itemView) {
public ConversationViewHolder(final @NonNull View itemView) {
super(itemView);
}
public <V extends View & BindableConversationItem> V getView() {
//noinspection unchecked
return (V)itemView;
public BindableConversationItem getBindable() {
return (BindableConversationItem) itemView;
}
}
@@ -526,7 +533,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
StickyHeaderViewHolder(View itemView) {
super(itemView);
textView = ViewUtil.findById(itemView, R.id.text);
textView = itemView.findViewById(R.id.text);
}
StickyHeaderViewHolder(TextView textView) {
@@ -567,21 +574,21 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
}
}
private static class DiffCallback extends DiffUtil.ItemCallback<MessageRecord> {
private static class DiffCallback extends DiffUtil.ItemCallback<ConversationMessage> {
@Override
public boolean areItemsTheSame(@NonNull MessageRecord oldItem, @NonNull MessageRecord newItem) {
return oldItem.isMms() == newItem.isMms() && oldItem.getId() == newItem.getId();
public boolean areItemsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
return oldItem.getMessageRecord().isMms() == newItem.getMessageRecord().isMms() && oldItem.getMessageRecord().getId() == newItem.getMessageRecord().getId();
}
@Override
public boolean areContentsTheSame(@NonNull MessageRecord oldItem, @NonNull MessageRecord newItem) {
public boolean areContentsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
// Corner rounding is not part of the model, so we can't use this yet
return false;
}
}
interface ItemClickListener extends BindableConversationItem.EventListener {
void onItemClick(MessageRecord item);
void onItemLongClick(View maskTarget, MessageRecord item);
void onItemClick(ConversationMessage item);
void onItemLongClick(View maskTarget, ConversationMessage item);
}
}

View File

@@ -7,26 +7,32 @@ final class ConversationData {
private final long threadId;
private final long lastSeen;
private final int lastSeenPosition;
private final int lastScrolledPosition;
private final boolean hasSent;
private final boolean isMessageRequestAccepted;
private final boolean hasPreMessageRequestMessages;
private final int jumpToPosition;
private final int threadSize;
ConversationData(long threadId,
long lastSeen,
int lastSeenPosition,
int lastScrolledPosition,
boolean hasSent,
boolean isMessageRequestAccepted,
boolean hasPreMessageRequestMessages,
int jumpToPosition)
int jumpToPosition,
int threadSize)
{
this.threadId = threadId;
this.lastSeen = lastSeen;
this.lastSeenPosition = lastSeenPosition;
this.hasSent = hasSent;
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.hasPreMessageRequestMessages = hasPreMessageRequestMessages;
this.jumpToPosition = jumpToPosition;
this.threadId = threadId;
this.lastSeen = lastSeen;
this.lastSeenPosition = lastSeenPosition;
this.lastScrolledPosition = lastScrolledPosition;
this.hasSent = hasSent;
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.hasPreMessageRequestMessages = hasPreMessageRequestMessages;
this.jumpToPosition = jumpToPosition;
this.threadSize = threadSize;
}
public long getThreadId() {
@@ -41,6 +47,10 @@ final class ConversationData {
return lastSeenPosition;
}
int getLastScrolledPosition() {
return lastScrolledPosition;
}
boolean hasSent() {
return hasSent;
}
@@ -57,7 +67,15 @@ final class ConversationData {
return jumpToPosition >= 0;
}
boolean shouldScrollToLastSeen() {
return lastSeenPosition > 0;
}
int getJumpToPosition() {
return jumpToPosition;
}
int getThreadSize() {
return threadSize;
}
}

View File

@@ -4,25 +4,35 @@ import android.content.Context;
import android.database.ContentObserver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.DataSource;
import androidx.paging.PositionalDataSource;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* Core data source for loading an individual conversation.
*/
class ConversationDataSource extends PositionalDataSource<MessageRecord> {
class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
private static final String TAG = Log.tag(ConversationDataSource.class);
@@ -30,16 +40,13 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
private final Context context;
private final long threadId;
private final DataUpdatedCallback dataUpdateCallback;
private ConversationDataSource(@NonNull Context context,
long threadId,
@NonNull Invalidator invalidator,
@NonNull DataUpdatedCallback dataUpdateCallback)
@NonNull Invalidator invalidator)
{
this.context = context;
this.threadId = threadId;
this.dataUpdateCallback = dataUpdateCallback;
ContentObserver contentObserver = new ContentObserver(null) {
@Override
@@ -58,7 +65,7 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
}
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<MessageRecord> callback) {
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<ConversationMessage> callback) {
long start = System.currentTimeMillis();
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
@@ -66,116 +73,94 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
int totalCount = db.getConversationCount(threadId);
int effectiveCount = params.requestedStartPosition;
if (totalCount == 0 || params.requestedStartPosition > totalCount) {
}
MentionHelper mentionHelper = new MentionHelper();
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.requestedStartPosition, params.requestedLoadSize))) {
MessageRecord record;
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
records.add(record);
mentionHelper.add(record);
effectiveCount++;
}
}
mentionHelper.fetchMentions(context);
if (!isInvalid()) {
SizeFixResult result = ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
callback.onResult(result.messages, params.requestedStartPosition, result.total);
Util.runOnMain(dataUpdateCallback::onDataUpdated);
List<ConversationMessage> items = Stream.of(result.getItems())
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
.toList();
callback.onResult(items, params.requestedStartPosition, result.getTotal());
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", actualSize: " + result.getItems().size() + ", totalCount: " + result.getTotal());
} else {
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", totalCount: " + totalCount + " -- invalidated");
}
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
}
@Override
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<MessageRecord> callback) {
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<ConversationMessage> callback) {
long start = System.currentTimeMillis();
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
List<MessageRecord> records = new ArrayList<>(params.loadSize);
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
List<MessageRecord> records = new ArrayList<>(params.loadSize);
MentionHelper mentionHelper = new MentionHelper();
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.startPosition, params.loadSize))) {
MessageRecord record;
while ((record = reader.getNext()) != null && !isInvalid()) {
records.add(record);
mentionHelper.add(record);
}
}
callback.onResult(records);
mentionHelper.fetchMentions(context);
if (!isInvalid()) {
Util.runOnMain(dataUpdateCallback::onDataUpdated);
}
List<ConversationMessage> items = Stream.of(records)
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
.toList();
callback.onResult(items);
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
}
private static @NonNull SizeFixResult ensureMultipleOfPageSize(@NonNull List<MessageRecord> records,
int startPosition,
int pageSize,
int total)
{
if (records.size() + startPosition == total || (records.size() != 0 && records.size() % pageSize == 0)) {
return new SizeFixResult(records, total);
}
private static class MentionHelper {
if (records.size() < pageSize) {
Log.w(TAG, "Hit a miscalculation where we don't have the full dataset, but it's smaller than a page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
return new SizeFixResult(records, records.size() + startPosition);
}
private Collection<Long> messageIds = new LinkedList<>();
private Map<Long, List<Mention>> messageIdToMentions = new HashMap<>();
Log.w(TAG, "Hit a miscalculation where our data size isn't a multiple of the page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
int overflow = records.size() % pageSize;
return new SizeFixResult(records.subList(0, records.size() - overflow), total);
}
private static class SizeFixResult {
final List<MessageRecord> messages;
final int total;
private SizeFixResult(@NonNull List<MessageRecord> messages, int total) {
this.messages = messages;
this.total = total;
}
}
interface DataUpdatedCallback {
void onDataUpdated();
}
static class Invalidator {
private Runnable callback;
synchronized void invalidate() {
if (callback != null) {
callback.run();
void add(MessageRecord record) {
if (record.isMms()) {
messageIds.add(record.getId());
}
}
private synchronized void observe(@NonNull Runnable callback) {
this.callback = callback;
void fetchMentions(Context context) {
messageIdToMentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessages(messageIds);
}
@Nullable List<Mention> getMentions(long id) {
return messageIdToMentions.get(id);
}
}
static class Factory extends DataSource.Factory<Integer, MessageRecord> {
static class Factory extends DataSource.Factory<Integer, ConversationMessage> {
private final Context context;
private final long threadId;
private final Invalidator invalidator;
private final DataUpdatedCallback callback;
Factory(Context context, long threadId, @NonNull Invalidator invalidator, @NonNull DataUpdatedCallback callback) {
Factory(Context context, long threadId, @NonNull Invalidator invalidator) {
this.context = context;
this.threadId = threadId;
this.invalidator = invalidator;
this.callback = callback;
}
@Override
public @NonNull DataSource<Integer, MessageRecord> create() {
return new ConversationDataSource(context, threadId, invalidator, callback);
public @NonNull DataSource<Integer, ConversationMessage> create() {
return new ConversationDataSource(context, threadId, invalidator);
}
}
}

View File

@@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms.conversation;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -26,6 +27,7 @@ import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.net.Uri;
import android.text.Annotation;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
@@ -36,7 +38,6 @@ import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
@@ -52,18 +53,19 @@ import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.ConfirmIdentityDialog;
import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.BorderlessImageView;
import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
import org.thoughtcrime.securesms.components.DocumentView;
@@ -71,13 +73,12 @@ import org.thoughtcrime.securesms.components.LinkPreviewView;
import org.thoughtcrime.securesms.components.Outliner;
import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.SharedContactView;
import org.thoughtcrime.securesms.components.StickerView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
@@ -109,22 +110,26 @@ import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.VibrateUtil;
import org.thoughtcrime.securesms.util.UrlClickHandler;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static org.thoughtcrime.securesms.util.ThemeUtil.isDarkTheme;
/**
* A view that displays an individual conversation item within a conversation
* thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter.
@@ -143,11 +148,13 @@ public class ConversationItem extends LinearLayout implements BindableConversati
private static final Rect SWIPE_RECT = new Rect();
private MessageRecord messageRecord;
private Locale locale;
private boolean groupThread;
private LiveRecipient recipient;
private GlideRequests glideRequests;
private ConversationMessage conversationMessage;
private MessageRecord messageRecord;
private Locale locale;
private boolean groupThread;
private LiveRecipient recipient;
private GlideRequests glideRequests;
private ValueAnimator pulseOutlinerAlphaAnimator;
protected ConversationItemBodyBubble bodyBubble;
protected View reply;
@@ -164,15 +171,17 @@ public class ConversationItem extends LinearLayout implements BindableConversati
private ViewGroup container;
protected ReactionsConversationView reactionsView;
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
private @NonNull Outliner outliner = new Outliner();
private @NonNull Set<ConversationMessage> batchSelected = new HashSet<>();
private @NonNull Outliner outliner = new Outliner();
private @NonNull Outliner pulseOutliner = new Outliner();
private @NonNull List<Outliner> outliners = new ArrayList<>(2);
private LiveRecipient conversationRecipient;
private Stub<ConversationItemThumbnail> mediaThumbnailStub;
private Stub<AudioView> audioViewStub;
private Stub<DocumentView> documentViewStub;
private Stub<SharedContactView> sharedContactStub;
private Stub<LinkPreviewView> linkPreviewStub;
private Stub<StickerView> stickerStub;
private Stub<BorderlessImageView> stickerStub;
private Stub<ViewOnceMessageView> revealableStub;
private @Nullable EventListener eventListener;
@@ -186,6 +195,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
private final UrlClickListener urlClickListener = new UrlClickListener();
private final Context context;
@@ -238,22 +248,23 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
@Override
public void bind(@NonNull MessageRecord messageRecord,
public void bind(@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulseHighlight)
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulse)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
conversationRecipient = conversationRecipient.resolve();
this.messageRecord = messageRecord;
this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord();
this.locale = locale;
this.glideRequests = glideRequests;
this.batchSelected = batchSelected;
@@ -267,9 +278,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
setGutterSizes(messageRecord, groupThread);
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread);
setInteractionState(messageRecord, pulseHighlight);
setBodyText(messageRecord, searchQuery);
setBubbleState(messageRecord);
setInteractionState(conversationMessage, pulse);
setStatusIcons(messageRecord);
setContactPhoto(recipient.get());
setGroupMessageStatus(messageRecord, recipient.get());
@@ -383,20 +394,21 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (conversationRecipient != null) {
conversationRecipient.removeForeverObserver(this);
}
cancelPulseOutlinerAnimation();
}
public MessageRecord getMessageRecord() {
return messageRecord;
public ConversationMessage getConversationMessage() {
return conversationMessage;
}
/// MessageRecord Attribute Parsers
private void setBubbleState(MessageRecord messageRecord) {
if (messageRecord.isOutgoing()) {
if (messageRecord.isOutgoing() && !messageRecord.isRemoteDelete()) {
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
} else if (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord)) {
} else if (messageRecord.isRemoteDelete() || (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord))) {
bodyBubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_reveal_viewed_background_color), PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
@@ -407,7 +419,21 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_sent_text_secondary_color));
bodyBubble.setOutliner(shouldDrawBodyBubbleOutline(messageRecord) ? outliner : null);
pulseOutliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_mention_pulse_color));
pulseOutliner.setStrokeWidth(ViewUtil.dpToPx(4));
outliners.clear();
if (shouldDrawBodyBubbleOutline(messageRecord)) {
outliners.add(outliner);
}
outliners.add(pulseOutliner);
bodyBubble.setOutliners(outliners);
if (mediaThumbnailStub.resolved()) {
mediaThumbnailStub.get().setPulseOutliner(pulseOutliner);
}
if (audioViewStub.resolved()) {
setAudioViewTint(messageRecord);
@@ -428,38 +454,61 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
}
private void setInteractionState(MessageRecord messageRecord, boolean pulseHighlight) {
if (batchSelected.contains(messageRecord)) {
private void setInteractionState(ConversationMessage conversationMessage, boolean pulseMention) {
if (batchSelected.contains(conversationMessage)) {
setBackgroundResource(R.drawable.conversation_item_background);
setSelected(true);
} else if (pulseHighlight) {
setBackgroundResource(R.drawable.conversation_item_background_animated);
setSelected(true);
postDelayed(() -> setSelected(false), 500);
} else if (pulseMention) {
setBackground(null);
setSelected(false);
startPulseOutlinerAnimation();
} else {
setSelected(false);
}
if (mediaThumbnailStub.resolved()) {
mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
mediaThumbnailStub.get().setLongClickable(batchSelected.isEmpty());
}
if (audioViewStub.resolved()) {
audioViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
audioViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
audioViewStub.get().setClickable(batchSelected.isEmpty());
audioViewStub.get().setEnabled(batchSelected.isEmpty());
}
if (documentViewStub.resolved()) {
documentViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
documentViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty());
documentViewStub.get().setClickable(batchSelected.isEmpty());
}
}
private void startPulseOutlinerAnimation() {
pulseOutlinerAlphaAnimator = ValueAnimator.ofInt(0, 0x66, 0).setDuration(600);
pulseOutlinerAlphaAnimator.addUpdateListener(animator -> {
pulseOutliner.setAlpha((Integer) animator.getAnimatedValue());
bodyBubble.invalidate();
if (mediaThumbnailStub.resolved()) {
mediaThumbnailStub.get().invalidate();
}
});
pulseOutlinerAlphaAnimator.start();
}
private void cancelPulseOutlinerAnimation() {
if (pulseOutlinerAlphaAnimator != null) {
pulseOutlinerAlphaAnimator.cancel();
pulseOutlinerAlphaAnimator = null;
}
pulseOutliner.setAlpha(0);
}
private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord) {
return !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord);
boolean isIncomingViewedOnce = !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord);
return isIncomingViewedOnce || messageRecord.isRemoteDelete();
}
private boolean isCaptionlessMms(MessageRecord messageRecord) {
@@ -478,12 +527,20 @@ public class ConversationItem extends LinearLayout implements BindableConversati
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() != null;
}
private boolean isBorderless(MessageRecord messageRecord) {
//noinspection ConstantConditions
return isCaptionlessMms(messageRecord) &&
hasThumbnail(messageRecord) &&
((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide().isBorderless();
}
private boolean hasOnlyThumbnail(MessageRecord messageRecord) {
return hasThumbnail(messageRecord) &&
!hasAudio(messageRecord) &&
!hasDocument(messageRecord) &&
!hasSharedContact(messageRecord) &&
!hasSticker(messageRecord) &&
!isBorderless(messageRecord) &&
!isViewOnceMessage(messageRecord);
}
@@ -525,23 +582,29 @@ public class ConversationItem extends LinearLayout implements BindableConversati
return messageRecord.isMms() && ((MmsMessageRecord) messageRecord).isViewOnce();
}
private void setBodyText(MessageRecord messageRecord, @Nullable String searchQuery) {
private void setBodyText(@NonNull MessageRecord messageRecord,
@Nullable String searchQuery)
{
bodyText.setClickable(false);
bodyText.setFocusable(false);
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context));
bodyText.setMovementMethod(LongClickMovementMethod.getInstance(getContext()));
if (messageRecord.isRemoteDelete()) {
String deletedMessage = context.getString(R.string.ConversationItem_this_message_was_deleted);
String deletedMessage = context.getString(messageRecord.isOutgoing() ? R.string.ConversationItem_you_deleted_this_message : R.string.ConversationItem_this_message_was_deleted);
SpannableString italics = new SpannableString(deletedMessage);
italics.setSpan(new RelativeSizeSpan(0.9f), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
italics.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
italics.setSpan(new ForegroundColorSpan(ThemeUtil.getThemedColor(context, R.attr.conversation_item_delete_for_everyone_text_color)),
0,
deletedMessage.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
bodyText.setText(italics);
bodyText.setVisibility(View.VISIBLE);
} else if (isCaptionlessMms(messageRecord)) {
bodyText.setVisibility(View.GONE);
} else {
Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(getContext()), batchSelected.isEmpty());
Spannable styledText = linkifyMessageBody(conversationMessage.getDisplayBody(getContext()), batchSelected.isEmpty());
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery);
@@ -551,6 +614,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
bodyText.setOverflowText(null);
}
if (messageRecord.isOutgoing()) {
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, isDarkTheme(context) ? R.color.core_grey_60 : R.color.core_grey_20));
} else {
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_40));
}
bodyText.setText(styledText);
bodyText.setVisibility(View.VISIBLE);
}
@@ -673,7 +742,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
} else if (hasSticker(messageRecord) && isCaptionlessMms(messageRecord)) {
} else if ((hasSticker(messageRecord) && isCaptionlessMms(messageRecord)) || isBorderless(messageRecord)) {
bodyBubble.setBackgroundColor(Color.TRANSPARENT);
stickerStub.get().setVisibility(View.VISIBLE);
@@ -684,9 +753,16 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
stickerStub.get().setSticker(glideRequests, ((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide());
stickerStub.get().setThumbnailClickListener(new StickerClickListener());
if (hasSticker(messageRecord)) {
//noinspection ConstantConditions
stickerStub.get().setSlide(glideRequests, ((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide());
stickerStub.get().setThumbnailClickListener(new StickerClickListener());
} else {
//noinspection ConstantConditions
stickerStub.get().setSlide(glideRequests, ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide());
stickerStub.get().setThumbnailClickListener((v, slide) -> performClick());
}
stickerStub.get().setDownloadClickListener(downloadClickListener);
stickerStub.get().setOnLongClickListener(passthroughClickListener);
stickerStub.get().setOnClickListener(passthroughClickListener);
@@ -704,7 +780,6 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
List<Slide> thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides();
mediaThumbnailStub.get().setImageResource(glideRequests,
thumbnailSlides,
@@ -838,14 +913,16 @@ public class ConversationItem extends LinearLayout implements BindableConversati
contactPhoto.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onGroupMemberAvatarClicked(recipientId, conversationRecipient.get().requireGroupId());
eventListener.onGroupMemberClicked(recipientId, conversationRecipient.get().requireGroupId());
}
});
contactPhoto.setAvatar(glideRequests, recipient, false);
}
private SpannableString linkifyMessageBody(SpannableString messageBody, boolean shouldLinkifyAllLinks) {
private SpannableString linkifyMessageBody(@NonNull SpannableString messageBody,
boolean shouldLinkifyAllLinks)
{
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
boolean hasLinks = Linkify.addLinks(messageBody, shouldLinkifyAllLinks ? linkPattern : 0);
@@ -857,11 +934,18 @@ public class ConversationItem extends LinearLayout implements BindableConversati
URLSpan[] urlSpans = messageBody.getSpans(0, messageBody.length(), URLSpan.class);
for (URLSpan urlSpan : urlSpans) {
int start = messageBody.getSpanStart(urlSpan);
int end = messageBody.getSpanEnd(urlSpan);
messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
int start = messageBody.getSpanStart(urlSpan);
int end = messageBody.getSpanEnd(urlSpan);
URLSpan span = new InterceptableLongClickCopyLinkSpan(urlSpan.getURL(), urlClickListener);
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(messageBody);
for (Annotation annotation : mentionAnnotations) {
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return messageBody;
}
@@ -884,7 +968,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
//noinspection ConstantConditions
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getText(), quote.isOriginalMissing(), quote.getAttachment());
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getDisplayText(), quote.isOriginalMissing(), quote.getAttachment());
quoteView.setVisibility(View.VISIBLE);
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
@@ -953,7 +1037,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
reactionsView.setOnClickListener(v -> {
if (eventListener == null) return;
eventListener.onReactionClicked(current.getId(), current.isMms());
eventListener.onReactionClicked(this, current.getId(), current.isMms());
});
}
@@ -977,7 +1061,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
if (hasSticker(messageRecord)) {
if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
return stickerFooter;
} else if (hasSharedContact(messageRecord)) {
return sharedContactStub.get().getFooter();
@@ -1002,21 +1086,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
@SuppressLint("SetTextI18n")
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
if (groupThread && !messageRecord.isOutgoing() && groupSender != null && groupSenderProfileName != null) {
if (FeatureFlags.profileDisplay()) {
groupSender.setText(recipient.getDisplayName(getContext()));
groupSenderProfileName.setVisibility(View.GONE);
} else {
groupSender.setText(recipient.toShortString(context));
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
groupSenderProfileName.setText("~" + recipient.getProfileName().toString());
groupSenderProfileName.setVisibility(View.VISIBLE);
} else {
groupSenderProfileName.setText(null);
groupSenderProfileName.setVisibility(View.GONE);
}
}
groupSender.setText(recipient.getDisplayName(getContext()));
groupSenderProfileName.setVisibility(View.GONE);
}
}
@@ -1026,7 +1097,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (shouldDrawBodyBubbleOutline(messageRecord)) {
groupSender.setTextColor(stickerAuthorColor);
groupSenderProfileName.setTextColor(stickerAuthorColor);
} else if (hasSticker(messageRecord)) {
} else if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
groupSender.setTextColor(stickerAuthorColor);
groupSenderProfileName.setTextColor(stickerAuthorColor);
} else {
@@ -1075,33 +1146,41 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (current.isOutgoing()) {
background = R.drawable.message_bubble_background_sent_alone;
outliner.setRadius(bigRadius);
pulseOutliner.setRadius(bigRadius);
} else {
background = R.drawable.message_bubble_background_received_alone;
outliner.setRadius(bigRadius);
pulseOutliner.setRadius(bigRadius);
}
} else if (isStartOfMessageCluster(current, previous, isGroupThread)) {
if (current.isOutgoing()) {
background = R.drawable.message_bubble_background_sent_start;
outliner.setRadii(bigRadius, bigRadius, smallRadius, bigRadius);
pulseOutliner.setRadii(bigRadius, bigRadius, smallRadius, bigRadius);
} else {
background = R.drawable.message_bubble_background_received_start;
outliner.setRadii(bigRadius, bigRadius, bigRadius, smallRadius);
pulseOutliner.setRadii(bigRadius, bigRadius, bigRadius, smallRadius);
}
} else if (isEndOfMessageCluster(current, next, isGroupThread)) {
if (current.isOutgoing()) {
background = R.drawable.message_bubble_background_sent_end;
outliner.setRadii(bigRadius, smallRadius, bigRadius, bigRadius);
pulseOutliner.setRadii(bigRadius, smallRadius, bigRadius, bigRadius);
} else {
background = R.drawable.message_bubble_background_received_end;
outliner.setRadii(smallRadius, bigRadius, bigRadius, bigRadius);
pulseOutliner.setRadii(smallRadius, bigRadius, bigRadius, bigRadius);
}
} else {
if (current.isOutgoing()) {
background = R.drawable.message_bubble_background_sent_middle;
outliner.setRadii(bigRadius, smallRadius, smallRadius, bigRadius);
pulseOutliner.setRadii(bigRadius, smallRadius, smallRadius, bigRadius);
} else {
background = R.drawable.message_bubble_background_received_middle;
outliner.setRadii(smallRadius, bigRadius, bigRadius, smallRadius);
pulseOutliner.setRadii(smallRadius, bigRadius, bigRadius, smallRadius);
}
}
@@ -1323,7 +1402,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
public void onClick(View v, Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick();
} else if (eventListener != null && hasSticker(messageRecord)){
} else if (eventListener != null && hasSticker(messageRecord)) {
//noinspection ConstantConditions
eventListener.onStickerClicked(((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide().asAttachment().getSticker());
}
@@ -1390,13 +1469,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (!shouldInterceptClicks(messageRecord) && parent != null) {
parent.onClick(v);
} else if (messageRecord.isFailed()) {
Intent intent = new Intent(context, MessageDetailsActivity.class);
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId());
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, messageRecord.getThreadId());
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, groupThread && messageRecord.isPush());
intent.putExtra(MessageDetailsActivity.RECIPIENT_EXTRA, conversationRecipient.getId());
context.startActivity(intent);
if (eventListener != null) {
eventListener.onMessageWithErrorClicked(messageRecord);
}
} else if (!messageRecord.isOutgoing() && messageRecord.isIdentityMismatchFailure()) {
handleApproveIdentity();
} else if (messageRecord.isPendingInsecureSmsFallback()) {
@@ -1405,6 +1480,33 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
}
private final class UrlClickListener implements UrlClickHandler {
@Override
public boolean handleOnClick(@NonNull String url) {
return eventListener != null && eventListener.onUrlClicked(url);
}
}
private class MentionClickableSpan extends ClickableSpan {
private final RecipientId mentionedRecipientId;
MentionClickableSpan(RecipientId mentionedRecipientId) {
this.mentionedRecipientId = mentionedRecipientId;
}
@Override
public void onClick(@NonNull View widget) {
if (eventListener != null && !Recipient.resolved(mentionedRecipientId).isLocalNumber()) {
VibrateUtil.vibrateTick(context);
eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId());
}
}
@Override
public void updateDrawState(@NonNull TextPaint ds) { }
}
private void handleMessageApproval() {
final int title;
final int message;

View File

@@ -5,13 +5,18 @@ import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.Outliner;
import org.thoughtcrime.securesms.util.Util;
import java.util.Collections;
import java.util.List;
public class ConversationItemBodyBubble extends LinearLayout {
@Nullable private Outliner outliner;
@Nullable private List<Outliner> outliners = Collections.emptyList();
@Nullable private OnSizeChangedListener sizeChangedListener;
public ConversationItemBodyBubble(Context context) {
@@ -26,8 +31,8 @@ public class ConversationItemBodyBubble extends LinearLayout {
super(context, attrs, defStyleAttr);
}
public void setOutliner(@Nullable Outliner outliner) {
this.outliner = outliner;
public void setOutliners(@NonNull List<Outliner> outliners) {
this.outliners = outliners;
}
public void setOnSizeChangedListener(@Nullable OnSizeChangedListener listener) {
@@ -38,9 +43,11 @@ public class ConversationItemBodyBubble extends LinearLayout {
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (outliner == null) return;
if (Util.isEmpty(outliners)) return;
outliner.draw(canvas, 0, getMeasuredWidth(), getMeasuredHeight(), 0);
for (Outliner outliner : outliners) {
outliner.draw(canvas, 0, getMeasuredWidth(), getMeasuredHeight(), 0);
}
}
@Override

View File

@@ -114,8 +114,8 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
private void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder) {
if (cannotSwipeViewHolder(viewHolder)) return;
ConversationItem item = ((ConversationItem) viewHolder.itemView);
MessageRecord messageRecord = item.getMessageRecord();
ConversationItem item = ((ConversationItem) viewHolder.itemView);
ConversationMessage messageRecord = item.getConversationMessage();
onSwipeListener.onSwipe(messageRecord);
}
@@ -169,7 +169,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
if (!(viewHolder.itemView instanceof ConversationItem)) return true;
ConversationItem item = ((ConversationItem) viewHolder.itemView);
return !swipeAvailabilityProvider.isSwipeAvailable(item.getMessageRecord()) ||
return !swipeAvailabilityProvider.isSwipeAvailable(item.getConversationMessage()) ||
item.disallowSwipe(latestDownX, latestDownY);
}
@@ -192,10 +192,10 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
}
interface SwipeAvailabilityProvider {
boolean isSwipeAvailable(MessageRecord messageRecord);
boolean isSwipeAvailable(ConversationMessage conversationMessage);
}
interface OnSwipeListener {
void onSwipe(MessageRecord messageRecord);
void onSwipe(ConversationMessage conversationMessage);
}
}

View File

@@ -0,0 +1,145 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.text.SpannableString;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.util.Conversions;
import java.security.MessageDigest;
import java.util.Collections;
import java.util.List;
/**
* A view level model used to pass arbitrary message related information needed
* for various presentations.
*/
public class ConversationMessage {
@NonNull private final MessageRecord messageRecord;
@NonNull private final List<Mention> mentions;
@Nullable private final SpannableString body;
private ConversationMessage(@NonNull MessageRecord messageRecord) {
this(messageRecord, null, null);
}
private ConversationMessage(@NonNull MessageRecord messageRecord,
@Nullable CharSequence body,
@Nullable List<Mention> mentions)
{
this.messageRecord = messageRecord;
this.body = body != null ? SpannableString.valueOf(body) : null;
this.mentions = mentions != null ? mentions : Collections.emptyList();
if (!this.mentions.isEmpty() && this.body != null) {
MentionAnnotation.setMentionAnnotations(this.body, this.mentions);
}
}
public @NonNull MessageRecord getMessageRecord() {
return messageRecord;
}
public @NonNull List<Mention> getMentions() {
return mentions;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ConversationMessage that = (ConversationMessage) o;
return messageRecord.equals(that.messageRecord);
}
@Override
public int hashCode() {
return messageRecord.hashCode();
}
public long getUniqueId(@NonNull MessageDigest digest) {
String unique = (messageRecord.isMms() ? "MMS::" : "SMS::") + messageRecord.getId();
byte[] bytes = digest.digest(unique.getBytes());
return Conversions.byteArrayToLong(bytes);
}
public @NonNull SpannableString getDisplayBody(Context context) {
if (mentions.isEmpty() || body == null) {
return messageRecord.getDisplayBody(context);
}
return body;
}
/**
* Factory providing multiple ways of creating {@link ConversationMessage}s.
*/
public static class ConversationMessageFactory {
/**
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord. No database or
* heavy work performed as the message is assumed to not have any mentions.
*/
@AnyThread
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord) {
return new ConversationMessage(messageRecord);
}
/**
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord, potentially annotated body, and
* list of actual mentions. No database or heavy work performed as the body and mentions are assumed to be
* fully updated with display names.
*
* @param body Contains appropriate {@link MentionAnnotation}s and is updated with actual profile names.
* @param mentions List of actual mentions (i.e., not placeholder) matching annotation ranges in body.
*/
@AnyThread
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, @Nullable CharSequence body, @Nullable List<Mention> mentions) {
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
return new ConversationMessage(messageRecord, body, mentions);
}
return createWithResolvedData(messageRecord);
}
/**
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord and will update and modify the provided
* mentions from placeholder to actual. This method may perform database operations to resolve mentions to display names.
*
* @param mentions List of placeholder mentions to be used to update the body in the provided MessageRecord.
*/
@WorkerThread
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @Nullable List<Mention> mentions) {
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
}
return createWithResolvedData(messageRecord);
}
/**
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord, and will query for potential mentions. If mentions
* are found, the body of the provided message will be updated and modified to match actual mentions. This will perform
* database operations to query for mentions and then to resolve mentions to display names.
*/
@WorkerThread
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord) {
if (messageRecord.isMms()) {
List<Mention> mentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessage(messageRecord.getId());
if (!mentions.isEmpty()) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
}
}
return createWithResolvedData(messageRecord);
}
}
}

View File

@@ -5,7 +5,9 @@ import android.animation.AnimatorSet;
import android.app.Activity;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
@@ -19,7 +21,6 @@ import android.widget.RelativeLayout;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
@@ -32,10 +33,11 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.MaskView;
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -91,6 +93,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
private OnHideListener onHideListener;
private AnimatorSet revealAnimatorSet = new AnimatorSet();
private AnimatorSet revealMaskAnimatorSet = new AnimatorSet();
private AnimatorSet hideAnimatorSet = new AnimatorSet();
private AnimatorSet hideAllButMaskAnimatorSet = new AnimatorSet();
private AnimatorSet hideMaskAnimatorSet = new AnimatorSet();
@@ -185,16 +188,31 @@ public final class ConversationReactionOverlay extends RelativeLayout {
maskView.setTarget(maskTarget);
hideAnimatorSet.end();
toolbar.setVisibility(VISIBLE);
setVisibility(View.VISIBLE);
revealAnimatorSet.start();
if (Build.VERSION.SDK_INT >= 21) {
this.activity = activity;
originalStatusBarColor = activity.getWindow().getStatusBarColor();
activity.getWindow().setStatusBarColor(ContextCompat.getColor(activity, R.color.action_mode_status_bar));
activity.getWindow().setStatusBarColor(ThemeUtil.getThemedColor(getContext(), R.attr.reactions_overlay_toolbar_background_color));
if (!ThemeUtil.isDarkTheme(getContext()) && Build.VERSION.SDK_INT >= 23) {
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
}
}
public void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
maskView.setPadding(0, maskPaddingTop, 0, maskPaddingBottom);
maskView.setTarget(maskTarget);
hideAnimatorSet.end();
toolbar.setVisibility(GONE);
setVisibility(VISIBLE);
revealMaskAnimatorSet.start();
}
public void hide() {
maskView.setTarget(null);
hideInternal(hideAnimatorSet, onHideListener);
@@ -218,8 +236,9 @@ public final class ConversationReactionOverlay extends RelativeLayout {
revealAnimatorSet.end();
hideAnimatorSet.start();
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
if (Build.VERSION.SDK_INT >= 23 && activity != null) {
activity.getWindow().setStatusBarColor(originalStatusBarColor);
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
activity = null;
}
@@ -358,7 +377,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
view.setTranslationY(0);
boolean isAtCustomIndex = i == customEmojiIndex;
boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && ReactionEmoji.values()[i].emoji.equals(oldEmoji);
boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && oldEmoji != null && ReactionEmoji.values()[i].emoji.equals(EmojiUtil.getCanonicalRepresentation(oldEmoji));
boolean isAtCustomIndexAndOldEmojiExists = isAtCustomIndex && oldEmoji != null;
if (!foundSelected &&
@@ -379,13 +398,13 @@ public final class ConversationReactionOverlay extends RelativeLayout {
view.setImageEmoji(oldEmoji);
view.setTag(oldEmoji);
} else {
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
}
} else if (isAtCustomIndex) {
view.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.ic_any_emoji_32));
view.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.reactions_overlay_custom_emoji_icon));
view.setTag(null);
} else {
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
}
}
}
@@ -447,7 +466,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
if (selected == customEmojiIndex) {
onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null);
} else {
onReactionSelectedListener.onReactionSelected(messageRecord, ReactionEmoji.values()[selected].emoji);
onReactionSelectedListener.onReactionSelected(messageRecord, SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[selected].emoji));
}
} else {
hide();
@@ -534,6 +553,9 @@ public final class ConversationReactionOverlay extends RelativeLayout {
revealAnimatorSet.setInterpolator(INTERPOLATOR);
revealAnimatorSet.playTogether(reveals);
revealMaskAnimatorSet.setInterpolator(INTERPOLATOR);
revealMaskAnimatorSet.playTogether(overlayRevealAnim);
List<Animator> hides = Stream.of(emojiViews)
.mapIndexed((idx, v) -> {
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);

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