Compare commits

...

187 Commits

Author SHA1 Message Date
Greyson Parrelli
1730260343 Bump version to 4.74.2 2020-10-19 17:34:08 -04:00
Greyson Parrelli
27506e9ed8 Updated language translations. 2020-10-19 17:33:25 -04:00
Alex Hart
dc64a186d5 Fix mediastore access for Android Q. 2020-10-19 18:16:29 -03:00
Alex Hart
3163e09b98 Fix issue with backup deletion. 2020-10-19 10:27:18 -03:00
Alex Hart
dcb9978bb1 Bump version to 4.74.1 2020-10-16 16:40:15 -03:00
Alex Hart
4a94a0a5c5 Updated language translations. 2020-10-16 16:36:47 -03:00
Alex Hart
8a2d20403e Add Proximity sensing back to voice note. 2020-10-16 16:23:04 -03:00
Alex Hart
ec706e95cc Backup style and copy tweak. 2020-10-16 16:17:34 -03:00
Alex Hart
bd3b14a27f Fix seeking voice notes that do not have waveforms. 2020-10-16 15:37:26 -03:00
Alex Hart
082d9e852c Voice Note Beta Feedback fixes. 2020-10-16 13:14:01 -03:00
Alex Hart
36da519b26 Bump version to 4.74.0 2020-10-15 17:43:35 -03:00
Alex Hart
06ffdde892 Updated language translations. 2020-10-15 17:42:28 -03:00
Greyson Parrelli
1ec57c080c Update targetSdk to 29. 2020-10-15 16:19:17 -04:00
Alan Evans
a635f27c68 Hide group link when not enabled. 2020-10-15 16:19:17 -04:00
Alex Hart
ee3d7a9a35 Implement new workflow for scoped storage backup selection. 2020-10-15 16:19:17 -04:00
Alex Hart
9a1c869efe Allow consecutive voice notes to be played as a playlist. 2020-10-15 16:19:17 -04:00
Alan Evans
837ed76f85 Show reminder banner to administrators for pending group join requests. 2020-10-15 16:19:17 -04:00
Cody Henthorne
b46589cd14 Remove mentions feature flag. 2020-10-15 16:19:17 -04:00
Alan Evans
d04e4606d2 Remove GV2 create flag. 2020-10-15 16:19:17 -04:00
Greyson Parrelli
385bd0eb8a Fix possible crash for unregistered devices. 2020-10-15 16:19:17 -04:00
Greyson Parrelli
089656e5c4 Add an application migration to do a CDS refresh. 2020-10-15 16:19:17 -04:00
Greyson Parrelli
84ec6dd458 Improve network reliability during resumable uploads. 2020-10-15 16:19:17 -04:00
Cody Henthorne
322c139c26 Fix bug of video showing on next call after cancel pre-join.
Fixes #10083
2020-10-15 16:19:17 -04:00
Alan Evans
babe1833bb Derive GV2 master key and group id from GV1. 2020-10-15 16:19:17 -04:00
Alex Hart
9effa47dd8 Allow voice notes to continue playback after leaving conversation. 2020-10-15 16:19:17 -04:00
Greyson Parrelli
7ef57cc0cf Add support for syncing pinned status with storage service. 2020-10-15 16:19:17 -04:00
Greyson Parrelli
97420aae1b Add a Github Action to test our docker build every day.
Runs at 5am UTC, which is ~midnight EST.
2020-10-15 16:19:17 -04:00
Greyson Parrelli
415e6309f9 Ensure CI runs on 5.x branches. 2020-10-15 16:19:17 -04:00
Greyson Parrelli
83e63ff854 Improve the reproducible build process.
* Moved stuff into it's own `reproducible-builds` directory.
* Improved reproducible build by using a debian snapshot and more clearly listing dependencies.
* Removed signing from assembleReelase.
* Updated README.
2020-10-15 16:19:17 -04:00
Greyson Parrelli
de7f103130 Add support for modern profile sharing. 2020-10-15 16:19:12 -04:00
Alan Evans
2cb912681d Bump version to 4.73.4 2020-10-13 15:18:00 -03:00
Alan Evans
04bdf94b78 Updated language translations. 2020-10-13 15:18:00 -03:00
Cody Henthorne
c7389ddaa7 Fix bug causing incorrect mention suggestions. 2020-10-13 15:18:00 -03:00
Greyson Parrelli
e778ab2e3a Fix issue with remote delete sent transcripts. 2020-10-13 13:50:21 -04:00
Alan Evans
533d86607f Bump version to 4.73.3 2020-10-12 15:24:34 -03:00
Alan Evans
cb2096670f Updated language translations. 2020-10-12 15:19:00 -03:00
Alan Evans
284f221a9d Handle no actual change to group. 2020-10-12 15:11:57 -03:00
Greyson Parrelli
bc639dd438 Show error message when unable to compute safety number. 2020-10-12 12:14:13 -04:00
Greyson Parrelli
1baddbb40e Fix some oddities with message request behavior.
There was a weird case where how our intent checking could behave
differently when coming from search. There's also some funny
interactions where backups, where because the 'time message requests was
enabled' is reset to System.currentTimeMillis() post-restore, we thought
there were always 'pre-message-request messages'. Only mattered when
profileSharing is also disabled, so impact isn't huge. Given a lot of
this UI is going away soon, rather than doing the complicated thing of
backing up the true timestamp, I just default it to 0 to mitigate
things.
2020-10-12 10:09:35 -04:00
Alan Evans
f784dab868 Bump version to 4.73.2 2020-10-09 17:46:21 -03:00
Alan Evans
85192aaa21 Updated language translations. 2020-10-09 17:46:21 -03:00
Alan Evans
054c705fe2 Respect the 206 paged response from the server group logs endpoint.
Prevent the deduplicate message logic firing and log it if it does.
2020-10-09 17:46:21 -03:00
Alan Evans
07b0d8cf6e Utilities for correctly handling json parsing errors on network responses. 2020-10-09 17:11:19 -03:00
Greyson Parrelli
597d16f566 Ensure one row per recipient in getRecipientSettingsForSync().
Technically there's no unique constraint in ThreadDatabase to guarantee
only one thread per recipient. We saw a crash that indicated that one
user has two threads for the same recipient. That's not true for any of
my devices. Still, best to play it safe here while we try to figure out
why this is happening.
2020-10-09 12:16:38 -04:00
Greyson Parrelli
0ca2c781c3 Only show the delivery status icon for 'sending' on remote deletes. 2020-10-08 16:29:13 -04:00
Greyson Parrelli
f642de9c41 Disable mention clicks in multi-select mode. 2020-10-08 14:09:44 -04:00
Greyson Parrelli
8965388d05 Fix rendering of remote-deleted view-once messages. 2020-10-08 14:04:00 -04:00
Alan Evans
58c4582f15 Bump version to 4.73.1 2020-10-08 12:53:17 -03:00
Alan Evans
44bc1b5cc0 Updated language translations. 2020-10-08 12:51:08 -03:00
Greyson Parrelli
714ebb3e08 Allow remote deletes of pending messages. 2020-10-08 10:58:55 -04:00
Greyson Parrelli
8f871c2e3a Don't allow quote-jumps to remote deleted messages. 2020-10-08 10:29:46 -04:00
Greyson Parrelli
5cdc5bc441 Ensure reactions are deleted for remote-deleted messages.
We were doing this for MmsDatabase, but not SmsDatabase. Includes a
migration to cleanup any existing bad state.
2020-10-08 10:21:57 -04:00
Cody Henthorne
8d060837ad Cleanup abandoned mentions during backup restore. 2020-10-08 09:46:26 -04:00
Greyson Parrelli
1d230d4cd6 Schedule another attribute refresh for GV2. 2020-10-07 20:29:40 -04:00
Greyson Parrelli
3636ae7667 Add the Pixel 4 back to the CameraX blacklist.
It's having pretty bad exposure problems.
2020-10-07 19:54:24 -04:00
Greyson Parrelli
9ffb5112c6 Bump version to 4.73.0 2020-10-07 17:22:05 -04:00
Greyson Parrelli
ca5d574cd7 Updated language translations. 2020-10-07 17:22:05 -04:00
Greyson Parrelli
c80283dbcc Inline remote delete feature flag. 2020-10-07 17:22:05 -04:00
Greyson Parrelli
3fcaddf2d3 Update delete for everyone education text. 2020-10-07 17:22:05 -04:00
Greyson Parrelli
6ecff5bce9 Ensure the storage manifest has all inserts and deletes.
A user hit a fishy case where not all inserts were present in the full
keyset. It's unclear how that would happen, so I'm being even more
explicit here.
2020-10-07 17:22:05 -04:00
Greyson Parrelli
a103c7dcb6 Apply storage service values for phone number privacy. 2020-10-07 17:22:05 -04:00
Greyson Parrelli
63746bbb47 Add support for syncing forced unread status. 2020-10-07 17:22:05 -04:00
Alan Evans
ed0be6fc9a Add dialog transitions to group manager. 2020-10-07 17:22:05 -04:00
Alan Evans
26404ff5d7 More descriptive copy for group link permission errors. 2020-10-07 17:22:05 -04:00
Alan Evans
adf1674877 Support sgnl://signal.group links. 2020-10-07 17:22:05 -04:00
Greyson Parrelli
ab2235fc88 Prefer remote value for profile sharing for groups during storage sync. 2020-10-07 17:22:05 -04:00
Cody Henthorne
441a6d3fe7 Fix start call resizing improperly with wrapping text. 2020-10-07 17:22:05 -04:00
Greyson Parrelli
e00397620a Simplify storing storage-service-specific recipient values.
This gives us the ability to separate things we need for the Recipient
class from things we only need for storage syncing.

Not only does this simplify the storage service model building code
(i.e. we no longer need to pass around a set of archived recipients),
but it also eliminates a join on the Identity table for building regular
recipients, which should help perf.
2020-10-07 17:22:05 -04:00
Alan Evans
38fa58c0a3 Write previous group state to the database for advanced change messages. 2020-10-06 11:21:56 -03:00
Alan Evans
b40fd7b243 Fix Audio slides reporting images.
Fixes #10063
2020-10-06 11:09:50 -03:00
Alan Evans
ae34877496 Use Emoji respecting textview in group member lists. 2020-10-06 10:36:48 -03:00
Greyson Parrelli
599cf1e5cb Ensure we refresh recipients after changing storage keys. 2020-10-06 10:32:03 -03:00
Greyson Parrelli
474963dcf1 Add the ability to migrate to new KBS enclaves. 2020-10-06 10:32:03 -03:00
Alan Evans
e22384b6b4 New copy for GV2 direct add message request. 2020-10-05 14:54:18 -03:00
Cody Henthorne
fb00652396 Fix incorrect UI for inactive groups. 2020-10-05 12:59:00 -04:00
Alan Evans
a5dbb5d91f Block unknown group messages from blocked senders. 2020-10-05 12:30:29 -03:00
Alan Evans
e75a03b6f8 Bump version to 4.72.6 2020-10-02 12:25:40 -03:00
Alan Evans
eb7fe7f3e0 Updated language translations. 2020-10-02 12:25:40 -03:00
Cody Henthorne
3179808f17 Cleanup mentions with bad thread ids or ranges, or duplicates. 2020-10-02 12:25:40 -03:00
Alan Evans
fde9f05bd0 Use GV2 change descriptions for invite events. 2020-10-02 10:40:57 -03:00
Alan Evans
8de4290c5b Fix can create backups when timed backup is waiting for charging constraint. 2020-10-02 10:32:04 -03:00
Alan Evans
19c74c8872 Fix English use of quantity zero string. 2020-10-02 10:31:11 -03:00
Alan Evans
50edb5d1f4 Bump version to 4.72.5 2020-09-30 17:38:38 -03:00
Cody Henthorne
c6ccfd7e75 Fix API19 crash when inflating new WebRTC UI. 2020-09-30 17:38:15 -03:00
Alan Evans
3796ce69e4 Clear auth cache on first verification failure. 2020-09-30 17:28:42 -03:00
Cody Henthorne
9835e31b46 Attempt to cleanup invalid mentions. 2020-09-30 15:56:23 -04:00
Alan Evans
a35040c909 Bump version to 4.72.4 2020-09-30 16:05:27 -03:00
Alan Evans
a4c94638ca Updated language translations. 2020-09-30 15:59:29 -03:00
Cody Henthorne
e70a8ae6a0 Drop messages with mentions not sent to V2 Groups. 2020-09-30 14:52:18 -04:00
Alan Evans
100359e38d Allow in notification reply to multi message if you can reply to latest. 2020-09-30 15:42:07 -03:00
Cody Henthorne
cd995aca56 Fix incorrect mention association when messages are deleted. 2020-09-30 14:35:02 -04:00
Alan Evans
3a4bae88ca Add network spinner to add members. 2020-09-30 13:59:39 -03:00
Cody Henthorne
e60eae27fb Tweak font sizes and PIP boundaries in call view. 2020-09-30 11:51:48 -04:00
Alan Evans
cd6c01e230 Fix spinner not disappearing when adding members with no network. 2020-09-30 12:25:35 -03:00
Alan Evans
0af264429f During GV2 storage sync, recover from recipient present but group not present. 2020-09-30 10:11:51 -03:00
Alan Evans
a6d3862350 Ignore bad messages from blocked senders. 2020-09-30 10:08:21 -03:00
Alan Evans
3fca4850dd Fix xml inflation crash. 2020-09-29 16:40:37 -03:00
Alan Evans
ba7e41d9a6 Fix missing Submit Debug Log loading progress spinner. 2020-09-29 15:23:31 -03:00
Alan Evans
fe33ce3413 Various groups V2 dialog copy changes. 2020-09-29 12:03:32 -03:00
Alan Evans
4e25e8aaa2 Ensure clock adjustments does not stop remote config refresh. 2020-09-29 11:10:25 -03:00
Alan Evans
91be826c7d Bump version to 4.72.3 2020-09-28 16:35:44 -03:00
Alan Evans
fdfe0cddb8 Updated language translations. 2020-09-28 16:32:18 -03:00
Alan Evans
e8ef62116f Write gv2-3 capability. 2020-09-28 14:15:19 -03:00
Alan Evans
caf8bb39d8 Fix desktop sync with body-less messages. 2020-09-28 11:53:27 -03:00
Alan Evans
222ba6ee53 Hide admin options on bottom sheet for members not currently in group. 2020-09-28 10:15:29 -03:00
Alan Evans
8dcda73072 Fix media preview crash. 2020-09-28 09:45:06 -03:00
Alan Evans
810365d334 Bump version to 4.72.2 2020-09-25 15:29:36 -03:00
Alan Evans
4b31510589 Updated language translations. 2020-09-25 15:24:47 -03:00
Alan Evans
dfce9a34b8 Fix leave group crash. 2020-09-25 15:18:34 -03:00
Alex Hart
dc9370c32b Fix false group name and avatar updates. 2020-09-25 15:18:34 -03:00
Cody Henthorne
8dbc721c08 Fix stale call preview state by finishing when leaving. 2020-09-25 15:18:34 -03:00
Cody Henthorne
6448b84430 Fix various mention issues.
Fixes #9960
2020-09-25 15:18:34 -03:00
Alan Evans
93d6ce40c3 GV2 learn more copy update. 2020-09-25 15:18:34 -03:00
Alan Evans
ce5be2c1be Share group link via signal to one recipient. 2020-09-25 12:33:14 -03:00
Alan Evans
20fe837022 Enable and disable group link options with first switch. 2020-09-25 12:32:48 -03:00
Greyson Parrelli
e3ce18fa3e Fix possible threading issues with attachment cleanup.
The way things were ordered, it was possible for us to create an
attachment file, but have it 'cleaned up' before we were able to link it
to an attachment row.
2020-09-24 16:51:20 -04:00
Greyson Parrelli
864a1d5e93 Prefer remote value for profile sharing during storage sync. 2020-09-24 12:41:31 -04:00
Greyson Parrelli
9cf7eec247 Log sent timestamps when hitting message processing errors. 2020-09-24 12:26:18 -04:00
Greyson Parrelli
d9c15621f6 Log more details around conversation fetch times. 2020-09-24 12:26:02 -04:00
Greyson Parrelli
fea14218a9 Don't allow borderless images to have quotes attached.
Fixes #9924
2020-09-24 12:11:40 -04:00
Greyson Parrelli
dbbded5250 Bump version to 4.72.1 2020-09-24 10:54:07 -04:00
Greyson Parrelli
d65cfc7981 Updated language translations. 2020-09-24 10:53:44 -04:00
Greyson Parrelli
dc9124f291 Fix crash in RetrieveProfileJob. 2020-09-24 10:46:56 -04:00
Cody Henthorne
4cd433b6bc Retain call start timestamp per peer to prevent race conditions. 2020-09-24 10:43:39 -04:00
Greyson Parrelli
f9a9ee6b0c Bump version to 4.72.0 2020-09-23 16:54:38 -04:00
Greyson Parrelli
1741f7ed58 Updated language translations. 2020-09-23 16:54:38 -04:00
Alan Evans
d459c751be Show linked device update message if we don't have the capability to join a group by link. 2020-09-23 16:54:38 -04:00
Alan Evans
34ef8b52f6 Display a loading message if group update message is taking a while to load. 2020-09-23 16:54:38 -04:00
Alan Evans
5ae96905bb Do not allow replying on reactions and messages without visible content. 2020-09-23 16:54:38 -04:00
Alan Evans
b1fdbc0151 Refresh own GV2 capability on group create. 2020-09-23 16:54:38 -04:00
Alan Evans
a5ad27b5f2 Hide "My contacts" phone number privacy option. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
efcd5052a2 Remove Pixel 4 from the CameraX blacklist on Android 11. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
f2b10c0ba8 Always include ourselves in optimistic profile fetches. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
f182be2d79 Inline CDS feature flag. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
41b10630bb Default to WEBP for sticker contentTypes. 2020-09-23 16:54:38 -04:00
Alan Evans
45915bed90 Inline GV2 feature flag. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
a2c2ab428a Fallback to profile fetches for unlisted contacts. 2020-09-23 16:54:38 -04:00
Alan Evans
a05f74d302 Do not set color before profile name is known. 2020-09-23 16:54:38 -04:00
Alan Evans
74e94f3a97 Separate capability reads from writes and introduce gv2-2 write flag. 2020-09-23 16:54:38 -04:00
Christian Ascheberg
15ee8c6cac Fix timestamp of missed call record.
Fixes #7647
2020-09-23 16:54:38 -04:00
Alex Hart
18957b1f41 Remove members menu item for group message requests. 2020-09-23 16:54:38 -04:00
Cody Henthorne
29930cac41 Use mention-updated body for unread reaction notification text. 2020-09-23 16:54:38 -04:00
Cody Henthorne
e3338dc3ff Add MMS info to conversation settings. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
97b7b4a501 Fix crash when receiving SMS before finishing registration.
If someone has set Signal as the default SMS but has cleared data or
otherwise reset the app's storage state, it can get into a weird
situation. Notably, it'll crash because SmsReceiveJob.onRun() expects
Recipient.self() to be available.

However, it also makes it impossible to get the registration SMS,
because the app won't post a notification for the code.

This change will post notifications and SmsRetriever broadcasts for
relevant SMS messages.
2020-09-23 16:54:38 -04:00
Greyson Parrelli
b471a72856 Don't show the link preview megaphone to new users. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
fed7d911a3 Revert "Listen to the uiMode configuration changes."
This reverts commit dda98a474d.

This commit ended up causing spontaneous theme changes that have been
hard to track down. It's likely it just didn't fit into our theme system
well. We need to take a closer look, but in the meantime, a revert is in
order.
2020-09-23 16:54:38 -04:00
Cody Henthorne
ca442970a3 Add Research Megaphone. 2020-09-23 16:54:38 -04:00
Angus Turnbull
9dbb77c10a Remove some calls to GMS APIs for utility functions.
Fixes #9629
2020-09-23 16:54:38 -04:00
Dan
1116502bc0 Add vCard support for received MMS. 2020-09-23 16:54:38 -04:00
Cody Henthorne
edaf17bdd4 Fix invisible media controls and notch jank.
Fixes #9993.
2020-09-23 16:54:38 -04:00
Alan Evans
c61d731358 Allow side-by-side installation of staging build. 2020-09-23 16:54:38 -04:00
Cody Henthorne
a8415a3484 Add pre-join vanity view for 1:1 video calls. 2020-09-23 16:54:38 -04:00
Alan Evans
cd2467085e Correct storage query deleted filter argument. 2020-09-23 16:54:38 -04:00
Alan Evans
64efb3d2a4 Do not set or read reaction target phone number. 2020-09-23 16:54:38 -04:00
Alex Hart
e05f137bd8 Add animations to call screen. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
0c73ddc08b Ensure we HTML-decode the <title> tag.
Fixes #10020
2020-09-23 16:54:38 -04:00
Greyson Parrelli
19cc43c442 Add a charging constraint to the backup job. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
7108fc81a9 Prevent redundant JobScheduler jobs.
Some devices actually enforce a scheduling rate, and will crash if you
submit more than, say, 250 jobs in 1 minute. This can happen when
catching up with messages and scheduling a lot of
PushDecryptMessageJobs.

While it'd be tricky to limit jobs with constraints, this just does the
simple thing of not enqueueing unnecessary jobs for constraint-less
jobs.
2020-09-23 16:54:38 -04:00
Alex Hart
5943b9d7d6 Fix sending receipts.
Fixes #10016
2020-09-23 16:54:38 -04:00
Alex Hart
0271e4c918 Add lifecycle check in SnackbarAsyncTask. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
9dc33eff3a Remove thumbnails from the AttachmentDatabase.
Glide can do everything for us now, including video thumbnails.
2020-09-23 16:54:38 -04:00
Jim Gustafson
5aef1c8a68 Update to RingRTC v2.7.0 2020-09-23 16:54:38 -04:00
Alan Evans
c608a05270 Prevent a resolve call in main. 2020-09-23 16:54:38 -04:00
Cody Henthorne
e2cfd247c3 Fix mention parsing for quotes. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
97eb9154b2 Prevent NPE when setting sticker emoji. 2020-09-23 16:54:38 -04:00
Jim Gustafson
d7ff635445 RingRTC: Update to v2.6.0
Co-authored-by: Peter Thatcher <peter@signal.org>
2020-09-23 16:54:38 -04:00
Alan Evans
aff57fb54e Create the temporary backup file hidden in the final location.
Fixes #10003
2020-09-23 16:54:38 -04:00
Greyson Parrelli
e89285a219 Reduce log noise. 2020-09-23 16:54:38 -04:00
Greyson Parrelli
706f43caa8 Remove AttachmentsV3 feature flag. 2020-09-23 16:54:38 -04:00
Cody Henthorne
dc4faf57cb Add foundational UX and state support for Group Calling. 2020-09-23 16:54:38 -04:00
Alex Hart
7baf8052a2 Fix savedInstanceState crash. 2020-09-23 16:54:37 -04:00
Alan Evans
d3c59585fd Bump version to 4.71.5 2020-09-14 11:16:09 -03:00
Alan Evans
859bb8dc79 Updated language translations. 2020-09-14 11:12:30 -03:00
Alan Evans
58cd2e07ba Add some required face blurring models back.
Fixes #10009
2020-09-14 10:49:33 -03:00
Greyson Parrelli
a5a6fb590a Bump version to 4.71.4 2020-09-10 18:03:51 -04:00
Greyson Parrelli
3619993e68 Updated language translations. 2020-09-10 18:03:28 -04:00
Greyson Parrelli
88e12c78fa Disable mentions megaphone. 2020-09-10 17:54:57 -04:00
Alan Evans
5c285b4ac6 Cycle groups v2 feature flag. 2020-09-10 18:47:36 -03:00
Cody Henthorne
c6b729c470 Bump version to 4.71.3 2020-09-10 15:38:55 -04:00
Cody Henthorne
890014759e Updated language translations. 2020-09-10 15:38:12 -04:00
Cody Henthorne
68c1c43381 Update radio styling in storage settings. 2020-09-10 14:40:29 -04:00
Greyson Parrelli
d0dfcaaad5 Fix issue with storage key intersections.
- When doing the intersection, ignore keys that have type mismatches (same storageId, different types)
- If we detect that scenario, schedule a force push to happen afterwards
- Also schedule a force push afterwards if we detect that there's keys in the manifest that don't have any storage item on the service
2020-09-10 14:01:41 -04:00
Alan Evans
3cffaddc0a Validate incoming Group lengths and remote delete entries if wrong.
Ignore incoming messages with bad V1 group lengths.
2020-09-10 14:39:29 -03:00
Alex Hart
bf4cac0c82 Fix unarchive menu action. 2020-09-10 13:49:16 -03:00
Alex Hart
f680749a00 Use proper lifecycle for SimpleTask which touches fragment view. 2020-09-10 13:40:09 -03:00
Cody Henthorne
13a67980d9 Fix wrong timestamp being used when trimming by length. 2020-09-10 12:09:28 -04:00
Alan Evans
f110d595d2 Fix selection limit for add members GV1.
Fixes #10005
2020-09-10 09:59:57 -03:00
515 changed files with 18990 additions and 7354 deletions

View File

@@ -6,6 +6,7 @@ on:
branches:
- 'master'
- '4.**'
- '5.**'
jobs:
build:

18
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Reproducible Build Check
on:
schedule:
- cron: '0 5 * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build image
run: cd reproducible-builds && docker build -t signal-android . && cd ..
- name: Test build
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assembleRelease

2
.gitignore vendored
View File

@@ -23,5 +23,5 @@ ffpr
test/androidTestEspresso/res/values/arrays.xml
obj/
jni/libspeex/.deps/
*.sh
pkcs11.password
dev.keystore

View File

@@ -1,25 +0,0 @@
FROM ubuntu:17.10
RUN dpkg --add-architecture i386 && \
apt-get update -y && \
apt-get install -y software-properties-common && \
apt-get update -y && \
apt-get install -y libc6:i386=2.26-0ubuntu2.1 libncurses5:i386=6.0+20160625-1ubuntu1 libstdc++6:i386=7.2.0-8ubuntu3.2 lib32z1=1:1.2.11.dfsg-0ubuntu2 wget openjdk-8-jdk=8u171-b11-0ubuntu0.17.10.1 git unzip opensc pcscd && \
rm -rf /var/lib/apt/lists/* && \
apt-get autoremove -y && \
apt-get clean
ENV ANDROID_SDK_FILENAME android-sdk_r24.4.1-linux.tgz
ENV ANDROID_SDK_URL https://dl.google.com/android/${ANDROID_SDK_FILENAME}
ENV ANDROID_API_LEVELS android-28
ENV ANDROID_BUILD_TOOLS_VERSION 28.0.3
ENV ANDROID_HOME /usr/local/android-sdk-linux
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
RUN cd /usr/local/ && \
wget -q ${ANDROID_SDK_URL} && \
tar -xzf ${ANDROID_SDK_FILENAME} && \
rm ${ANDROID_SDK_FILENAME}
RUN echo y | android update sdk --no-ui -a --filter ${ANDROID_API_LEVELS}
RUN echo y | android update sdk --no-ui -a --filter extra-android-m2repository,extra-android-support,extra-google-google_play_services,extra-google-m2repository
RUN echo y | android update sdk --no-ui -a --filter tools,platform-tools,build-tools-${ANDROID_BUILD_TOOLS_VERSION}
RUN rm -rf ${ANDROID_HOME}/tools && unzip ${ANDROID_HOME}/temp/*.zip -d ${ANDROID_HOME}

View File

@@ -80,8 +80,8 @@ protobuf {
}
}
def canonicalVersionCode = 705
def canonicalVersionName = "4.71.2"
def canonicalVersionCode = 723
def canonicalVersionName = "4.74.2"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@@ -92,20 +92,29 @@ def abiPostFix = ['universal' : 0,
android {
flavorDimensions "none"
compileSdkVersion 28
buildToolsVersion '28.0.3'
compileSdkVersion 29
buildToolsVersion '29.0.3'
useLibrary 'org.apache.http.legacy'
dexOptions {
javaMaxHeapSize "4g"
}
signingConfigs {
staging {
storeFile file("${project.rootDir}/dev.keystore")
storePassword 'android'
keyAlias 'staging'
keyPassword 'android'
}
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion 19
targetSdkVersion 28
targetSdkVersion 29
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -123,9 +132,10 @@ android {
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
buildConfigField "String", "KBS_SERVICE_ID", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
@@ -165,7 +175,7 @@ android {
}
aaptOptions {
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!fssd_25_8bit_gray_v1.tflite:!fssd_25_8bit_v1.tflite:!blazeface.tfl'
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl'
}
buildTypes {
@@ -195,6 +205,8 @@ android {
}
staging {
initWith debug
applicationIdSuffix ".staging"
signingConfig signingConfigs.staging
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
@@ -203,8 +215,10 @@ android {
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", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
buildConfigField "String", "KBS_SERVICE_ID", "\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\""
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
"\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
}
@@ -301,6 +315,7 @@ dependencies {
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'com.google.android.exoplayer:extension-mediasession:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.0.0'
implementation 'org.signal:aesgcmprovider:0.0.3'
@@ -310,7 +325,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.5.1'
implementation 'org.signal:ringrtc-android:2.7.0'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
@@ -371,10 +386,11 @@ dependencies {
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
testImplementation 'androidx.test:core:1.2.0'
testImplementation ('org.robolectric:robolectric:4.2') {
testImplementation ('org.robolectric:robolectric:4.4') {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
testImplementation 'org.robolectric:shadows-multidex:4.2'
testImplementation 'org.robolectric:shadows-multidex:4.4'
testImplementation 'org.hamcrest:hamcrest:2.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
@@ -440,16 +456,6 @@ task signProductionWebsiteRelease {
}
}
tasks.whenTaskAdded { task ->
if (task.name.equals("assemblePlayRelease")) {
task.finalizedBy signProductionPlayRelease
}
if (task.name.equals("assembleWebsiteRelease")) {
task.finalizedBy signProductionWebsiteRelease
}
}
def getLastCommitTimestamp() {
new ByteArrayOutputStream().withStream { os ->
def result = exec {

View File

@@ -5,5 +5,12 @@
<application
android:name=".FlipperApplicationContext"
tools:replace="android:name"/>
tools:replace="android:name">
<activity
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" />
</application>
</manifest>

View File

@@ -5,7 +5,7 @@
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle" />
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
<permission android:name="${applicationId}.ACCESS_SECRETS"
android:label="Access to TextSecure Secrets"
android:protectionLevel="signature" />
@@ -36,7 +36,8 @@
<uses-permission android:name="android.permission.WRITE_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.CAMERA" />
@@ -113,7 +114,7 @@
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity"
<activity android:name=".WebRtcCallActivity"
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
android:excludeFromRecents="true"
android:screenOrientation="portrait"
@@ -127,25 +128,25 @@
android:theme="@style/TextSecure.DarkNoActionBar"
android:screenOrientation="portrait"
android:noHistory="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".InviteActivity"
android:theme="@style/Signal.Light.NoActionBar.Invite"
android:windowSoftInputMode="stateHidden"
android:parentActivityName=".MainActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.MainActivity" />
android:value=".MainActivity" />
</activity>
<activity android:name=".PromptMmsActivity"
android:label="Configure MMS Settings"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceProvisioningActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -155,7 +156,7 @@
</activity>
<activity android:name=".preferences.MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".sharing.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
@@ -164,7 +165,7 @@
android:taskAffinity=""
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/>
@@ -195,7 +196,7 @@
android:launchMode="singleTask"
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -223,6 +224,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="sgnl"
android:host="signal.group" />
</intent-filter>
<intent-filter android:autoVerify="true"
tools:targetApi="23">
<action android:name="android.intent.action.VIEW" />
@@ -242,7 +251,7 @@
<activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
@@ -257,16 +266,16 @@
android:taskAffinity=""
android:excludeFromRecents="true"
android:theme="@style/TextSecure.LightTheme.Popup"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" />
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<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|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
@@ -275,64 +284,64 @@
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
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|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".migrations.ApplicationMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseCreateActivity"
android:label="@string/AndroidManifest__create_passphrase"
android:windowSoftInputMode="stateUnchanged"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphrasePromptActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightIntroTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".NewConversationActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PushContactSelectionActivity"
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".giph.ui.GiphyActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.MediaSendActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseChangeActivity"
android:label="@string/AndroidManifest__change_passphrase"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".VerifyIdentityActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ApplicationPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.NOTIFICATION_PREFERENCES" />
@@ -343,45 +352,45 @@
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".revealable.ViewOnceMessageActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden"
android:excludeFromRecents="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".stickers.StickerManagementActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceActivity"
android:label="@string/AndroidManifest__linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".logsubmit.SubmitDebugLogActivity"
android:label="@string/AndroidManifest__log_submit"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaPreviewActivity"
android:label="@string/AndroidManifest__media_preview"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".AvatarPreviewActivity"
android:label="@string/AndroidManifest__media_preview"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediaoverview.MediaOverviewActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DummyActivity"
android:theme="@android:style/Theme.NoDisplay"
@@ -396,7 +405,7 @@
<activity android:name=".PlayServicesProblemActivity"
android:theme="@style/TextSecure.DialogActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".SmsSendtoActivity">
<intent-filter>
@@ -420,7 +429,7 @@
android:excludeFromRecents="true"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -432,15 +441,15 @@
<activity android:name=".mediasend.AvatarSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".BlockedContactsActivity"
android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".scribbles.ImageEditorStickerSelectActivity"
android:theme="@style/TextSecure.DarkTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".profiles.edit.EditProfileActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
@@ -449,16 +458,16 @@
<activity android:name=".lock.v2.CreateKbsPinActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".lock.v2.KbsMigrationActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ClearProfileAvatarActivity"
android:theme="@style/Theme.AppCompat.Dialog.Alert"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:icon="@drawable/clear_profile_avatar"
android:label="@string/AndroidManifest_remove_photo">
@@ -474,39 +483,39 @@
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.ContactShareEditActivity"
android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.ContactNameEditActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.SharedContactDetailsActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ShortcutLauncherActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity
android:name=".maps.PlacePickerActivity"
android:label="@string/PlacePickerActivity_title"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MainActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".pin.PinRestoreActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
@@ -521,18 +530,30 @@
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".megaphone.ClientDeprecatedActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
<service android:name=".components.voice.VoiceNotePlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service android:name=".service.QuickResponseService"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
android:exported="true" >
@@ -650,15 +671,15 @@
<provider android:name=".providers.PartProvider"
android:grantUriPermissions="true"
android:exported="false"
android:authorities="org.thoughtcrime.provider.securesms" />
android:authorities="${applicationId}.part" />
<provider android:name=".providers.MmsBodyProvider"
android:grantUriPermissions="true"
android:exported="false"
android:authorities="org.thoughtcrime.provider.securesms.mms" />
android:authorities="${applicationId}.mms" />
<provider android:name="androidx.core.content.FileProvider"
android:authorities="org.thoughtcrime.securesms.fileprovider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
@@ -667,23 +688,23 @@
</provider>
<provider android:name=".database.DatabaseContentProviders$Conversation"
android:authorities="org.thoughtcrime.securesms.database.conversation"
android:authorities="${applicationId}.database.conversation"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$ConversationList"
android:authorities="org.thoughtcrime.securesms.database.conversationlist"
android:authorities="${applicationId}.database.conversationlist"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Attachment"
android:authorities="org.thoughtcrime.securesms.database.attachment"
android:authorities="${applicationId}.database.attachment"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Sticker"
android:authorities="org.thoughtcrime.securesms.database.sticker"
android:authorities="${applicationId}.database.sticker"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$StickerPack"
android:authorities="org.thoughtcrime.securesms.database.stickerpack"
android:authorities="${applicationId}.database.stickerpack"
android:exported="false" />
<receiver android:name=".service.BootReceiver">

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.account.AccountAttributes;
public final class AppCapabilities {
@@ -9,12 +8,13 @@ public final class AppCapabilities {
}
private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
public static SignalServiceProfile.Capabilities getCapabilities(boolean storageCapable) {
return new SignalServiceProfile.Capabilities(UUID_CAPABLE, FeatureFlags.groupsV2(), storageCapable);
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable);
}
}

View File

@@ -137,7 +137,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
RegistrationUtil.maybeMarkRegistrationComplete(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
@@ -155,6 +154,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
Log.i(TAG, "App is now visible.");
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin();

View File

@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
@@ -64,6 +65,8 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
implements SharedPreferences.OnSharedPreferenceChangeListener
{
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
@SuppressWarnings("unused")
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
@@ -96,6 +99,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
initFragment(android.R.id.content, new BackupsPreferenceFragment());
} else if (icicle == null) {
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
}

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -10,12 +9,7 @@ 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;
import androidx.annotation.NonNull;
@@ -40,6 +34,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FullscreenHelper;
/**
* Activity for displaying avatars full screen.
@@ -81,17 +76,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
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);
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
Context context = getApplicationContext();
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
@@ -140,47 +125,13 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
toolbar.setTitle(recipient.getDisplayName(context));
});
avatar.setOnClickListener(v -> toggleUiVisibility());
FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
}
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
for (View view : views) {
view.animate()
.alpha(hide ? 0 : 1)
.start();
}
});
}
private void toggleUiVisibility() {
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
showSystemUI();
} else {
hideSystemUI();
}
}
private void hideSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN );
}
private void showSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
}
@Override
@@ -188,36 +139,4 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
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,10 +1,14 @@
package org.thoughtcrime.securesms;
import android.net.Uri;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -22,7 +26,8 @@ import java.util.Locale;
import java.util.Set;
public interface BindableConversationItem extends Unbindable {
void bind(@NonNull ConversationMessage messageRecord,
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@@ -49,6 +54,11 @@ public interface BindableConversationItem extends Unbindable {
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onVoiceNotePause(@NonNull Uri uri);
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);

View File

@@ -106,7 +106,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
public static final String MULTI_SELECT = "multi_select";
public static final String REFRESHABLE = "refreshable";
public static final String RECENTS = "recents";
public static final String TOTAL_CAPACITY = "total_capacity";
public static final String SELECTION_LIMIT = "selection_limit";
public static final String CURRENT_SELECTION = "current_selection";
private ConstraintLayout constraintLayout;
@@ -208,7 +208,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
selectionLimit = requireActivity().getIntent().getIntExtra(TOTAL_CAPACITY, NO_LIMIT);
selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
currentSelection = getCurrentSelection();
updateGroupLimit(getChipCount());

View File

@@ -0,0 +1,49 @@
package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
import java.util.Objects;
/**
* Used in our {@link BuildConfig} to tie together the various attributes of a KBS instance. This
* is sitting in the root directory so it can be accessed by the build config.
*/
public final class KbsEnclave {
private final String enclaveName;
private final String serviceId;
private final String mrEnclave;
public KbsEnclave(@NonNull String enclaveName, @NonNull String serviceId, @NonNull String mrEnclave) {
this.enclaveName = enclaveName;
this.serviceId = serviceId;
this.mrEnclave = mrEnclave;
}
public @NonNull String getMrEnclave() {
return mrEnclave;
}
public @NonNull String getEnclaveName() {
return enclaveName;
}
public @NonNull String getServiceId() {
return serviceId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KbsEnclave enclave = (KbsEnclave) o;
return enclaveName.equals(enclave.enclaveName) &&
serviceId.equals(enclave.serviceId) &&
mrEnclave.equals(enclave.mrEnclave);
}
@Override
public int hashCode() {
return Objects.hash(enclaveName, serviceId, mrEnclave);
}
}

View File

@@ -31,8 +31,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
@@ -69,8 +67,10 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.StorageUtil;
import java.util.HashMap;
import java.util.Locale;
@@ -119,6 +119,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private boolean cameFromAllMedia;
private boolean showThread;
private MediaDatabase.Sorting sorting;
private FullscreenHelper fullscreenHelper;
private @Nullable Cursor cursor = null;
@@ -133,7 +134,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
intent.setDataAndType(attachment.getDataUri(), mediaRecord.getContentType());
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
return intent;
}
@@ -147,10 +148,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
showSystemUI();
fullscreenHelper = new FullscreenHelper(this);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -261,6 +259,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
albumRail = findViewById(R.id.media_preview_album_rail);
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
albumRail.setItemAnimator(null); // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
albumRail.setAdapter(albumRailAdapter);
@@ -273,9 +272,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
anchorMarginsToBottomInsets(detailsContainer);
anchorMarginsToTopInsets(toolbarLayout);
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
}
private void initializeResources() {
@@ -386,21 +385,30 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (mediaItem != null) {
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
if (StorageUtil.canWriteToMediaStore()) {
performSavetoDisk(mediaItem);
return;
}
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
performSavetoDisk(mediaItem);
})
.execute();
});
}
}
private void performSavetoDisk(@NonNull MediaItem mediaItem) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
}
@SuppressLint("StaticFieldLeak")
private void deleteMedia() {
MediaItem mediaItem = getCurrentMediaItem();
@@ -546,7 +554,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
@Override
public boolean singleTapOnMedia() {
toggleUiVisibility();
fullscreenHelper.toggleUiVisibility();
return true;
}
@@ -556,32 +564,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
finish();
}
private void toggleUiVisibility() {
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
showSystemUI();
} else {
hideSystemUI();
}
}
private void hideSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN );
}
private void showSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
}
private class ViewPagerListener extends ExtendedOnPageChangedListener {
@Override
@@ -697,33 +679,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
});
}
private static void anchorMarginsToTopInsets(@NonNull View viewToAnchor) {
ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor, (view, insets) -> {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
layoutParams.setMargins(insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(),
layoutParams.bottomMargin);
view.setLayoutParams(layoutParams);
return insets;
});
}
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
for (View view : views) {
view.animate()
.alpha(hide ? 0 : 1)
.start();
}
});
}
private static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
@SuppressLint("UseSparseArrays")
@@ -801,7 +756,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
return new MediaItem(Recipient.live(recipientId).get(),
Recipient.live(threadRecipientId).get(),
attachment,
Objects.requireNonNull(attachment.getDataUri()),
Objects.requireNonNull(attachment.getUri()),
mediaRecord.getContentType(),
mediaRecord.getDate(),
mediaRecord.isOutgoing());

View File

@@ -33,6 +33,7 @@ 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.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -65,16 +66,17 @@ public class NewConversationActivity extends ContactSelectionActivity
launch(Recipient.resolved(recipientId.get()));
} else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
if (FeatureFlags.cds() && NetworkConstraint.isMet(this)) {
Log.i(TAG, "[onContactSelected] CDS enabled. Doing contact refresh.");
if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
Log.i(TAG, "[onContactSelected] 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.");
if (!resolved.isRegistered() || !resolved.hasUuid()) {
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
try {
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
resolved = Recipient.resolved(resolved.getId());
@@ -102,7 +104,7 @@ public class NewConversationActivity extends ContactSelectionActivity
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
intent.setDataAndType(getIntent().getData(), getIntent().getType());
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);

View File

@@ -48,7 +48,7 @@ public class SmsSendtoActivity extends Activity {
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
} else {
Recipient recipient = Recipient.external(this, destination.getDestination());
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
nextIntent = new Intent(this, ConversationActivity.class);
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());

View File

@@ -22,6 +22,7 @@ import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
@@ -56,6 +57,7 @@ import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
@@ -90,11 +92,13 @@ import org.whispersystems.libsignal.fingerprint.Fingerprint;
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.UUID;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
@@ -307,16 +311,26 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
byte[] localId;
byte[] remoteId;
if (FeatureFlags.verifyV2() && recipient.resolve().getUuid().isPresent()) {
Recipient resolved = recipient.resolve();
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
Log.i(TAG, "Using UUID (version 2).");
version = 2;
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
remoteId = UuidUtil.toByteArray(recipient.resolve().getUuid().get());
} else {
remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
Log.i(TAG, "Using E164 (version 1).");
version = 1;
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
remoteId = recipient.resolve().requireE164().getBytes();
remoteId = resolved.requireE164().getBytes();
} else {
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getUuid().isPresent(), resolved.getE164().isPresent()));
new AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
.setOnDismissListener(dialog -> requireActivity().finish())
.show();
return;
}
this.recipient.observe(this, this::setRecipientText);

View File

@@ -19,8 +19,6 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.app.PictureInPictureParams;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
@@ -41,9 +39,11 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
@@ -52,11 +52,12 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
@@ -85,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
//noinspection ConstantConditions
getSupportActionBar().hide();
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
@@ -124,6 +126,11 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
if (!isInPipMode()) {
EventBus.getDefault().unregister(this);
}
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
finish();
}
}
@Override
@@ -132,11 +139,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
super.onStop();
EventBus.getDefault().unregister(this);
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
startService(intent);
}
}
@Override
@@ -162,7 +171,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
private boolean enterPipModeIfPossible() {
if (isSystemPipEnabledAndAvailable()) {
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(9, 16))
.build();
@@ -196,21 +205,18 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
private void initializeResources() {
callScreen = ViewUtil.findById(this, R.id.callScreen);
callScreen = findViewById(R.id.callScreen);
callScreen.setControlsListener(new ControlsListener());
}
private void initializeViewModel() {
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
viewModel.setIsInPipMode(isInPipMode());
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime);
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard);
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
}
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
@@ -375,19 +381,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
startService(intent);
}
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
callScreen.setRecipient(event.getRecipient());
}
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
callScreen.setRecipient(event.getRecipient());
private void handleOutgoingCall() {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
}
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
Log.i(TAG, "handleTerminate called: " + hangupType.name());
callScreen.setRecipient(recipient);
callScreen.setStatusFromHangupType(hangupType);
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
@@ -398,62 +398,47 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
delayedFinish();
}
private void handleCallRinging(@NonNull WebRtcViewModel event) {
callScreen.setRecipient(event.getRecipient());
private void handleCallRinging() {
callScreen.setStatus(getString(R.string.RedPhone_ringing));
}
private void handleCallBusy(@NonNull WebRtcViewModel event) {
private void handleCallBusy() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_busy));
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
}
private void handleCallConnected(@NonNull WebRtcViewModel event) {
private void handleCallConnected() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
callScreen.setRecipient(event.getRecipient());
}
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
private void handleRecipientUnavailable() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
delayedFinish();
}
private void handleServerFailure(@NonNull WebRtcViewModel event) {
private void handleServerFailure() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish();
}
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.RedPhone_number_not_registered);
dialog.setIconAttribute(R.attr.dialog_alert_icon);
dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice);
dialog.setCancelable(true);
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.show();
new AlertDialog.Builder(this)
.setTitle(R.string.RedPhone_number_not_registered)
.setIconAttribute(R.attr.dialog_alert_icon)
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
.setCancelable(true)
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.show();
}
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
final IdentityKey theirKey = event.getIdentityKey();
final Recipient recipient = event.getRecipient();
final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
if (theirKey == null) {
handleTerminate(recipient, HangupMessage.Type.NORMAL);
@@ -493,32 +478,29 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(final WebRtcViewModel event) {
public void onEventMainThread(@NonNull WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event);
viewModel.setRecipient(event.getRecipient());
callScreen.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_CONNECTED: handleCallConnected(); break;
case NETWORK_FAILURE: handleServerFailure(); break;
case CALL_RINGING: handleCallRinging(); 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 RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
case CALL_OUTGOING: handleOutgoingCall(); break;
case CALL_BUSY: handleCallBusy(); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
}
callScreen.setLocalRenderer(event.getLocalRenderer());
callScreen.setRemoteRenderer(event.getRemoteRenderer());
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
viewModel.updateFromWebRtcViewModel(event, enableVideo);
@@ -530,6 +512,24 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private final class ControlsListener implements WebRtcCallView.ControlsListener {
@Override
public void onStartCall(boolean isVideoCall) {
enableVideoIfAvailable = isVideoCall;
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
startService(intent);
MessageSender.onMessageSent();
}
@Override
public void onCancelStartCall() {
finish();
}
@Override
public void onControlsFadeOut() {
if (videoTooltip != null) {
@@ -594,8 +594,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
@Override
public void onDownCaretPressed() {
public void onShowParticipantsList() {
CallParticipantsListDialog.show(getSupportFragmentManager());
}
@Override
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
viewModel.setIsViewingFocusedParticipant(page);
}
}

View File

@@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.animation;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import androidx.annotation.NonNull;
public class ResizeAnimation extends Animation {
private final View target;
private final int targetWidthPx;
private final int targetHeightPx;
private int startWidth;
private int startHeight;
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
this.target = target;
this.targetWidthPx = targetWidthPx;
this.targetHeightPx = targetHeightPx;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
ViewGroup.LayoutParams params = target.getLayoutParams();
params.width = newWidth;
params.height = newHeight;
target.setLayoutParams(params);
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
this.startWidth = width;
this.startHeight = height;
}
@Override
public boolean willChangeBounds() {
return true;
}
}

View File

@@ -106,10 +106,7 @@ public abstract class Attachment {
}
@Nullable
public abstract Uri getDataUri();
@Nullable
public abstract Uri getThumbnailUri();
public abstract Uri getUri();
public int getTransferState() {
return transferState;

View File

@@ -57,7 +57,7 @@ public class DatabaseAttachment extends Attachment {
@Override
@Nullable
public Uri getDataUri() {
public Uri getUri() {
if (hasData) {
return PartAuthority.getAttachmentDataUri(attachmentId);
} else {
@@ -65,16 +65,6 @@ public class DatabaseAttachment extends Attachment {
}
}
@Override
@Nullable
public Uri getThumbnailUri() {
if (hasThumbnail) {
return PartAuthority.getAttachmentThumbnailUri(attachmentId);
} else {
return null;
}
}
public AttachmentId getAttachmentId() {
return attachmentId;
}

View File

@@ -15,13 +15,7 @@ public class MmsNotificationAttachment extends Attachment {
@Nullable
@Override
public Uri getDataUri() {
return null;
}
@Nullable
@Override
public Uri getThumbnailUri() {
public Uri getUri() {
return null;
}

View File

@@ -42,17 +42,10 @@ public class PointerAttachment extends Attachment {
@Nullable
@Override
public Uri getDataUri() {
public Uri getUri() {
return null;
}
@Nullable
@Override
public Uri getThumbnailUri() {
return null;
}
public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) {
List<Attachment> results = new LinkedList<>();

View File

@@ -20,12 +20,7 @@ public class TombstoneAttachment extends Attachment {
}
@Override
public @Nullable Uri getDataUri() {
return null;
}
@Override
public @Nullable Uri getThumbnailUri() {
public @Nullable Uri getUri() {
return null;
}
}

View File

@@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri;
private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri,
@NonNull String contentType,
@@ -29,11 +28,10 @@ public class UriAttachment extends Attachment {
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
this(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,
@@ -51,22 +49,15 @@ public class UriAttachment extends Attachment {
@Nullable TransformProperties 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;
this.dataUri = dataUri;
}
@Override
@NonNull
public Uri getDataUri() {
public Uri getUri() {
return dataUri;
}
@Override
@Nullable
public Uri getThumbnailUri() {
return thumbnailUri;
}
@Override
public boolean equals(Object other) {
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);

View File

@@ -1,378 +0,0 @@
package org.thoughtcrime.securesms.audio;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Pair;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.lang.ref.WeakReference;
public class AudioSlidePlayer implements SensorEventListener {
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
private final @NonNull Context context;
private final @NonNull AudioSlide slide;
private final @NonNull Handler progressEventHandler;
private final @NonNull AudioManager audioManager;
private final @NonNull SensorManager sensorManager;
private final @NonNull Sensor proximitySensor;
private final @Nullable WakeLock wakeLock;
private @NonNull WeakReference<Listener> listener;
private @Nullable SimpleExoPlayer mediaPlayer;
private long startTime;
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
playing.get().setListener(listener);
return playing.get();
} else {
return new AudioSlidePlayer(context, slide, listener);
}
}
private AudioSlidePlayer(@NonNull Context context,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
this.context = context;
this.slide = slide;
this.listener = new WeakReference<>(listener);
this.progressEventHandler = new ProgressEventHandler(this);
this.audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
this.sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
if (Build.VERSION.SDK_INT >= 21) {
this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
} else {
this.wakeLock = null;
}
}
public void play(final double progress) throws IOException {
play(progress, false);
}
private void play(final double progress, boolean earpiece) throws IOException {
if (this.mediaPlayer != null) {
return;
}
if (slide.getUri() == null) {
throw new IOException("Slide has no URI!");
}
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl();
this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
this.startTime = System.currentTimeMillis();
mediaPlayer.prepare(createMediaSource(slide.getUri()));
mediaPlayer.setPlayWhenReady(true);
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
.setContentType(earpiece ? C.CONTENT_TYPE_SPEECH : C.CONTENT_TYPE_MUSIC)
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
.build());
mediaPlayer.addListener(new Player.EventListener() {
boolean started = false;
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
switch (playbackState) {
case Player.STATE_READY:
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
synchronized (AudioSlidePlayer.this) {
if (mediaPlayer == null) return;
if (started) {
Log.d(TAG, "Already started. Ignoring.");
return;
}
started = true;
if (progress > 0) {
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
}
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
setPlaying(AudioSlidePlayer.this);
}
notifyOnStart();
progressEventHandler.sendEmptyMessage(0);
break;
case Player.STATE_ENDED:
Log.i(TAG, "onComplete");
synchronized (AudioSlidePlayer.this) {
mediaPlayer = null;
sensorManager.unregisterListener(AudioSlidePlayer.this);
if (wakeLock != null && wakeLock.isHeld()) {
if (Build.VERSION.SDK_INT >= 21) {
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
}
}
}
notifyOnStop();
progressEventHandler.removeMessages(0);
}
}
@Override
public void onPlayerError(ExoPlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error);
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
synchronized (AudioSlidePlayer.this) {
mediaPlayer = null;
sensorManager.unregisterListener(AudioSlidePlayer.this);
if (wakeLock != null && wakeLock.isHeld()) {
if (Build.VERSION.SDK_INT >= 21) {
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
}
}
}
notifyOnStop();
progressEventHandler.removeMessages(0);
}
});
}
private MediaSource createMediaSource(@NonNull Uri uri) {
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
return new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
.setExtractorsFactory(extractorsFactory)
.createMediaSource(uri);
}
public synchronized void stop() {
Log.i(TAG, "Stop called!");
removePlaying(this);
if (this.mediaPlayer != null) {
this.mediaPlayer.stop();
this.mediaPlayer.release();
}
sensorManager.unregisterListener(AudioSlidePlayer.this);
this.mediaPlayer = null;
}
public synchronized static void stopAll() {
if (playing.isPresent()) {
playing.get().stop();
}
}
public void setListener(@NonNull Listener listener) {
this.listener = new WeakReference<>(listener);
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
notifyOnStart();
}
}
public @NonNull AudioSlide getAudioSlide() {
return slide;
}
private Pair<Double, Integer> getProgress() {
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
return new Pair<>(0D, 0);
} else {
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
(int) mediaPlayer.getCurrentPosition());
}
}
private void notifyOnStart() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStart();
}
});
}
private void notifyOnStop() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStop();
}
});
}
private void notifyOnProgress(final double progress, final long millis) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onProgress(progress, millis);
}
});
}
private @NonNull Listener getListener() {
Listener listener = this.listener.get();
if (listener != null) return listener;
else return new Listener() {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(double progress, long millis) {}
};
}
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() != player) {
playing.get().notifyOnStop();
playing.get().stop();
}
playing = Optional.of(player);
}
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() == player) {
playing = Optional.absent();
}
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) return;
if (mediaPlayer == null || mediaPlayer.getPlaybackState() != Player.STATE_READY) return;
int streamType;
if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
streamType = AudioManager.STREAM_VOICE_CALL;
} else {
streamType = AudioManager.STREAM_MUSIC;
}
if (streamType == AudioManager.STREAM_VOICE_CALL &&
mediaPlayer.getAudioStreamType() != streamType &&
!audioManager.isWiredHeadsetOn())
{
double position = mediaPlayer.getCurrentPosition();
double duration = mediaPlayer.getDuration();
double progress = position / duration;
if (wakeLock != null) wakeLock.acquire();
stop();
try {
play(progress, true);
} catch (IOException e) {
Log.w(TAG, e);
}
} else if (streamType == AudioManager.STREAM_MUSIC &&
mediaPlayer.getAudioStreamType() != streamType &&
System.currentTimeMillis() - startTime > 500)
{
if (wakeLock != null) wakeLock.release();
stop();
notifyOnStop();
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
public interface Listener {
void onStart();
void onStop();
void onProgress(double progress, long millis);
}
private static class ProgressEventHandler extends Handler {
private final WeakReference<AudioSlidePlayer> playerReference;
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
this.playerReference = new WeakReference<>(player);
}
@Override
public void handleMessage(Message msg) {
AudioSlidePlayer player = playerReference.get();
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
return;
}
Pair<Double, Integer> progress = player.getProgress();
player.notifyOnProgress(progress.first, progress.second);
sendEmptyMessageDelayed(0, 50);
}
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
}
}
}

View File

@@ -4,6 +4,8 @@ package org.thoughtcrime.securesms.backup;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
@@ -13,10 +15,15 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil;
@@ -24,27 +31,53 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import java.util.Objects;
public class BackupDialog {
public static void showEnableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
private static final String TAG = Log.tag(BackupDialog.class);
public static void showEnableBackupDialog(@NonNull Context context,
@Nullable Intent backupDirectorySelectionIntent,
@Nullable String backupDirectoryDisplayName,
@NonNull Runnable onBackupsEnabled)
{
String[] password = BackupUtil.generateBackupPassphrase();
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_enable_local_backups)
.setView(R.layout.backup_enable_dialog)
.setView(backupDirectorySelectionIntent != null ? R.layout.backup_enable_dialog_v29 : R.layout.backup_enable_dialog)
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(created -> {
if (backupDirectoryDisplayName != null) {
TextView folderName = dialog.findViewById(R.id.backup_enable_dialog_folder_name);
if (folderName != null) {
folderName.setText(backupDirectoryDisplayName);
}
}
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(v -> {
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
if (confirmationCheckBox.isChecked()) {
if (backupDirectorySelectionIntent != null && backupDirectorySelectionIntent.getData() != null) {
Uri backupDirectoryUri = backupDirectorySelectionIntent.getData();
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
context.getContentResolver()
.takePersistableUriPermission(backupDirectoryUri, takeFlags);
}
BackupPassphrase.set(context, Util.join(password, " "));
TextSecurePreferences.setNextBackupTime(context, 0);
TextSecurePreferences.setBackupEnabled(context, true);
LocalBackupListener.schedule(context);
preference.setChecked(true);
onBackupsEnabled.run();
created.dismiss();
} else {
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
@@ -75,16 +108,38 @@ public class BackupDialog {
}
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
@RequiresApi(29)
public static void showChooseBackupLocationDialog(@NonNull Fragment fragment, int requestCode) {
new AlertDialog.Builder(fragment.requireContext())
.setView(R.layout.backup_choose_location_dialog)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
dialog.dismiss();
})
.setPositiveButton(R.string.BackupDialog_choose_folder, ((dialog, which) -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
fragment.startActivityForResult(intent, requestCode);
dialog.dismiss();
}))
.create()
.show();
}
public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) {
new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_delete_backups)
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
BackupPassphrase.set(context, null);
TextSecurePreferences.setBackupEnabled(context, false);
BackupUtil.deleteAllBackups();
preference.setChecked(false);
BackupUtil.disableBackups(context);
onBackupsDisabled.run();
})
.create()
.show();

View File

@@ -3,9 +3,12 @@ package org.thoughtcrime.securesms.backup;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import androidx.annotation.RequiresApi;
import androidx.documentfile.provider.DocumentFile;
import com.annimon.stream.function.Consumer;
import com.annimon.stream.function.Predicate;
@@ -50,6 +53,7 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.crypto.BadPaddingException;
@@ -84,7 +88,32 @@ public class FullBackupExporter extends FullBackupBase {
@NonNull String passphrase)
throws IOException
{
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase);
try (OutputStream outputStream = new FileOutputStream(output)) {
internalExport(context, attachmentSecret, input, outputStream, passphrase);
}
}
@RequiresApi(29)
public static void export(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase input,
@NonNull DocumentFile output,
@NonNull String passphrase)
throws IOException
{
try (OutputStream outputStream = Objects.requireNonNull(context.getContentResolver().openOutputStream(output.getUri()))) {
internalExport(context, attachmentSecret, input, outputStream, passphrase);
}
}
private static void internalExport(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase input,
@NonNull OutputStream fileOutputStream,
@NonNull String passphrase)
throws IOException
{
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
int count = 0;
try {
@@ -322,7 +351,7 @@ public class FullBackupExporter extends FullBackupBase {
private byte[] iv;
private int counter;
private BackupFrameOutputStream(@NonNull File output, @NonNull String passphrase) throws IOException {
private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException {
try {
byte[] salt = Util.getSecretBytes(32);
byte[] key = getBackupKey(passphrase, salt);
@@ -334,7 +363,7 @@ public class FullBackupExporter extends FullBackupBase {
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
this.mac = Mac.getInstance("HmacSHA256");
this.outputStream = new FileOutputStream(output);
this.outputStream = output;
this.iv = Util.getSecretBytes(16);
this.counter = Conversions.byteArrayToInt(iv);

View File

@@ -6,9 +6,11 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import androidx.annotation.NonNull;
import android.net.Uri;
import android.util.Pair;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
@@ -25,8 +27,8 @@ import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -36,7 +38,6 @@ import org.whispersystems.libsignal.util.ByteUtil;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -46,6 +47,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -61,13 +63,14 @@ public class FullBackupImporter extends FullBackupBase {
private static final String TAG = FullBackupImporter.class.getSimpleName();
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase db, @NonNull File file, @NonNull String passphrase)
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
throws IOException
{
BackupRecordInputStream inputStream = new BackupRecordInputStream(file, passphrase);
int count = 0;
int count = 0;
try (InputStream is = getInputStream(context, uri)) {
BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase);
try {
db.beginTransaction();
dropAllTables(db);
@@ -93,6 +96,14 @@ public class FullBackupImporter extends FullBackupBase {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count));
}
private static @NonNull InputStream getInputStream(@NonNull Context context, @NonNull Uri uri) throws IOException{
if (BackupUtil.isUserSelectionRequired(context)) {
return Objects.requireNonNull(context.getContentResolver().openInputStream(uri));
} else {
return new FileInputStream(new File(Objects.requireNonNull(uri.getPath())));
}
}
private static void processVersion(@NonNull SQLiteDatabase db, DatabaseVersion version) throws IOException {
if (version.getVersion() > db.getVersion()) {
throw new DatabaseDowngradeException(db.getVersion(), version.getVersion());
@@ -138,13 +149,11 @@ public class FullBackupImporter extends FullBackupBase {
inputStream.readAttachmentTo(output.second, attachment.getLength());
contentValues.put(AttachmentDatabase.DATA, dataFile.getAbsolutePath());
contentValues.put(AttachmentDatabase.THUMBNAIL, (String)null);
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first);
} catch (BadMacException e) {
Log.w(TAG, "Bad MAC for attachment " + attachment.getAttachmentId() + "! Can't restore it.", e);
dataFile.delete();
contentValues.put(AttachmentDatabase.DATA, (String) null);
contentValues.put(AttachmentDatabase.THUMBNAIL, (String) null);
contentValues.put(AttachmentDatabase.DATA_RANDOM, (String) null);
}
@@ -223,9 +232,9 @@ public class FullBackupImporter extends FullBackupBase {
private byte[] iv;
private int counter;
private BackupRecordInputStream(@NonNull File file, @NonNull String passphrase) throws IOException {
private BackupRecordInputStream(@NonNull InputStream in, @NonNull String passphrase) throws IOException {
try {
this.in = new FileInputStream(file);
this.in = in;
byte[] headerLengthBytes = new byte[4];
Util.readFully(in, headerLengthBytes);

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.color;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import androidx.annotation.ColorInt;
@@ -8,6 +9,7 @@ import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.HashMap;
import java.util.Map;
@@ -68,6 +70,11 @@ public enum MaterialColor {
this.serialized = serialized;
}
public @ColorInt int toNotificationColor(@NonNull Context context) {
final boolean isDark = ThemeUtil.isDarkNotificationTheme(context);
return context.getResources().getColor(isDark ? shadeColor : mainColor);
}
public @ColorInt int toConversationColor(@NonNull Context context) {
return context.getResources().getColor(mainColor);
}

View File

@@ -5,6 +5,7 @@ import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
@@ -16,6 +17,8 @@ import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.lifecycle.Observer;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
@@ -28,19 +31,17 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.audio.AudioWaveForm;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public final class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
public final class AudioView extends FrameLayout {
private static final String TAG = AudioView.class.getSimpleName();
@@ -62,11 +63,14 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
@ColorInt private final int waveFormUnplayedBarsColor;
@Nullable private SlideClickListener downloadListener;
@Nullable private AudioSlidePlayer audioSlidePlayer;
private int backwardsCounter;
private int lottieDirection;
private boolean isPlaying;
private long durationMillis;
private AudioSlide audioSlide;
private Callbacks callbacks;
private final Observer<VoiceNotePlaybackState> playbackStateObserver = this::onPlaybackState;
public AudioView(Context context) {
this(context, null);
@@ -122,11 +126,18 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
EventBus.getDefault().unregister(this);
}
public Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
return playbackStateObserver;
}
public void setAudio(final @NonNull AudioSlide audio,
final @Nullable Callbacks callbacks,
final boolean showControls)
{
this.callbacks = callbacks;
if (seekBar instanceof WaveFormSeekBarView) {
if (audioSlidePlayer != null && !Objects.equals(audioSlidePlayer.getAudioSlide().getUri(), audio.getUri())) {
if (audioSlide != null && !Objects.equals(audioSlide.getUri(), audio.getUri())) {
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
waveFormView.setWaveMode(false);
seekBar.setProgress(0);
@@ -147,12 +158,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
seekBar.setEnabled(true);
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
showPlayButton();
lottieDirection = REVERSE;
playPauseButton.cancelAnimation();
playPauseButton.setFrame(0);
}
this.audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
this.audioSlide = audio;
if (seekBar instanceof WaveFormSeekBarView) {
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
@@ -177,28 +185,47 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
}
}
public void cleanup() {
if (this.audioSlidePlayer != null && isPlaying) {
this.audioSlidePlayer.stop();
}
}
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
this.downloadListener = listener;
}
@Override
public void onStart() {
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset());
onProgress(voiceNotePlaybackState.getUri(),
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
voiceNotePlaybackState.getPlayheadPositionMillis());
}
private void onStart(@NonNull Uri uri, boolean autoReset) {
if (!Objects.equals(uri, audioSlide.getUri())) {
if (audioSlide != null && audioSlide.getUri() != null) {
onStop(audioSlide.getUri(), autoReset);
}
return;
}
if (isPlaying) {
return;
}
isPlaying = true;
togglePlayToPause();
}
@Override
public void onStop() {
private void onStop(@NonNull Uri uri, boolean autoReset) {
if (!Objects.equals(uri, audioSlide.getUri())) {
return;
}
if (!isPlaying) {
return;
}
isPlaying = false;
togglePauseToPlay();
if (autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) {
if (autoReset || autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) {
backwardsCounter = 4;
rewind();
}
@@ -230,8 +257,11 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
this.downloadButton.setEnabled(enabled);
}
@Override
public void onProgress(double progress, long millis) {
private void onProgress(@NonNull Uri uri, double progress, long millis) {
if (audioSlide == null || !Objects.equals(uri, audioSlide.getUri())) {
return;
}
int seekProgress = (int) Math.floor(progress * seekBar.getMax());
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
@@ -312,37 +342,27 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
}
public void stopPlaybackAndReset() {
if (this.audioSlidePlayer != null && isPlaying) {
this.audioSlidePlayer.stop();
togglePauseToPlay();
if (audioSlide == null || audioSlide.getUri() == null) return;
if (callbacks != null) {
callbacks.onStopAndReset(audioSlide.getUri());
rewind();
}
rewind();
}
private class PlayPauseClickedListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (lottieDirection == REVERSE) {
try {
Log.d(TAG, "playbutton onClick");
if (audioSlidePlayer != null) {
togglePlayToPause();
audioSlidePlayer.play(getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
if (audioSlide == null || audioSlide.getUri() == null) return;
if (callbacks != null) {
if (lottieDirection == REVERSE) {
callbacks.onPlay(audioSlide.getUri(), getProgress());
} else {
callbacks.onPause(audioSlide.getUri());
}
} else {
Log.d(TAG, "pausebutton onClick");
if (audioSlidePlayer != null) {
togglePauseToPlay();
audioSlidePlayer.stop();
if (autoRewind) {
rewind();
}
}
}
};
}
}
@@ -370,28 +390,28 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser && durationMillis > 0) {
float progressFloat = progress / (float) seekBar.getMax();
updateProgress(progressFloat, (long) (durationMillis * progressFloat));
}
}
@Override
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
if (audioSlide == null || audioSlide.getUri() == null) return;
wasPlaying = isPlaying;
if (audioSlidePlayer != null && isPlaying) {
audioSlidePlayer.stop();
if (isPlaying) {
if (callbacks != null) {
callbacks.onPause(audioSlide.getUri());
}
}
}
@Override
public synchronized void onStopTrackingTouch(SeekBar seekBar) {
try {
if (audioSlidePlayer != null && wasPlaying) {
audioSlidePlayer.play(getProgress());
if (audioSlide == null || audioSlide.getUri() == null) return;
if (callbacks != null) {
if (wasPlaying) {
callbacks.onSeekTo(audioSlide.getUri(), getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
@@ -405,9 +425,15 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventAsync(final PartProgressEvent event) {
if (audioSlidePlayer != null && event.attachment.equals(audioSlidePlayer.getAudioSlide().asAttachment())) {
if (audioSlide != null && event.attachment.equals(audioSlide.asAttachment())) {
circleProgress.setInstantProgress(((float) event.progress) / event.total);
}
}
public interface Callbacks {
void onPlay(@NonNull Uri audioUri, double progress);
void onPause(@NonNull Uri audioUri);
void onSeekTo(@NonNull Uri audioUri, double progress);
void onStopAndReset(@NonNull Uri audioUri);
}
}

View File

@@ -54,7 +54,7 @@ public class BorderlessImageView extends FrameLayout {
}
public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
boolean showControls = slide.asAttachment().getDataUri() == null;
boolean showControls = slide.asAttachment().getUri() == null;
if (slide.hasSticker()) {
image.setFit(new CenterInside());

View File

@@ -46,7 +46,8 @@ import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
public class ComposeText extends EmojiEditText {
private CharSequence combinedHint;
private CharSequence hint;
private SpannableString subHint;
private MentionRendererDelegate mentionRendererDelegate;
private MentionValidatorWatcher mentionValidatorWatcher;
@@ -84,8 +85,14 @@ public class ComposeText extends EmojiEditText {
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!TextUtils.isEmpty(combinedHint)) {
setHint(combinedHint);
if (!TextUtils.isEmpty(hint)) {
if (!TextUtils.isEmpty(subHint)) {
setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint))
.append("\n")
.append(ellipsizeToWidth(subHint)));
} else {
setHint(ellipsizeToWidth(hint));
}
}
}
@@ -93,7 +100,7 @@ public class ComposeText extends EmojiEditText {
protected void onSelectionChanged(int selectionStart, int selectionEnd) {
super.onSelectionChanged(selectionStart, selectionEnd);
if (FeatureFlags.mentions() && getText() != null) {
if (getText() != null) {
boolean selectionChanged = changeSelectionForPartialMentions(getText(), selectionStart, selectionEnd);
if (selectionChanged) {
return;
@@ -143,18 +150,24 @@ public class ComposeText extends EmojiEditText {
}
public void setHint(@NonNull String hint, @Nullable CharSequence subHint) {
if (subHint != null) {
Spannable subHintSpannable = new SpannableString(subHint);
subHintSpannable.setSpan(new RelativeSizeSpan(0.5f), 0, subHintSpannable.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
this.hint = hint;
combinedHint = new SpannableStringBuilder().append(ellipsizeToWidth(hint))
.append("\n")
.append(ellipsizeToWidth(subHintSpannable));
if (subHint != null) {
this.subHint = new SpannableString(subHint);
this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
} else {
combinedHint = ellipsizeToWidth(hint);
this.subHint = null;
}
super.setHint(combinedHint);
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(hint);
}
public void appendInvite(String invite) {
@@ -179,9 +192,7 @@ public class ComposeText extends EmojiEditText {
}
public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
if (FeatureFlags.mentions()) {
mentionValidatorWatcher.setMentionValidator(mentionValidator);
}
mentionValidatorWatcher.setMentionValidator(mentionValidator);
}
private boolean isLandscape() {
@@ -248,11 +259,9 @@ public class ComposeText extends EmojiEditText {
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ThemeUtil.getThemedColor(getContext(), R.attr.conversation_mention_background_color));
if (FeatureFlags.mentions()) {
addTextChangedListener(new MentionDeleter());
mentionValidatorWatcher = new MentionValidatorWatcher();
addTextChangedListener(mentionValidatorWatcher);
}
addTextChangedListener(new MentionDeleter());
mentionValidatorWatcher = new MentionValidatorWatcher();
addTextChangedListener(mentionValidatorWatcher);
}
private boolean changeSelectionForPartialMentions(@NonNull Spanned spanned, int selectionStart, int selectionEnd) {

View File

@@ -35,6 +35,7 @@ public class ConversationItemFooter extends LinearLayout {
private ExpirationTimerView timerView;
private ImageView insecureIndicatorView;
private DeliveryStatusView deliveryStatusView;
private boolean onlyShowSendingStatus;
public ConversationItemFooter(Context context) {
super(context);
@@ -93,6 +94,11 @@ public class ConversationItemFooter extends LinearLayout {
deliveryStatusView.setTint(color);
}
public void setOnlyShowSendingStatus(boolean onlyShowSending, MessageRecord messageRecord) {
this.onlyShowSendingStatus = onlyShowSending;
presentDeliveryStatus(messageRecord);
}
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
dateView.forceLayout();
if (messageRecord.isFailed()) {
@@ -173,14 +179,29 @@ public class ConversationItemFooter extends LinearLayout {
}
private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) {
if (!messageRecord.isFailed() && !messageRecord.isPendingInsecureSmsFallback()) {
if (!messageRecord.isOutgoing()) deliveryStatusView.setNone();
else if (messageRecord.isPending()) deliveryStatusView.setPending();
else if (messageRecord.isRemoteRead()) deliveryStatusView.setRead();
else if (messageRecord.isDelivered()) deliveryStatusView.setDelivered();
else deliveryStatusView.setSent();
} else {
if (messageRecord.isFailed() || messageRecord.isPendingInsecureSmsFallback()) {
deliveryStatusView.setNone();
return;
}
if (onlyShowSendingStatus) {
if (messageRecord.isOutgoing() && messageRecord.isPending()) {
deliveryStatusView.setPending();
} else {
deliveryStatusView.setNone();
}
} else {
if (!messageRecord.isOutgoing()) {
deliveryStatusView.setNone();
} else if (messageRecord.isPending()) {
deliveryStatusView.setPending();
} else if (messageRecord.isRemoteRead()) {
deliveryStatusView.setRead();
} else if (messageRecord.isDelivered()) {
deliveryStatusView.setDelivered();
} else {
deliveryStatusView.setSent();
}
}
}
}

View File

@@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.components;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ThemeUtil;
/**
* Base dialog fragment for rendering as a full screen dialog with animation
* transitions.
*/
public abstract class FullScreenDialogFragment extends DialogFragment {
protected Toolbar toolbar;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme_FullScreenDialog
: R.style.TextSecure_LightTheme_FullScreenDialog);
}
@Override
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.full_screen_dialog_fragment, container, false);
inflater.inflate(getDialogLayoutResource(), view.findViewById(R.id.full_screen_dialog_content), true);
toolbar = view.findViewById(R.id.full_screen_dialog_toolbar);
toolbar.setTitle(getTitle());
toolbar.setNavigationOnClickListener(v -> onNavigateUp());
return view;
}
protected void onNavigateUp() {
dismissAllowingStateLoss();
}
protected abstract @StringRes int getTitle();
protected abstract @LayoutRes int getDialogLayoutResource();
}

View File

@@ -242,14 +242,14 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
if (!viewOnceSlides.isEmpty()) {
thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(GONE);
} else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) {
} else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getUri() != null) {
thumbnailView.setVisibility(VISIBLE);
attachmentContainerView.setVisibility(GONE);
dismissView.setBackgroundResource(R.drawable.dismiss_background);
if (imageVideoSlides.get(0).hasVideo()) {
attachmentVideoOverlayView.setVisibility(VISIBLE);
}
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getUri()))
.centerCrop()
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components;
import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@@ -106,7 +107,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
viewHolder.imageView.setImageDrawable(null);
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA));
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID));
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
@@ -116,7 +117,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
int width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
final Uri uri = Uri.fromFile(new File(path));
final Uri uri = ContentUris.withAppendedId(RecentPhotosLoader.BASE_URL, rowId);
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);

View File

@@ -119,7 +119,7 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
this.activeRecipients.clear();
presentContact(contact);
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null);
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getUri() : null);
presentActionButtons(ContactUtil.getRecipients(getContext(), contact));
for (LiveRecipient recipient : activeRecipients.values()) {

View File

@@ -279,7 +279,7 @@ public class ThumbnailView extends FrameLayout {
getTransferControls().setVisibility(View.GONE);
}
if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() &&
if (slide.getUri() != null && slide.hasPlayOverlay() &&
(slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE || isPreview))
{
this.playOverlay.setVisibility(View.VISIBLE);
@@ -288,12 +288,12 @@ public class ThumbnailView extends FrameLayout {
}
if (Util.equals(slide, this.slide)) {
Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri());
Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getUri());
return new SettableFuture<>(false);
}
if (this.slide != null && this.slide.getFastPreflightId() != null &&
(!slide.hasVideo() || Util.equals(this.slide.getThumbnailUri(), slide.getThumbnailUri())) &&
(!slide.hasVideo() || Util.equals(this.slide.getUri(), slide.getUri())) &&
Util.equals(this.slide.getFastPreflightId(), slide.getFastPreflightId()))
{
Log.i(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId());
@@ -301,7 +301,7 @@ public class ThumbnailView extends FrameLayout {
return new SettableFuture<>(false);
}
Log.i(TAG, "loading part with id " + slide.asAttachment().getDataUri()
Log.i(TAG, "loading part with id " + slide.asAttachment().getUri()
+ ", progress " + slide.getTransferState() + ", fast preflight id: " +
slide.asAttachment().getFastPreflightId());
@@ -327,7 +327,7 @@ public class ThumbnailView extends FrameLayout {
blurhash.setImageDrawable(null);
}
if (slide.getThumbnailUri() != null) {
if (slide.getUri() != null) {
if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) {
SettableFuture<Boolean> thumbnailFuture = new SettableFuture<>();
thumbnailFuture.deferTo(result);
@@ -412,7 +412,7 @@ public class ThumbnailView extends FrameLayout {
}
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getUri()))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transition(withCrossFade()), fit);
@@ -469,10 +469,10 @@ public class ThumbnailView extends FrameLayout {
private class ThumbnailClickDispatcher implements View.OnClickListener {
@Override
public void onClick(View view) {
if (thumbnailClickListener != null &&
slide != null &&
slide.asAttachment().getDataUri() != null &&
slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
if (thumbnailClickListener != null &&
slide != null &&
slide.asAttachment().getUri() != null &&
slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
{
thumbnailClickListener.onClick(view, slide);
} else if (parentClickListener != null) {

View File

@@ -23,7 +23,12 @@ public class OutdatedBuildReminder extends Reminder {
private static CharSequence getPluralsText(final Context context) {
int days = getDaysUntilExpiry() - 1;
return context.getResources().getQuantityString(R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, days, days);
if (days == 0) {
return context.getString(R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today);
} else {
return context.getResources().getQuantityString(R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, days, days);
}
}
@Override

View File

@@ -0,0 +1,44 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import java.util.List;
/**
* Shown to admins when there are pending group join requests.
*/
public final class PendingGroupJoinRequestsReminder extends Reminder {
private PendingGroupJoinRequestsReminder(@Nullable CharSequence title,
@NonNull CharSequence text)
{
super(title, text);
}
public static Reminder create(@NonNull Context context, int count) {
String message = context.getResources().getQuantityString(R.plurals.PendingGroupJoinRequestsReminder_d_pending_member_requests, count, count);
Reminder reminder = new PendingGroupJoinRequestsReminder(null, message);
reminder.addAction(new Action(context.getString(R.string.PendingGroupJoinRequestsReminder_view), R.id.reminder_action_review_join_requests));
return reminder;
}
@Override
public boolean isDismissable() {
return true;
}
@Override
public @NonNull Importance getImportance() {
return Importance.NORMAL;
}
}

View File

@@ -74,7 +74,7 @@ public abstract class Reminder {
NORMAL, ERROR, TERMINAL
}
public final class Action {
public static final class Action {
private final CharSequence title;
private final int actionId;

View File

@@ -0,0 +1,253 @@
package org.thoughtcrime.securesms.components.voice;
import android.content.ComponentName;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import org.thoughtcrime.securesms.logging.Log;
import java.util.Objects;
/**
* Encapsulates control of voice note playback from an Activity component.
*
* This class assumes that it will be created within the scope of Activity#onCreate
*
* The workhorse of this repository is the ProgressEventHandler, which will supply a
* steady stream of update events to the set callback.
*/
public class VoiceNoteMediaController implements DefaultLifecycleObserver {
public static final String EXTRA_MESSAGE_ID = "voice.note.message_id";
public static final String EXTRA_PROGRESS = "voice.note.playhead";
public static final String EXTRA_PLAY_SINGLE = "voice.note.play.single";
private static final String TAG = Log.tag(VoiceNoteMediaController.class);
private MediaBrowserCompat mediaBrowser;
private AppCompatActivity activity;
private ProgressEventHandler progressEventHandler;
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback();
public VoiceNoteMediaController(@NonNull AppCompatActivity activity) {
this.activity = activity;
this.mediaBrowser = new MediaBrowserCompat(activity,
new ComponentName(activity, VoiceNotePlaybackService.class),
new ConnectionCallback(),
null);
activity.getLifecycle().addObserver(this);
}
public LiveData<VoiceNotePlaybackState> getVoiceNotePlaybackState() {
return voiceNotePlaybackState;
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
mediaBrowser.connect();
}
@Override
public void onResume(@NonNull LifecycleOwner owner) {
activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
clearProgressEventHandler();
if (MediaControllerCompat.getMediaController(activity) != null) {
MediaControllerCompat.getMediaController(activity).unregisterCallback(mediaControllerCompatCallback);
}
mediaBrowser.disconnect();
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
activity.getLifecycle().removeObserver(this);
activity = null;
}
private static boolean isPlayerActive(@NonNull PlaybackStateCompat playbackStateCompat) {
return playbackStateCompat.getState() == PlaybackStateCompat.STATE_BUFFERING ||
playbackStateCompat.getState() == PlaybackStateCompat.STATE_PLAYING;
}
private @NonNull MediaControllerCompat getMediaController() {
return MediaControllerCompat.getMediaController(activity);
}
public void startConsecutivePlayback(@NonNull Uri audioSlideUri, long messageId, double progress) {
startPlayback(audioSlideUri, messageId, progress, false);
}
public void startSinglePlayback(@NonNull Uri audioSlideUri, long messageId, double progress) {
startPlayback(audioSlideUri, messageId, progress, true);
}
/**
* Tells the Media service to begin playback of a given audio slide. If the audio
* slide is currently playing, we jump to the desired position and then begin playback.
*
* @param audioSlideUri The Uri of the desired audio slide
* @param messageId The Message id of the given audio slide
* @param progress The desired progress % to seek to.
* @param singlePlayback The player will only play back the specified Uri, and not build a playlist.
*/
private void startPlayback(@NonNull Uri audioSlideUri, long messageId, double progress, boolean singlePlayback) {
if (isCurrentTrack(audioSlideUri)) {
long duration = getMediaController().getMetadata().getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
getMediaController().getTransportControls().seekTo((long) (duration * progress));
getMediaController().getTransportControls().play();
} else {
Bundle extras = new Bundle();
extras.putLong(EXTRA_MESSAGE_ID, messageId);
extras.putDouble(EXTRA_PROGRESS, progress);
extras.putBoolean(EXTRA_PLAY_SINGLE, singlePlayback);
getMediaController().getTransportControls().playFromUri(audioSlideUri, extras);
}
}
/**
* Pauses playback if the given audio slide is playing.
*
* @param audioSlideUri The Uri of the audio slide to pause.
*/
public void pausePlayback(@NonNull Uri audioSlideUri) {
if (isCurrentTrack(audioSlideUri)) {
getMediaController().getTransportControls().pause();
}
}
/**
* Seeks to a given position if th given audio slide is playing. This call
* is ignored if the given audio slide is not currently playing.
*
* @param audioSlideUri The Uri of the audio slide to seek.
* @param progress The progress percentage to seek to.
*/
public void seekToPosition(@NonNull Uri audioSlideUri, double progress) {
if (isCurrentTrack(audioSlideUri)) {
long duration = getMediaController().getMetadata().getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
getMediaController().getTransportControls().pause();
getMediaController().getTransportControls().seekTo((long) (duration * progress));
getMediaController().getTransportControls().play();
}
}
/**
* Stops playback if the given audio slide is playing
*
* @param audioSlideUri The Uri of the audio slide to stop
*/
public void stopPlaybackAndReset(@NonNull Uri audioSlideUri) {
if (isCurrentTrack(audioSlideUri)) {
getMediaController().getTransportControls().stop();
}
}
private boolean isCurrentTrack(@NonNull Uri uri) {
MediaMetadataCompat metadataCompat = getMediaController().getMetadata();
return metadataCompat != null && Objects.equals(metadataCompat.getDescription().getMediaUri(), uri);
}
private void notifyProgressEventHandler() {
if (progressEventHandler == null) {
progressEventHandler = new ProgressEventHandler(getMediaController(), voiceNotePlaybackState);
progressEventHandler.sendEmptyMessage(0);
}
}
private void clearProgressEventHandler() {
if (progressEventHandler != null) {
progressEventHandler = null;
}
}
private final class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
@Override
public void onConnected() {
try {
MediaSessionCompat.Token token = mediaBrowser.getSessionToken();
MediaControllerCompat mediaController = new MediaControllerCompat(activity, token);
MediaControllerCompat.setMediaController(activity, mediaController);
mediaController.registerCallback(mediaControllerCompatCallback);
mediaControllerCompatCallback.onPlaybackStateChanged(mediaController.getPlaybackState());
} catch (RemoteException e) {
Log.w(TAG, "onConnected: Failed to set media controller", e);
}
}
}
private static class ProgressEventHandler extends Handler {
private final MediaControllerCompat mediaController;
private final MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState;
private ProgressEventHandler(@NonNull MediaControllerCompat mediaController,
@NonNull MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState) {
this.mediaController = mediaController;
this.voiceNotePlaybackState = voiceNotePlaybackState;
}
@Override
public void handleMessage(Message msg) {
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
if (isPlayerActive(mediaController.getPlaybackState()) &&
mediaMetadataCompat != null &&
mediaMetadataCompat.getDescription() != null)
{
Uri mediaUri = Objects.requireNonNull(mediaMetadataCompat.getDescription().getMediaUri());
boolean autoReset = Objects.equals(mediaUri, VoiceNotePlaybackPreparer.NEXT_URI) || Objects.equals(mediaUri, VoiceNotePlaybackPreparer.END_URI);
voiceNotePlaybackState.postValue(new VoiceNotePlaybackState(mediaUri,
mediaController.getPlaybackState().getPosition(),
mediaMetadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION),
autoReset));
sendEmptyMessageDelayed(0, 50);
} else {
voiceNotePlaybackState.postValue(VoiceNotePlaybackState.NONE);
}
}
}
private final class MediaControllerCompatCallback extends MediaControllerCompat.Callback {
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
if (isPlayerActive(state)) {
notifyProgressEventHandler();
} else {
clearProgressEventHandler();
}
}
}
}

View File

@@ -0,0 +1,100 @@
package org.thoughtcrime.securesms.components.voice;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.media.MediaDescriptionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
import java.util.Objects;
/**
* Factory responsible for building out MediaDescriptionCompat objects for voice notes.
*/
class VoiceNoteMediaDescriptionCompatFactory {
public static final String EXTRA_MESSAGE_POSITION = "voice.note.extra.MESSAGE_POSITION";
public static final String EXTRA_THREAD_RECIPIENT_ID = "voice.note.extra.RECIPIENT_ID";
public static final String EXTRA_AVATAR_RECIPIENT_ID = "voice.note.extra.SENDER_ID";
public static final String EXTRA_THREAD_ID = "voice.note.extra.THREAD_ID";
public static final String EXTRA_COLOR = "voice.note.extra.COLOR";
public static final String EXTRA_MESSAGE_ID = "voice.note.extra.MESSAGE_ID";
private static final String TAG = Log.tag(VoiceNoteMediaDescriptionCompatFactory.class);
private VoiceNoteMediaDescriptionCompatFactory() {}
/**
* Build out a MediaDescriptionCompat for a given voice note. Expects to be run
* on a background thread.
*
* @param context Context.
* @param messageRecord The MessageRecord of the given voice note.
*
* @return A MediaDescriptionCompat with all the details the service expects.
*/
@WorkerThread
static MediaDescriptionCompat buildMediaDescription(@NonNull Context context,
@NonNull MessageRecord messageRecord)
{
int startingPosition = DatabaseFactory.getMmsSmsDatabase(context)
.getMessagePositionInConversation(messageRecord.getThreadId(),
messageRecord.getDateReceived());
Recipient threadRecipient = Objects.requireNonNull(DatabaseFactory.getThreadDatabase(context)
.getRecipientForThreadId(messageRecord.getThreadId()));
Recipient sender = messageRecord.isOutgoing() ? Recipient.self() : messageRecord.getIndividualRecipient();
Recipient avatarRecipient = threadRecipient.isGroup() ? threadRecipient : sender;
Bundle extras = new Bundle();
extras.putString(EXTRA_THREAD_RECIPIENT_ID, threadRecipient.getId().serialize());
extras.putString(EXTRA_AVATAR_RECIPIENT_ID, avatarRecipient.getId().serialize());
extras.putLong(EXTRA_MESSAGE_POSITION, startingPosition);
extras.putLong(EXTRA_THREAD_ID, messageRecord.getThreadId());
extras.putString(EXTRA_COLOR, threadRecipient.getColor().serialize());
extras.putLong(EXTRA_MESSAGE_ID, messageRecord.getId());
NotificationPrivacyPreference preference = TextSecurePreferences.getNotificationPrivacy(context);
String title;
if (preference.isDisplayContact() && threadRecipient.isGroup()) {
title = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__s_to_s,
sender.getDisplayName(context),
threadRecipient.getDisplayName(context));
} else if (preference.isDisplayContact()) {
title = sender.getDisplayName(context);
} else {
title = context.getString(R.string.MessageNotifier_signal_message);
}
String subtitle = null;
if (preference.isDisplayContact()) {
subtitle = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__voice_message,
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(),
messageRecord.getDateReceived()));
}
Uri uri = ((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide().getUri();
return new MediaDescriptionCompat.Builder()
.setMediaUri(uri)
.setTitle(title)
.setSubtitle(subtitle)
.setExtras(extras)
.build();
}
}

View File

@@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.components.voice;
import android.content.Context;
import android.net.Uri;
import android.support.v4.media.MediaDescriptionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.AssetDataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
/**
* This class is responsible for creating a MediaSource object for a given MediaDescriptionCompat
*/
final class VoiceNoteMediaSourceFactory {
private final Context context;
VoiceNoteMediaSourceFactory(Context context) {
this.context = context;
}
/**
* Creates a MediaSource for a given MediaDescriptionCompat
*
* @param description The description to build from
*
* @return A preparable MediaSource
*/
public @Nullable MediaSource createMediaSource(MediaDescriptionCompat description) {
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
return new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
.setExtractorsFactory(extractorsFactory)
.createMediaSource(description.getMediaUri());
}
}

View File

@@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.components.voice;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.Player;
public class VoiceNoteNotificationControlDispatcher extends DefaultControlDispatcher {
private final VoiceNoteQueueDataAdapter dataAdapter;
public VoiceNoteNotificationControlDispatcher(@NonNull VoiceNoteQueueDataAdapter dataAdapter) {
this.dataAdapter = dataAdapter;
}
@Override
public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) {
boolean isQueueToneIndex = windowIndex % 2 == 1;
boolean isSeekingToStart = positionMs == C.TIME_UNSET;
if (isQueueToneIndex && isSeekingToStart) {
int nextVoiceNoteWindowIndex = player.getCurrentWindowIndex() < windowIndex ? windowIndex + 1 : windowIndex - 1;
if (dataAdapter.size() <= nextVoiceNoteWindowIndex) {
return super.dispatchSeekTo(player, windowIndex, positionMs);
} else {
return super.dispatchSeekTo(player, nextVoiceNoteWindowIndex, positionMs);
}
} else {
return super.dispatchSeekTo(player, windowIndex, positionMs);
}
}
}

View File

@@ -0,0 +1,160 @@
package org.thoughtcrime.securesms.components.voice;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.RemoteException;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.Objects;
class VoiceNoteNotificationManager {
private static final short NOW_PLAYING_NOTIFICATION_ID = 32221;
private final Context context;
private final MediaControllerCompat controller;
private final PlayerNotificationManager notificationManager;
VoiceNoteNotificationManager(@NonNull Context context,
@NonNull MediaSessionCompat.Token token,
@NonNull PlayerNotificationManager.NotificationListener listener,
@NonNull VoiceNoteQueueDataAdapter dataAdapter)
{
this.context = context;
try {
controller = new MediaControllerCompat(context, token);
} catch (RemoteException e) {
throw new IllegalArgumentException("Could not create a controller with given token");
}
notificationManager = PlayerNotificationManager.createWithNotificationChannel(context,
NotificationChannels.VOICE_NOTES,
R.string.NotificationChannel_voice_notes,
NOW_PLAYING_NOTIFICATION_ID,
new DescriptionAdapter());
notificationManager.setMediaSessionToken(token);
notificationManager.setSmallIcon(R.drawable.ic_notification);
notificationManager.setRewindIncrementMs(0);
notificationManager.setFastForwardIncrementMs(0);
notificationManager.setNotificationListener(listener);
notificationManager.setColorized(true);
notificationManager.setControlDispatcher(new VoiceNoteNotificationControlDispatcher(dataAdapter));
}
public void hideNotification() {
notificationManager.setPlayer(null);
}
public void showNotification(@NonNull Player player) {
notificationManager.setPlayer(player);
}
private final class DescriptionAdapter implements PlayerNotificationManager.MediaDescriptionAdapter {
private RecipientId cachedRecipientId;
private Bitmap cachedBitmap;
@Override
public String getCurrentContentTitle(Player player) {
if (hasMetadata()) {
return Objects.requireNonNull(controller.getMetadata().getDescription().getTitle()).toString();
} else {
return null;
}
}
@Override
public @Nullable PendingIntent createCurrentContentIntent(Player player) {
if (!hasMetadata()) return null;
RecipientId recipientId = RecipientId.from(Objects.requireNonNull(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID)));
int startingPosition = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION);
long threadId = controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID);
MaterialColor color;
try {
color = MaterialColor.fromSerialized(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_COLOR));
} catch (MaterialColor.UnknownColorException e) {
color = ContactColors.UNKNOWN_COLOR;
}
notificationManager.setColor(color.toNotificationColor(context));
Intent conversationActivity = ConversationActivity.buildIntent(context,
recipientId,
threadId,
ThreadDatabase.DistributionTypes.DEFAULT,
startingPosition);
conversationActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return PendingIntent.getActivity(context,
0,
conversationActivity,
PendingIntent.FLAG_CANCEL_CURRENT);
}
@Override
public String getCurrentContentText(Player player) {
if (hasMetadata()) {
return Objects.toString(controller.getMetadata().getDescription().getSubtitle(), null);
} else {
return null;
}
}
@Override
public @Nullable Bitmap getCurrentLargeIcon(Player player, PlayerNotificationManager.BitmapCallback callback) {
if (!hasMetadata() || !TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact()) {
cachedBitmap = null;
cachedRecipientId = null;
return null;
}
RecipientId currentRecipientId = RecipientId.from(Objects.requireNonNull(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_AVATAR_RECIPIENT_ID)));
if (Objects.equals(currentRecipientId, cachedRecipientId) && cachedBitmap != null) {
return cachedBitmap;
} else {
cachedRecipientId = currentRecipientId;
SignalExecutors.BOUNDED.execute(() -> {
try {
cachedBitmap = AvatarUtil.getBitmapForNotification(context, Recipient.resolved(cachedRecipientId));
callback.onBitmap(cachedBitmap);
} catch (Exception e) {
cachedBitmap = null;
}
});
return null;
}
}
private boolean hasMetadata() {
return controller.getMetadata() != null && controller.getMetadata().getDescription() != null;
}
}
}

View File

@@ -0,0 +1,268 @@
package org.thoughtcrime.securesms.components.voice;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* ExoPlayer Preparer for Voice Notes. This only supports ACTION_PLAY_FROM_URI
*/
final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
private static final String TAG = Log.tag(VoiceNotePlaybackPreparer.class);
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
private static final long LIMIT = 5;
public static final Uri NEXT_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-down.ogg");
public static final Uri END_URI = Uri.parse("file:///android_asset/sounds/state-change_confirm-up.ogg");
private final Context context;
private final SimpleExoPlayer player;
private final VoiceNoteQueueDataAdapter queueDataAdapter;
private final VoiceNoteMediaSourceFactory mediaSourceFactory;
private final ConcatenatingMediaSource dataSource;
private boolean canLoadMore;
private Uri latestUri = Uri.EMPTY;
VoiceNotePlaybackPreparer(@NonNull Context context,
@NonNull SimpleExoPlayer player,
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter,
@NonNull VoiceNoteMediaSourceFactory mediaSourceFactory)
{
this.context = context;
this.player = player;
this.queueDataAdapter = queueDataAdapter;
this.mediaSourceFactory = mediaSourceFactory;
this.dataSource = new ConcatenatingMediaSource();
}
@Override
public long getSupportedPrepareActions() {
return PlaybackStateCompat.ACTION_PLAY_FROM_URI;
}
@Override
public void onPrepare() {
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepare");
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromMediaId");
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
throw new UnsupportedOperationException("VoiceNotePlaybackPreparer does not support onPrepareFromSearch");
}
@Override
public void onPrepareFromUri(final Uri uri, Bundle extras) {
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
double progress = extras.getDouble(VoiceNoteMediaController.EXTRA_PROGRESS, 0);
boolean singlePlayback = extras.getBoolean(VoiceNoteMediaController.EXTRA_PLAY_SINGLE, false);
canLoadMore = false;
latestUri = uri;
queueDataAdapter.clear();
dataSource.clear();
SimpleTask.run(EXECUTOR,
() -> {
if (singlePlayback) {
return loadMediaDescriptionForSinglePlayback(messageId);
} else {
return loadMediaDescriptionsForConsecutivePlayback(messageId);
}
},
descriptions -> {
if (Util.hasItems(descriptions) && Objects.equals(latestUri, uri)) {
applyDescriptionsToQueue(descriptions);
int window = Math.max(0, queueDataAdapter.indexOf(uri));
player.addListener(new Player.EventListener() {
@Override
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
if (timeline.getWindowCount() >= window) {
player.seekTo(window, (long) (player.getDuration() * progress));
player.removeListener(this);
}
}
});
player.prepare(dataSource);
canLoadMore = !singlePlayback;
}
});
}
@Override
public String[] getCommands() {
return new String[0];
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
}
private void applyDescriptionsToQueue(@NonNull List<MediaDescriptionCompat> descriptions) {
for (MediaDescriptionCompat description : descriptions) {
int holderIndex = queueDataAdapter.indexOf(description.getMediaUri());
MediaDescriptionCompat next = createNextClone(description);
int currentIndex = player.getCurrentWindowIndex();
if (holderIndex != -1) {
queueDataAdapter.remove(holderIndex);
queueDataAdapter.remove(holderIndex);
queueDataAdapter.add(holderIndex, createNextClone(description));
queueDataAdapter.add(holderIndex, description);
if (currentIndex != holderIndex) {
dataSource.removeMediaSource(holderIndex);
dataSource.addMediaSource(holderIndex, mediaSourceFactory.createMediaSource(description));
}
if (currentIndex != holderIndex + 1) {
dataSource.removeMediaSource(holderIndex + 1);
dataSource.addMediaSource(holderIndex + 1, mediaSourceFactory.createMediaSource(next));
}
} else {
int insertLocation = queueDataAdapter.indexAfter(description);
queueDataAdapter.add(insertLocation, next);
queueDataAdapter.add(insertLocation, description);
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(next));
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(description));
}
}
int lastIndex = queueDataAdapter.size() - 1;
MediaDescriptionCompat last = queueDataAdapter.getMediaDescription(lastIndex);
if (Objects.equals(last.getMediaUri(), NEXT_URI)) {
MediaDescriptionCompat end = createEndClone(last);
queueDataAdapter.remove(lastIndex);
queueDataAdapter.add(lastIndex, end);
dataSource.removeMediaSource(lastIndex);
dataSource.addMediaSource(lastIndex, mediaSourceFactory.createMediaSource(end));
}
}
private @NonNull MediaDescriptionCompat createEndClone(@NonNull MediaDescriptionCompat source) {
return buildUpon(source).setMediaId("end").setMediaUri(END_URI).build();
}
private @NonNull MediaDescriptionCompat createNextClone(@NonNull MediaDescriptionCompat source) {
return buildUpon(source).setMediaId("next").setMediaUri(NEXT_URI).build();
}
private @NonNull MediaDescriptionCompat.Builder buildUpon(@NonNull MediaDescriptionCompat source) {
return new MediaDescriptionCompat.Builder()
.setSubtitle(source.getSubtitle())
.setDescription(source.getDescription())
.setTitle(source.getTitle())
.setIconUri(source.getIconUri())
.setIconBitmap(source.getIconBitmap())
.setMediaId(source.getMediaId())
.setExtras(source.getExtras());
}
public void loadMoreVoiceNotes() {
if (!canLoadMore) {
return;
}
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
long messageId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
SimpleTask.run(EXECUTOR,
() -> loadMediaDescriptionsForConsecutivePlayback(messageId),
descriptions -> {
if (Util.hasItems(descriptions) && canLoadMore) {
applyDescriptionsToQueue(descriptions);
}
});
}
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionForSinglePlayback(long messageId) {
try {
MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
if (!MessageRecordUtil.hasAudio(messageRecord)) {
Log.w(TAG, "Message does not contain audio.");
return Collections.emptyList();
}
return Collections.singletonList(VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context ,messageRecord));
} catch (NoSuchMessageException e) {
Log.w(TAG, "Could not find message.", e);
return Collections.emptyList();
}
}
@WorkerThread
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionsForConsecutivePlayback(long messageId) {
try {
List<MessageRecord> recordsBefore = DatabaseFactory.getMmsSmsDatabase(context).getMessagesBeforeVoiceNoteExclusive(messageId, LIMIT);
List<MessageRecord> recordsAfter = DatabaseFactory.getMmsSmsDatabase(context).getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
return Stream.of(buildFilteredMessageRecordList(recordsBefore, recordsAfter))
.map(record -> VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, record))
.toList();
} catch (NoSuchMessageException e) {
Log.w(TAG, "Could not find message.", e);
return Collections.emptyList();
}
}
@VisibleForTesting
static @NonNull List<MessageRecord> buildFilteredMessageRecordList(@NonNull List<MessageRecord> recordsBefore, @NonNull List<MessageRecord> recordsAfter) {
Collections.reverse(recordsBefore);
List<MessageRecord> filteredBefore = Stream.of(recordsBefore)
.takeWhile(MessageRecordUtil::hasAudio)
.toList();
Collections.reverse(filteredBefore);
List<MessageRecord> filteredAfter = Stream.of(recordsAfter)
.takeWhile(MessageRecordUtil::hasAudio)
.toList();
filteredBefore.addAll(filteredAfter);
return filteredBefore;
}
}

View File

@@ -0,0 +1,240 @@
package org.thoughtcrime.securesms.components.voice;
import android.app.Notification;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Process;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.media.MediaBrowserServiceCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import org.thoughtcrime.securesms.logging.Log;
import java.util.Collections;
import java.util.List;
/**
* Android Service responsible for playback of voice notes.
*/
public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
private static final String TAG = Log.tag(VoiceNotePlaybackService.class);
private static final String EMPTY_ROOT_ID = "empty-root-id";
private static final int LOAD_MORE_THRESHOLD = 2;
private static final long SUPPORTED_ACTIONS = PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_PAUSE |
PlaybackStateCompat.ACTION_SEEK_TO |
PlaybackStateCompat.ACTION_STOP |
PlaybackStateCompat.ACTION_PLAY_PAUSE;
private MediaSessionCompat mediaSession;
private MediaSessionConnector mediaSessionConnector;
private PlaybackStateCompat.Builder stateBuilder;
private SimpleExoPlayer player;
private BecomingNoisyReceiver becomingNoisyReceiver;
private VoiceNoteNotificationManager voiceNoteNotificationManager;
private VoiceNoteQueueDataAdapter queueDataAdapter;
private VoiceNotePlaybackPreparer voiceNotePlaybackPreparer;
private VoiceNoteProximityManager voiceNoteProximityManager;
private boolean isForegroundService;
private final LoadControl loadControl = new DefaultLoadControl.Builder()
.setBufferDurationsMs(Integer.MAX_VALUE,
Integer.MAX_VALUE,
Integer.MAX_VALUE,
Integer.MAX_VALUE)
.createDefaultLoadControl();
@Override
public void onCreate() {
super.onCreate();
mediaSession = new MediaSessionCompat(this, TAG);
stateBuilder = new PlaybackStateCompat.Builder()
.setActions(SUPPORTED_ACTIONS);
mediaSessionConnector = new MediaSessionConnector(mediaSession, null);
becomingNoisyReceiver = new BecomingNoisyReceiver(this, mediaSession.getSessionToken());
player = ExoPlayerFactory.newSimpleInstance(this, new DefaultRenderersFactory(this), new DefaultTrackSelector(), loadControl);
queueDataAdapter = new VoiceNoteQueueDataAdapter();
voiceNoteNotificationManager = new VoiceNoteNotificationManager(this,
mediaSession.getSessionToken(),
new VoiceNoteNotificationManagerListener(),
queueDataAdapter);
VoiceNoteMediaSourceFactory mediaSourceFactory = new VoiceNoteMediaSourceFactory(this);
voiceNotePlaybackPreparer = new VoiceNotePlaybackPreparer(this, player, queueDataAdapter, mediaSourceFactory);
voiceNoteProximityManager = new VoiceNoteProximityManager(this, player);
mediaSession.setPlaybackState(stateBuilder.build());
player.addListener(new VoiceNotePlayerEventListener());
player.setAudioAttributes(new AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_SPEECH)
.setUsage(C.USAGE_MEDIA)
.build(), true);
mediaSessionConnector.setPlayer(player, voiceNotePlaybackPreparer);
mediaSessionConnector.setQueueNavigator(new VoiceNoteQueueNavigator(mediaSession, queueDataAdapter));
setSessionToken(mediaSession.getSessionToken());
mediaSession.setActive(true);
}
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
player.stop(true);
}
@Override
public void onDestroy() {
super.onDestroy();
mediaSession.setActive(false);
mediaSession.release();
becomingNoisyReceiver.unregister();
player.release();
}
@Override
public @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
if (clientUid == Process.myUid()) {
return new BrowserRoot(EMPTY_ROOT_ID, null);
} else {
return null;
}
}
@Override
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
result.sendResult(Collections.emptyList());
}
private class VoiceNotePlayerEventListener implements Player.EventListener {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
switch (playbackState) {
case Player.STATE_BUFFERING:
case Player.STATE_READY:
voiceNoteProximityManager.onPlayerReady();
voiceNoteNotificationManager.showNotification(player);
if (!playWhenReady) {
stopForeground(false);
becomingNoisyReceiver.unregister();
} else {
becomingNoisyReceiver.register();
}
break;
default:
voiceNoteProximityManager.onPlayerEnded();
becomingNoisyReceiver.unregister();
voiceNoteNotificationManager.hideNotification();
}
}
@Override
public void onPositionDiscontinuity(int reason) {
int currentWindowIndex = player.getCurrentWindowIndex();
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
currentWindowIndex + LOAD_MORE_THRESHOLD >= queueDataAdapter.size();
if (isWithinThreshold && currentWindowIndex % 2 == 0) {
voiceNotePlaybackPreparer.loadMoreVoiceNotes();
}
}
@Override
public void onPlayerError(ExoPlaybackException error) {
Log.w(TAG, "ExoPlayer error occurred:", error);
voiceNoteProximityManager.onPlayerError();
}
}
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
@Override
public void onNotificationStarted(int notificationId, Notification notification) {
if (!isForegroundService) {
ContextCompat.startForegroundService(getApplicationContext(), new Intent(getApplicationContext(), VoiceNotePlaybackService.class));
startForeground(notificationId, notification);
isForegroundService = true;
}
}
@Override
public void onNotificationCancelled(int notificationId) {
stopForeground(true);
isForegroundService = false;
stopSelf();
}
}
/**
* Receiver to pause playback when things become noisy.
*/
private static class BecomingNoisyReceiver extends BroadcastReceiver {
private static final IntentFilter NOISY_INTENT_FILTER = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private final Context context;
private final MediaControllerCompat controller;
private boolean registered;
private BecomingNoisyReceiver(Context context, MediaSessionCompat.Token token) {
this.context = context;
try {
this.controller = new MediaControllerCompat(context, token);
} catch (RemoteException e) {
throw new IllegalArgumentException("Failed to create controller from token", e);
}
}
void register() {
if (!registered) {
context.registerReceiver(this, NOISY_INTENT_FILTER);
registered = true;
}
}
void unregister() {
if (registered) {
context.unregisterReceiver(this);
registered = false;
}
}
public void onReceive(Context context, @NonNull Intent intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
controller.getTransportControls().pause();
}
}
}
}

View File

@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.components.voice;
import android.net.Uri;
import androidx.annotation.NonNull;
/**
* Domain-level state object representing the state of the currently playing voice note.
*/
public class VoiceNotePlaybackState {
public static final VoiceNotePlaybackState NONE = new VoiceNotePlaybackState(Uri.EMPTY, 0, 0, false);
private final Uri uri;
private final long playheadPositionMillis;
private final long trackDuration;
private final boolean autoReset;
public VoiceNotePlaybackState(@NonNull Uri uri, long playheadPositionMillis, long trackDuration, boolean autoReset) {
this.uri = uri;
this.playheadPositionMillis = playheadPositionMillis;
this.trackDuration = trackDuration;
this.autoReset = autoReset;
}
/**
* @return Uri of the currently playing AudioSlide
*/
public Uri getUri() {
return uri;
}
/**
* @return The last known playhead position
*/
public long getPlayheadPositionMillis() {
return playheadPositionMillis;
}
/**
* @return The track duration in ms
*/
public long getTrackDuration() {
return trackDuration;
}
/**
* @return true if we should reset the currently playing clip.
*/
public boolean isAutoReset() {
return autoReset;
}
}

View File

@@ -0,0 +1,119 @@
package org.thoughtcrime.securesms.components.voice;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.os.Build;
import android.os.PowerManager;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.util.Util;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.util.concurrent.TimeUnit;
class VoiceNoteProximityManager implements SensorEventListener {
private static final String TAG = Log.tag(VoiceNoteProximityManager.class);
private static final float PROXIMITY_THRESHOLD = 5f;
private final SimpleExoPlayer player;
private final AudioManager audioManager;
private final SensorManager sensorManager;
private final Sensor proximitySensor;
private final PowerManager.WakeLock wakeLock;
private long startTime;
VoiceNoteProximityManager(@NonNull Context context, @NonNull SimpleExoPlayer player) {
this.player = player;
this.audioManager = ServiceUtil.getAudioManager(context);
this.sensorManager = ServiceUtil.getSensorManager(context);
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
if (Build.VERSION.SDK_INT >= 21) {
this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
} else {
this.wakeLock = null;
}
}
void onPlayerReady() {
startTime = System.currentTimeMillis();
sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
}
void onPlayerEnded() {
sensorManager.unregisterListener(this);
if (wakeLock != null && wakeLock.isHeld() && Build.VERSION.SDK_INT >= 21) {
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
}
}
void onPlayerError() {
onPlayerEnded();
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY || player.getPlaybackState() != Player.STATE_READY) {
return;
}
final int desiredStreamType;
if (event.values[0] < PROXIMITY_THRESHOLD && event.values[0] != proximitySensor.getMaximumRange()) {
desiredStreamType = AudioManager.STREAM_VOICE_CALL;
} else {
desiredStreamType = AudioManager.STREAM_MUSIC;
}
final int currentStreamType = Util.getStreamTypeForAudioUsage(player.getAudioAttributes().usage);
if (desiredStreamType == AudioManager.STREAM_VOICE_CALL &&
desiredStreamType != currentStreamType &&
!audioManager.isWiredHeadsetOn())
{
if (wakeLock != null) {
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30));
}
player.setPlayWhenReady(false);
player.setAudioAttributes(new AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_SPEECH)
.setUsage(C.USAGE_VOICE_COMMUNICATION)
.build());
player.setPlayWhenReady(true);
startTime = System.currentTimeMillis();
} else if (desiredStreamType == AudioManager.STREAM_MUSIC &&
desiredStreamType != currentStreamType &&
System.currentTimeMillis() - startTime > 500)
{
if (wakeLock != null) {
wakeLock.release();
player.setPlayWhenReady(false);
player.setAudioAttributes(new AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_MUSIC)
.setUsage(C.USAGE_MEDIA)
.build(),
true);
}
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}

View File

@@ -0,0 +1,81 @@
package org.thoughtcrime.securesms.components.voice;
import android.net.Uri;
import android.support.v4.media.MediaDescriptionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
/**
* DataAdapter which maintains the current queue of MediaDescriptionCompat objects.
*/
final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAdapter {
private final List<MediaDescriptionCompat> descriptions = new LinkedList<>();
@Override
public MediaDescriptionCompat getMediaDescription(int position) {
return descriptions.get(position);
}
@Override
public void add(int position, MediaDescriptionCompat description) {
descriptions.add(position, description);
}
@Override
public void remove(int position) {
descriptions.remove(position);
}
@Override
public void move(int from, int to) {
MediaDescriptionCompat description = descriptions.remove(from);
descriptions.add(to, description);
}
int size() {
return descriptions.size();
}
int indexOf(@NonNull Uri uri) {
for (int i = 0; i < descriptions.size(); i++) {
if (Objects.equals(uri, descriptions.get(i).getMediaUri())) {
return i;
}
}
return -1;
}
int indexAfter(@NonNull MediaDescriptionCompat target) {
if (isEmpty()) {
return 0;
}
long targetMessageId = target.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
for (int i = 0; i < descriptions.size(); i++) {
long descriptionMessageId = descriptions.get(i).getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
if (descriptionMessageId > targetMessageId) {
return i;
}
}
return descriptions.size();
}
boolean isEmpty() {
return descriptions.isEmpty();
}
void clear() {
descriptions.clear();
}
}

View File

@@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.components.voice;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueEditor;
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator;
/**
* Navigator to help support seek forward and back.
*/
final class VoiceNoteQueueNavigator extends TimelineQueueNavigator {
private final TimelineQueueEditor.QueueDataAdapter queueDataAdapter;
public VoiceNoteQueueNavigator(@NonNull MediaSessionCompat mediaSession, @NonNull TimelineQueueEditor.QueueDataAdapter queueDataAdapter) {
super(mediaSession);
this.queueDataAdapter = queueDataAdapter;
}
@Override
public MediaDescriptionCompat getMediaDescription(Player player, int windowIndex) {
return queueDataAdapter.getMediaDescription(windowIndex);
}
}

View File

@@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.webrtc.EglBase;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.WeakHashMap;
public class BroadcastVideoSink implements VideoSink {
private final EglBase eglBase;
private final WeakHashMap<VideoSink, Boolean> sinks;
public BroadcastVideoSink(@Nullable EglBase eglBase) {
this.eglBase = eglBase;
this.sinks = new WeakHashMap<>();
}
public @Nullable EglBase getEglBase() {
return eglBase;
}
public void addSink(@NonNull VideoSink sink) {
sinks.put(sink, true);
}
public void removeSink(@NonNull VideoSink sink) {
sinks.remove(sink);
}
@Override
public void onFrame(@NonNull VideoFrame videoFrame) {
for (VideoSink sink : sinks.keySet()) {
sink.onFrame(videoFrame);
}
}
}

View File

@@ -0,0 +1,115 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AvatarUtil;
import java.util.Objects;
/**
* Encapsulates views needed to show a call participant including their
* avatar in full screen or pip mode, and their video feed.
*/
public class CallParticipantView extends ConstraintLayout {
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
private RecipientId recipientId;
private AvatarImageView avatar;
private TextureViewRenderer renderer;
private ImageView pipAvatar;
private ContactPhoto contactPhoto;
public CallParticipantView(@NonNull Context context) {
super(context);
onFinishInflate();
}
public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
avatar = findViewById(R.id.call_participant_item_avatar);
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
renderer = findViewById(R.id.call_participant_renderer);
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
}
void setCallParticipant(@NonNull CallParticipant participant) {
boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId());
recipientId = participant.getRecipient().getId();
renderer.setVisibility(participant.isVideoEnabled() ? View.VISIBLE : View.GONE);
if (participant.isVideoEnabled()) {
if (participant.getVideoSink().getEglBase() != null) {
renderer.init(participant.getVideoSink().getEglBase());
}
renderer.attachBroadcastVideoSink(participant.getVideoSink());
} else {
renderer.attachBroadcastVideoSink(null);
}
if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) {
avatar.setAvatar(participant.getRecipient());
AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this);
setPipAvatar(participant.getRecipient());
contactPhoto = participant.getRecipient().getContactPhoto();
}
}
void setRenderInPip(boolean shouldRenderInPip) {
avatar.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);
pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE);
}
private void setPipAvatar(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
GlideApp.with(this)
.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(getContext()))
.error(fallbackPhoto.asCallCard(getContext()))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(pipAvatar);
pipAvatar.setScaleType(contactPhoto == null ? ImageView.ScaleType.CENTER_INSIDE : ImageView.ScaleType.CENTER_CROP);
pipAvatar.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
}
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
@Override
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
ResourceContactPhoto photo = new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
return photo;
}
}
}

View File

@@ -0,0 +1,104 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.flexbox.AlignItems;
import com.google.android.flexbox.FlexboxLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.util.Util;
import java.util.Collections;
import java.util.List;
/**
* Can dynamically render a collection of call participants, adjusting their
* sizing and layout depending on the total number of participants.
*/
public class CallParticipantsLayout extends FlexboxLayout {
private List<CallParticipant> callParticipants = Collections.emptyList();
private boolean shouldRenderInPip;
public CallParticipantsLayout(@NonNull Context context) {
super(context);
}
public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
void update(@NonNull List<CallParticipant> callParticipants, boolean shouldRenderInPip) {
this.callParticipants = callParticipants;
this.shouldRenderInPip = shouldRenderInPip;
updateLayout();
}
private void updateLayout() {
if (shouldRenderInPip && Util.hasItems(callParticipants)) {
updateChildrenCount(1);
update(0, callParticipants.get(0));
} else {
int count = callParticipants.size();
updateChildrenCount(count);
for (int i = 0; i < callParticipants.size(); i++) {
update(i, callParticipants.get(i));
}
}
}
private void updateChildrenCount(int count) {
int childCount = getChildCount();
if (childCount < count) {
for (int i = childCount; i < count; i++) {
addCallParticipantView();
}
} else if (childCount > count) {
for (int i = count; i < childCount; i++) {
removeViewAt(count);
}
}
}
private void update(int index, @NonNull CallParticipant participant) {
CallParticipantView callParticipantView = (CallParticipantView) getChildAt(index);
callParticipantView.setCallParticipant(participant);
callParticipantView.setRenderInPip(shouldRenderInPip);
setChildLayoutParams(callParticipantView, index, getChildCount());
}
private void addCallParticipantView() {
View view = LayoutInflater.from(getContext()).inflate(R.layout.call_participant_item, this, false);
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) view.getLayoutParams();
params.setAlignSelf(AlignItems.STRETCH);
view.setLayoutParams(params);
addView(view);
}
private void setChildLayoutParams(@NonNull View child, int childPosition, int childCount) {
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) child.getLayoutParams();
if (childCount < 3) {
params.setFlexBasisPercent(1f);
} else {
if ((childCount % 2) != 0 && childPosition == childCount - 1) {
params.setFlexBasisPercent(1f);
} else {
params.setFlexBasisPercent(0.5f);
}
}
child.setLayoutParams(params);
}
}

View File

@@ -0,0 +1,215 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Represents the state of all participants, remote and local, combined with view state
* needed to properly render the participants. The view state primarily consists of
* if we are in System PIP mode and if we should show our video for an outgoing call.
*/
public final class CallParticipantsState {
private static final int SMALL_GROUP_MAX = 6;
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
Collections.emptyList(),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
null,
WebRtcLocalRenderState.GONE,
false,
false,
false);
private final WebRtcViewModel.State callState;
private final List<CallParticipant> remoteParticipants;
private final CallParticipant localParticipant;
private final CallParticipant focusedParticipant;
private final WebRtcLocalRenderState localRenderState;
private final boolean isInPipMode;
private final boolean showVideoForOutgoing;
private final boolean isViewingFocusedParticipant;
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
@NonNull List<CallParticipant> remoteParticipants,
@NonNull CallParticipant localParticipant,
@Nullable CallParticipant focusedParticipant,
@NonNull WebRtcLocalRenderState localRenderState,
boolean isInPipMode,
boolean showVideoForOutgoing,
boolean isViewingFocusedParticipant)
{
this.callState = callState;
this.remoteParticipants = remoteParticipants;
this.localParticipant = localParticipant;
this.localRenderState = localRenderState;
this.focusedParticipant = focusedParticipant;
this.isInPipMode = isInPipMode;
this.showVideoForOutgoing = showVideoForOutgoing;
this.isViewingFocusedParticipant = isViewingFocusedParticipant;
}
public @NonNull WebRtcViewModel.State getCallState() {
return callState;
}
public @NonNull List<CallParticipant> getGridParticipants() {
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
} else {
return getAllRemoteParticipants();
}
}
public @NonNull List<CallParticipant> getListParticipants() {
List<CallParticipant> listParticipants = new ArrayList<>();
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
listParticipants.addAll(getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size()));
} else if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
listParticipants.addAll(getAllRemoteParticipants().subList(SMALL_GROUP_MAX, getAllRemoteParticipants().size()));
} else {
return Collections.emptyList();
}
listParticipants.add(CallParticipant.EMPTY);
Collections.reverse(listParticipants);
return listParticipants;
}
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
return remoteParticipants;
}
public @NonNull CallParticipant getLocalParticipant() {
return localParticipant;
}
public @Nullable CallParticipant getFocusedParticipant() {
return focusedParticipant;
}
public @NonNull WebRtcLocalRenderState getLocalRenderState() {
return localRenderState;
}
public boolean isLargeVideoGroup() {
return getAllRemoteParticipants().size() > SMALL_GROUP_MAX;
}
public boolean isInPipMode() {
return isInPipMode;
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
@NonNull WebRtcViewModel webRtcViewModel,
boolean enableVideo)
{
boolean newShowVideoForOutgoing = oldState.showVideoForOutgoing;
if (enableVideo) {
newShowVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
newShowVideoForOutgoing = false;
}
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(webRtcViewModel.getLocalParticipant(),
oldState.isInPipMode,
newShowVideoForOutgoing,
webRtcViewModel.getState(),
oldState.getAllRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
return new CallParticipantsState(webRtcViewModel.getState(),
webRtcViewModel.getRemoteParticipants(),
webRtcViewModel.getLocalParticipant(),
focused,
localRenderState,
oldState.isInPipMode,
newShowVideoForOutgoing,
oldState.isViewingFocusedParticipant);
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) {
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
isInPip,
oldState.showVideoForOutgoing,
oldState.callState,
oldState.getAllRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
return new CallParticipantsState(oldState.callState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
localRenderState,
isInPip,
oldState.showVideoForOutgoing,
oldState.isViewingFocusedParticipant);
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
oldState.callState,
oldState.getAllRemoteParticipants().size(),
selectedPage == SelectedPage.FOCUSED);
return new CallParticipantsState(oldState.callState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
localRenderState,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
selectedPage == SelectedPage.FOCUSED);
}
private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant,
boolean isInPip,
boolean showVideoForOutgoing,
@NonNull WebRtcViewModel.State callState,
int numberOfRemoteParticipants,
boolean isViewingFocusedParticipant)
{
boolean displayLocal = !isInPip && localParticipant.isVideoEnabled();
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
if (displayLocal || showVideoForOutgoing) {
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 3) {
localRenderState = WebRtcLocalRenderState.SMALL_SQUARE;
} else {
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
}
} else if (callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
localRenderState = WebRtcLocalRenderState.LARGE;
}
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO;
}
return localRenderState;
}
public enum SelectedPage {
GRID,
FOCUSED
}
}

View File

@@ -9,6 +9,7 @@ import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
@@ -16,21 +17,26 @@ import androidx.core.view.GestureDetectorCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener {
private static final float DECELERATION_RATE = 0.99f;
private static final float DECELERATION_RATE = 0.99f;
private static final Interpolator FLING_INTERPOLATOR = new ViscousFluidInterpolator();
private static final Interpolator ADJUST_INTERPOLATOR = new AccelerateDecelerateInterpolator();
private final ViewGroup parent;
private final View child;
private final int framePadding;
private final int pipWidth;
private final int pipHeight;
private final ViewGroup parent;
private final View child;
private final int framePadding;
private final Queue<Runnable> runAfterFling;
private int pipWidth;
private int pipHeight;
private int activePointerId = MotionEvent.INVALID_POINTER_ID;
private float lastTouchX;
private float lastTouchY;
@@ -42,6 +48,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
private double projectionY;
private VelocityTracker velocityTracker;
private int maximumFlingVelocity;
private boolean isLockedToBottomEnd;
private Interpolator interpolator;
@SuppressLint("ClickableViewAccessibility")
public static PictureInPictureGestureHelper applyTo(@NonNull View child) {
@@ -95,6 +103,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
this.pipWidth = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_width);
this.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height);
this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity();
this.runAfterFling = new LinkedList<>();
this.interpolator = ADJUST_INTERPOLATOR;
}
public void clearVerticalBoundaries() {
@@ -105,11 +115,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
extraPaddingTop = topBoundary - parent.getTop();
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
if (isAnimating) {
fling();
} else if (!isDragging) {
onFling(null, null, 0, 0);
}
adjustPip();
}
private boolean onGestureFinished(MotionEvent e) {
@@ -123,12 +129,46 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
return false;
}
public void adjustPip() {
pipWidth = child.getMeasuredWidth();
pipHeight = child.getMeasuredHeight();
if (isAnimating) {
interpolator = ADJUST_INTERPOLATOR;
fling();
} else if (!isDragging) {
interpolator = ADJUST_INTERPOLATOR;
onFling(null, null, 0, 0);
}
}
public void lockToBottomEnd() {
isLockedToBottomEnd = true;
}
public void enableCorners() {
isLockedToBottomEnd = false;
}
public void performAfterFling(@NonNull Runnable runnable) {
if (isAnimating) {
runAfterFling.add(runnable);
} else {
runnable.run();
}
}
@Override
public boolean onDown(MotionEvent e) {
activePointerId = e.getPointerId(0);
lastTouchX = e.getX(activePointerId) + child.getX();
lastTouchY = e.getY(activePointerId) + child.getY();
isDragging = true;
pipWidth = child.getMeasuredWidth();
pipHeight = child.getMeasuredHeight();
interpolator = FLING_INTERPOLATOR;
return true;
}
@@ -167,6 +207,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
child.performClick();
return true;
}
private void fling() {
Point projection = new Point((int) projectionX, (int) projectionY);
Point nearestCornerPosition = findNearestCornerPosition(projection);
@@ -178,17 +225,30 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
.translationX(getTranslationXForPoint(nearestCornerPosition))
.translationY(getTranslationYForPoint(nearestCornerPosition))
.setDuration(250)
.setInterpolator(new ViscousFluidInterpolator())
.setInterpolator(interpolator)
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
Iterator<Runnable> afterFlingRunnables = runAfterFling.iterator();
while (afterFlingRunnables.hasNext()) {
Runnable runnable = afterFlingRunnables.next();
runnable.run();
afterFlingRunnables.remove();
}
}
})
.start();
}
private Point findNearestCornerPosition(Point projection) {
if (isLockedToBottomEnd) {
return parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? calculateBottomRightCoordinates(parent)
: calculateBottomLeftCoordinates(parent);
}
Point maxPoint = null;
double maxDistance = Double.MAX_VALUE;

View File

@@ -36,6 +36,8 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
private boolean enableFixedSize;
private int surfaceWidth;
private int surfaceHeight;
private boolean isInitialized;
private BroadcastVideoSink attachedVideoSink;
public TextureViewRenderer(@NonNull Context context) {
super(context);
@@ -49,8 +51,12 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.setSurfaceTextureListener(this);
}
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents) {
this.init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer());
public void init(@NonNull EglBase eglBase) {
if (isInitialized) return;
isInitialized = true;
this.init(eglBase.getEglBaseContext(), null, EglBase.CONFIG_PLAIN, new GlRectDrawer());
}
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) {
@@ -63,6 +69,30 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.eglRenderer.init(sharedContext, this, configAttributes, drawer);
}
public void attachBroadcastVideoSink(@Nullable BroadcastVideoSink videoSink) {
if (attachedVideoSink == videoSink) {
return;
}
if (attachedVideoSink != null) {
attachedVideoSink.removeSink(this);
}
if (videoSink != null) {
videoSink.addSink(this);
} else {
clearImage();
}
attachedVideoSink = videoSink;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
release();
}
public void release() {
eglRenderer.release();
}
@@ -125,6 +155,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
protected void onMeasure(int widthSpec, int heightSpec) {
ThreadUtils.checkIsOnMainThread();
widthSpec = MeasureSpec.makeMeasureSpec(resolveSizeAndState(0, widthSpec, 0), MeasureSpec.AT_MOST);
heightSpec = MeasureSpec.makeMeasureSpec(resolveSizeAndState(0, heightSpec, 0), MeasureSpec.AT_MOST);
Point size = videoLayoutMeasure.measure(widthSpec, heightSpec, this.rotatedFrameWidth, this.rotatedFrameHeight);
setMeasuredDimension(size.x, size.y);

View File

@@ -0,0 +1,64 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.events.CallParticipant;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
class WebRtcCallParticipantsPage {
private final List<CallParticipant> callParticipants;
private final boolean isSpeaker;
private final boolean isRenderInPip;
static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List<CallParticipant> callParticipants,
boolean isRenderInPip)
{
return new WebRtcCallParticipantsPage(callParticipants, false, isRenderInPip);
}
static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant,
boolean isRenderInPip)
{
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), true, isRenderInPip);
}
private WebRtcCallParticipantsPage(@NonNull List<CallParticipant> callParticipants,
boolean isSpeaker,
boolean isRenderInPip)
{
this.callParticipants = callParticipants;
this.isSpeaker = isSpeaker;
this.isRenderInPip = isRenderInPip;
}
public @NonNull List<CallParticipant> getCallParticipants() {
return callParticipants;
}
public boolean isRenderInPip() {
return isRenderInPip;
}
public boolean isSpeaker() {
return isSpeaker;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WebRtcCallParticipantsPage that = (WebRtcCallParticipantsPage) o;
return isSpeaker == that.isSpeaker &&
isRenderInPip == that.isRenderInPip &&
callParticipants.equals(that.callParticipants);
}
@Override
public int hashCode() {
return Objects.hash(callParticipants, isSpeaker);
}
}

View File

@@ -0,0 +1,127 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipantsPage, WebRtcCallParticipantsPagerAdapter.ViewHolder> {
private static final int VIEW_TYPE_MULTI = 0;
private static final int VIEW_TYPE_SINGLE = 1;
private final Runnable onPageClicked;
WebRtcCallParticipantsPagerAdapter(@NonNull Runnable onPageClicked) {
super(new DiffCallback());
this.onPageClicked = onPageClicked;
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final ViewHolder viewHolder;
switch (viewType) {
case VIEW_TYPE_SINGLE:
viewHolder = new SingleParticipantViewHolder((CallParticipantView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.call_participant_item,
parent,
false));
break;
case VIEW_TYPE_MULTI:
viewHolder = new MultipleParticipantViewHolder((CallParticipantsLayout) LayoutInflater.from(parent.getContext())
.inflate(R.layout.webrtc_call_participants_layout,
parent,
false));
break;
default:
throw new IllegalArgumentException("Unsupported viewType: " + viewType);
}
viewHolder.itemView.setOnClickListener(unused -> onPageClicked.run());
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(getItem(position));
}
@Override
public int getItemViewType(int position) {
return getItem(position).isSpeaker() ? VIEW_TYPE_SINGLE : VIEW_TYPE_MULTI;
}
static abstract class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
abstract void bind(WebRtcCallParticipantsPage page);
}
private static class MultipleParticipantViewHolder extends ViewHolder {
private final CallParticipantsLayout callParticipantsLayout;
private MultipleParticipantViewHolder(@NonNull CallParticipantsLayout callParticipantsLayout) {
super(callParticipantsLayout);
this.callParticipantsLayout = callParticipantsLayout;
}
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantsLayout.update(page.getCallParticipants(), page.isRenderInPip());
}
}
private static class SingleParticipantViewHolder extends ViewHolder {
private final CallParticipantView callParticipantView;
private SingleParticipantViewHolder(CallParticipantView callParticipantView) {
super(callParticipantView);
this.callParticipantView = callParticipantView;
ViewGroup.LayoutParams params = callParticipantView.getLayoutParams();
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
callParticipantView.setLayoutParams(params);
}
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantView.setCallParticipant(page.getCallParticipants().get(0));
callParticipantView.setRenderInPip(page.isRenderInPip());
}
}
private static final class DiffCallback extends DiffUtil.ItemCallback<WebRtcCallParticipantsPage> {
@Override
public boolean areItemsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) {
return oldItem.isSpeaker() == newItem.isSpeaker();
}
@Override
public boolean areContentsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) {
return oldItem.equals(newItem);
}
}
}

View File

@@ -0,0 +1,79 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant, WebRtcCallParticipantsRecyclerAdapter.ViewHolder> {
private static final int PARTICIPANT = 0;
private static final int EMPTY = 1;
protected WebRtcCallParticipantsRecyclerAdapter() {
super(new DiffCallback());
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == PARTICIPANT) {
return new ParticipantViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_item, parent, false));
} else {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_empty_item, parent, false));
}
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(getItem(position));
}
@Override
public int getItemViewType(int position) {
return getItem(position) == CallParticipant.EMPTY ? EMPTY : PARTICIPANT;
}
static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(@NonNull View itemView) {
super(itemView);
}
void bind(@NonNull CallParticipant callParticipant) {}
}
private static class ParticipantViewHolder extends ViewHolder {
private final CallParticipantView callParticipantView;
ParticipantViewHolder(@NonNull View itemView) {
super(itemView);
callParticipantView = itemView.findViewById(R.id.call_participant);
}
@Override
void bind(@NonNull CallParticipant callParticipant) {
callParticipantView.setCallParticipant(callParticipant);
}
}
private static class DiffCallback extends DiffUtil.ItemCallback<CallParticipant> {
@Override
public boolean areItemsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) {
return oldItem.getRecipient().equals(newItem.getRecipient());
}
@Override
public boolean areContentsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) {
return oldItem.equals(newItem);
}
}
}

View File

@@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -13,67 +15,85 @@ import android.widget.TextView;
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;
import androidx.constraintlayout.widget.Guideline;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.AutoTransition;
import androidx.transition.ChangeBounds;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
import androidx.transition.TransitionSet;
import androidx.viewpager2.widget.MarginPageTransformer;
import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BlurTransformation;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class WebRtcCallView extends FrameLayout {
private static final long TRANSITION_DURATION_MILLIS = 250;
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8;
private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16;
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
private static final long TRANSITION_DURATION_MILLIS = 250;
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8;
private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16;
public static final int FADE_OUT_DELAY = 5000;
public static final int FADE_OUT_DELAY = 5000;
public static final int PIP_RESIZE_DURATION = 300;
public static final int CONTROLS_HEIGHT = 98;
private TextureViewRenderer localRenderer;
private WebRtcAudioOutputToggleButton audioToggle;
private AccessibleToggleButton videoToggle;
private AccessibleToggleButton micToggle;
private ViewGroup largeLocalRenderContainer;
private ViewGroup localRenderPipFrame;
private ViewGroup smallLocalRenderContainer;
private ViewGroup remoteRenderContainer;
private ViewGroup smallLocalRenderFrame;
private TextureViewRenderer smallLocalRender;
private View largeLocalRenderFrame;
private TextureViewRenderer largeLocalRender;
private View largeLocalRenderNoVideo;
private ImageView largeLocalRenderNoVideoAvatar;
private TextView recipientName;
private TextView status;
private ConstraintLayout parent;
private AvatarImageView avatar;
private ImageView avatarCard;
private ConstraintLayout participantsParent;
private ControlsListener controlsListener;
private RecipientId recipientId;
private CameraState.Direction cameraDirection;
private ImageView answer;
private ImageView cameraDirectionToggle;
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
private ImageView hangup;
private View answerWithAudio;
private View answerWithAudioLabel;
private View ongoingFooterGradient;
private View footerGradient;
private View startCallControls;
private ViewPager2 callParticipantsPager;
private RecyclerView callParticipantsRecycler;
private Toolbar toolbar;
private int pagerBottomMarginDp;
private boolean controlsVisible = true;
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
private final Set<View> incomingCallViews = new HashSet<>();
private final Set<View> topViews = new HashSet<>();
@@ -82,7 +102,8 @@ public class WebRtcCallView extends FrameLayout {
private WebRtcControls controls = WebRtcControls.NONE;
private final Runnable fadeOutRunnable = () -> {
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls(); };
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls();
};
public WebRtcCallView(@NonNull Context context) {
this(context, null);
@@ -99,42 +120,61 @@ public class WebRtcCallView extends FrameLayout {
protected void onFinishInflate() {
super.onFinishInflate();
audioToggle = findViewById(R.id.call_screen_speaker_toggle);
videoToggle = findViewById(R.id.call_screen_video_toggle);
micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
localRenderPipFrame = findViewById(R.id.call_screen_pip);
largeLocalRenderContainer = findViewById(R.id.call_screen_large_local_renderer_holder);
smallLocalRenderContainer = findViewById(R.id.call_screen_small_local_renderer_holder);
remoteRenderContainer = findViewById(R.id.call_screen_remote_renderer_holder);
recipientName = findViewById(R.id.call_screen_recipient_name);
status = findViewById(R.id.call_screen_status);
parent = findViewById(R.id.call_screen);
avatar = findViewById(R.id.call_screen_recipient_avatar);
avatarCard = findViewById(R.id.call_screen_recipient_avatar_call_card);
answer = findViewById(R.id.call_screen_answer_call);
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
hangup = findViewById(R.id.call_screen_end_call);
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient);
audioToggle = findViewById(R.id.call_screen_speaker_toggle);
videoToggle = findViewById(R.id.call_screen_video_toggle);
micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
smallLocalRenderFrame = findViewById(R.id.call_screen_pip);
smallLocalRender = findViewById(R.id.call_screen_small_local_renderer);
largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame);
largeLocalRender = findViewById(R.id.call_screen_large_local_renderer);
largeLocalRenderNoVideo = findViewById(R.id.call_screen_large_local_video_off);
largeLocalRenderNoVideoAvatar = findViewById(R.id.call_screen_large_local_video_off_avatar);
recipientName = findViewById(R.id.call_screen_recipient_name);
status = findViewById(R.id.call_screen_status);
parent = findViewById(R.id.call_screen);
participantsParent = findViewById(R.id.call_screen_participants_parent);
answer = findViewById(R.id.call_screen_answer_call);
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
hangup = findViewById(R.id.call_screen_end_call);
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
footerGradient = findViewById(R.id.call_screen_footer_gradient);
startCallControls = findViewById(R.id.call_screen_start_call_controls);
callParticipantsPager = findViewById(R.id.call_screen_participants_pager);
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
toolbar = findViewById(R.id.call_screen_toolbar);
View topGradient = findViewById(R.id.call_screen_header_gradient);
View downCaret = findViewById(R.id.call_screen_down_arrow);
View decline = findViewById(R.id.call_screen_decline_call);
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
View incomingFooterGradient = findViewById(R.id.call_screen_incoming_footer_gradient);
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
View startCall = findViewById(R.id.call_screen_start_call_start_call);
View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
topViews.add(status);
callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
pagerAdapter = new WebRtcCallParticipantsPagerAdapter(this::toggleControls);
recyclerAdapter = new WebRtcCallParticipantsRecyclerAdapter();
callParticipantsPager.setAdapter(pagerAdapter);
callParticipantsRecycler.setAdapter(recyclerAdapter);
callParticipantsPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
runIfNonNull(controlsListener, listener -> listener.onPageChanged(position == 0 ? CallParticipantsState.SelectedPage.GRID : CallParticipantsState.SelectedPage.FOCUSED));
}
});
topViews.add(toolbar);
topViews.add(topGradient);
topViews.add(recipientName);
incomingCallViews.add(answer);
incomingCallViews.add(answerLabel);
incomingCallViews.add(decline);
incomingCallViews.add(declineLabel);
incomingCallViews.add(incomingFooterGradient);
incomingCallViews.add(footerGradient);
adjustableMarginsSet.add(micToggle);
adjustableMarginsSet.add(cameraDirectionToggle);
@@ -158,15 +198,18 @@ public class WebRtcCallView extends FrameLayout {
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed));
downCaret.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDownCaretPressed));
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
setOnClickListener(v -> toggleControls());
avatar.setOnClickListener(v -> toggleControls());
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(localRenderPipFrame);
startCall.setOnClickListener(v -> runIfNonNull(controlsListener, listener -> listener.onStartCall(videoToggle.isChecked())));
cancelStartCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCancelStartCall));
ColorMatrix greyScaleMatrix = new ColorMatrix();
greyScaleMatrix.setSaturation(0);
largeLocalRenderNoVideoAvatar.setAlpha(0.6f);
largeLocalRenderNoVideoAvatar.setColorFilter(new ColorMatrixColorFilter(greyScaleMatrix));
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
statusBarGuideline.setGuidelineBegin(statusBarHeight);
@@ -195,67 +238,99 @@ public class WebRtcCallView extends FrameLayout {
micToggle.setChecked(isMicEnabled, false);
}
public void setRemoteVideoEnabled(boolean isRemoteVideoEnabled) {
if (isRemoteVideoEnabled) {
remoteRenderContainer.setVisibility(View.VISIBLE);
public void updateCallParticipants(@NonNull CallParticipantsState state) {
List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
if (!state.getGridParticipants().isEmpty()) {
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.isInPipMode()));
}
if (state.getFocusedParticipant() != null && state.getAllRemoteParticipants().size() > 1) {
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
}
pagerAdapter.submitList(pages);
recyclerAdapter.submitList(state.getListParticipants());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
if (state.isLargeVideoGroup()) {
layoutParticipantsForLargeCount();
} else {
remoteRenderContainer.setVisibility(View.GONE);
layoutParticipantsForSmallCount();
}
}
public void setLocalRenderer(@Nullable TextureViewRenderer surfaceViewRenderer) {
if (localRenderer == surfaceViewRenderer) {
return;
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
smallLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
largeLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
if (localCallParticipant.getVideoSink().getEglBase() != null) {
smallLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
}
localRenderer = surfaceViewRenderer;
if (surfaceViewRenderer == null) {
setRenderer(largeLocalRenderContainer, null);
setRenderer(smallLocalRenderContainer, null);
} else {
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
localRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
}
}
public void setRemoteRenderer(@Nullable TextureViewRenderer remoteRenderer) {
setRenderer(remoteRenderContainer, remoteRenderer);
}
public void setLocalRenderState(WebRtcLocalRenderState localRenderState) {
videoToggle.setChecked(localRenderState != WebRtcLocalRenderState.GONE, false);
switch (localRenderState) {
switch (state) {
case GONE:
localRenderPipFrame.setVisibility(View.GONE);
largeLocalRenderContainer.setVisibility(View.GONE);
setRenderer(largeLocalRenderContainer, null);
setRenderer(smallLocalRenderContainer, null);
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
smallLocalRender.attachBroadcastVideoSink(null);
smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(false, false);
break;
case SMALL_RECTANGLE:
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToRectangle();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break;
case SMALL_SQUARE:
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToSquare();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break;
case LARGE:
localRenderPipFrame.setVisibility(View.GONE);
largeLocalRenderContainer.setVisibility(View.VISIBLE);
if (largeLocalRenderContainer.getChildCount() == 0) {
setRenderer(largeLocalRenderContainer, localRenderer);
}
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
largeLocalRenderFrame.setVisibility(View.VISIBLE);
largeLocalRenderNoVideo.setVisibility(View.GONE);
largeLocalRenderNoVideoAvatar.setVisibility(View.GONE);
smallLocalRender.attachBroadcastVideoSink(null);
smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break;
case SMALL:
localRenderPipFrame.setVisibility(View.VISIBLE);
largeLocalRenderContainer.setVisibility(View.GONE);
case LARGE_NO_VIDEO:
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.VISIBLE);
if (smallLocalRenderContainer.getChildCount() == 0) {
setRenderer(smallLocalRenderContainer, localRenderer);
}
}
}
largeLocalRenderNoVideo.setVisibility(View.VISIBLE);
largeLocalRenderNoVideoAvatar.setVisibility(View.VISIBLE);
public void setCameraDirection(@NonNull CameraState.Direction cameraDirection) {
this.cameraDirection = cameraDirection;
GlideApp.with(getContext().getApplicationContext())
.load(new ProfileContactPhoto(localCallParticipant.getRecipient(), localCallParticipant.getRecipient().getProfileAvatar()))
.transform(new CenterCrop(), new BlurTransformation(getContext(), 0.25f, BlurTransformation.MAX_RADIUS))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(largeLocalRenderNoVideoAvatar);
if (localRenderer != null) {
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
smallLocalRender.attachBroadcastVideoSink(null);
smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(false, false);
break;
}
}
@@ -265,17 +340,16 @@ public class WebRtcCallView extends FrameLayout {
}
recipientId = recipient.getId();
recipientName.setText(recipient.getDisplayName(getContext()));
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
avatar.setAvatar(GlideApp.with(this), recipient, false);
AvatarUtil.loadBlurredIconIntoViewBackground(recipient, this);
setRecipientCallCard(recipient);
}
public void showCallCard(boolean showCallCard) {
avatarCard.setVisibility(showCallCard ? VISIBLE : GONE);
avatar.setVisibility(showCallCard ? GONE : VISIBLE);
if (recipient.isGroup()) {
recipientName.setText(R.string.WebRtcCallView__group_call);
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
toolbar.inflateMenu(R.menu.group_call);
toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
}
} else {
recipientName.setText(recipient.getDisplayName(getContext()));
}
}
public void setStatus(@NonNull String status) {
@@ -302,11 +376,16 @@ public class WebRtcCallView extends FrameLayout {
}
}
public void setWebRtcControls(WebRtcControls webRtcControls) {
public void setWebRtcControls(@NonNull WebRtcControls webRtcControls) {
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
visibleViewSet.clear();
if (webRtcControls.displayStartCallControls()) {
visibleViewSet.add(footerGradient);
visibleViewSet.add(startCallControls);
}
if (webRtcControls.displayTopViews()) {
visibleViewSet.addAll(topViews);
}
@@ -341,7 +420,7 @@ public class WebRtcCallView extends FrameLayout {
if (webRtcControls.displayEndCall()) {
visibleViewSet.add(hangup);
visibleViewSet.add(ongoingFooterGradient);
visibleViewSet.add(footerGradient);
}
if (webRtcControls.displayMuteAudio()) {
@@ -358,6 +437,12 @@ public class WebRtcCallView extends FrameLayout {
updateButtonStateForLargeButtons();
}
if (webRtcControls.displayRemoteVideoRecycler()) {
callParticipantsRecycler.setVisibility(View.VISIBLE);
} else {
callParticipantsRecycler.setVisibility(View.GONE);
}
if (webRtcControls.isFadeOutEnabled()) {
if (!controls.isFadeOutEnabled()) {
scheduleFadeOut();
@@ -370,7 +455,7 @@ public class WebRtcCallView extends FrameLayout {
if (!visibleViewSet.equals(lastVisibleSet) || !controls.isFadeOutEnabled()) {
fadeInNewUiState(lastVisibleSet, webRtcControls.displaySmallOngoingCallButtons());
post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), videoToggle.getTop()));
post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop()));
}
}
@@ -378,8 +463,39 @@ public class WebRtcCallView extends FrameLayout {
return videoToggle;
}
private void animatePipToRectangle() {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
pictureInPictureGestureHelper.enableCorners();
pictureInPictureGestureHelper.adjustPip();
}
});
smallLocalRenderFrame.startAnimation(animation);
}
private void animatePipToSquare() {
pictureInPictureGestureHelper.lockToBottomEnd();
pictureInPictureGestureHelper.performAfterFling(() -> {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(72), ViewUtil.dpToPx(72));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
pictureInPictureGestureHelper.adjustPip();
}
});
smallLocalRenderFrame.startAnimation(animation);
});
}
private void toggleControls() {
if (controls.isFadeOutEnabled() && status.getVisibility() == VISIBLE) {
if (controls.isFadeOutEnabled() && toolbar.getVisibility() == VISIBLE) {
fadeOutControls();
} else {
fadeInControls();
@@ -394,14 +510,49 @@ public class WebRtcCallView extends FrameLayout {
private void fadeInControls() {
fadeControls(ConstraintSet.VISIBLE);
pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), videoToggle.getTop());
pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop());
scheduleFadeOut();
}
private void fadeControls(int visibility) {
private void layoutParticipantsForSmallCount() {
pagerBottomMarginDp = 0;
layoutParticipants();
}
private void layoutParticipantsForLargeCount() {
pagerBottomMarginDp = 104;
layoutParticipants();
}
private int withControlsHeight(int margin) {
if (margin == 0) {
return 0;
}
return controlsVisible ? margin + CONTROLS_HEIGHT : margin;
}
private void layoutParticipants() {
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.beginDelayedTransition(participantsParent, transition);
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(participantsParent);
constraintSet.setMargin(R.id.call_screen_participants_pager, ConstraintSet.BOTTOM, ViewUtil.dpToPx(withControlsHeight(pagerBottomMarginDp)));
constraintSet.applyTo(participantsParent);
}
private void fadeControls(int visibility) {
controlsVisible = visibility == VISIBLE;
Transition transition = new AutoTransition().setOrdering(TransitionSet.ORDERING_TOGETHER)
.setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();
@@ -412,6 +563,8 @@ public class WebRtcCallView extends FrameLayout {
}
constraintSet.applyTo(parent);
layoutParticipants();
}
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) {
@@ -458,40 +611,6 @@ public class WebRtcCallView extends FrameLayout {
}
}
private static void setRenderer(@NonNull ViewGroup container, @Nullable View renderer) {
if (renderer == null) {
container.removeAllViews();
return;
}
ViewParent parent = renderer.getParent();
if (parent != null && parent != container) {
((ViewGroup) parent).removeAllViews();
}
if (parent == container) {
return;
}
container.addView(renderer);
}
private void setRecipientCallCard(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
GlideApp.with(this).load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(getContext()))
.error(fallbackPhoto.asCallCard(getContext()))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(this.avatarCard);
if (contactPhoto == null) this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
else this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_CROP);
this.avatarCard.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
}
private void updateButtonStateForLargeButtons() {
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle);
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup);
@@ -508,14 +627,14 @@ public class WebRtcCallView extends FrameLayout {
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small);
}
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
@Override
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
return new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
}
private boolean showParticipantsList() {
controlsListener.onShowParticipantsList();
return true;
}
public interface ControlsListener {
void onStartCall(boolean isVideoCall);
void onCancelStartCall();
void onControlsFadeOut();
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
void onVideoChanged(boolean isVideoEnabled);
@@ -525,6 +644,7 @@ public class WebRtcCallView extends FrameLayout {
void onDenyCallPressed();
void onAcceptCallWithVoiceOnlyPressed();
void onAcceptCallPressed();
void onDownCaretPressed();
void onShowParticipantsList();
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
}
}

View File

@@ -10,60 +10,38 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
public class WebRtcCallViewModel extends ViewModel {
private final MutableLiveData<Boolean> remoteVideoEnabled = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
private final MutableLiveData<WebRtcLocalRenderState> localRenderState = new MutableLiveData<>(WebRtcLocalRenderState.GONE);
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> localVideoEnabled = new MutableLiveData<>(false);
private final MutableLiveData<CameraState.Direction> cameraDirection = new MutableLiveData<>(CameraState.Direction.FRONT);
private final LiveData<Boolean> shouldDisplayLocal = LiveDataUtil.combineLatest(isInPipMode, localVideoEnabled, (a, b) -> !a && b);
private final LiveData<WebRtcLocalRenderState> realLocalRenderState = LiveDataUtil.combineLatest(shouldDisplayLocal, localRenderState, this::getRealLocalRenderState);
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<Long> ellapsed = new MutableLiveData<>(-1L);
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
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;
private Runnable ellapsedTimeRunnable = this::handleTick;
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
private long callConnectedTime = -1;
private Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
private boolean answerWithVideoAvailable = false;
private Runnable elapsedTimeRunnable = this::handleTick;
private boolean canEnterPipMode = false;
private final WebRtcCallRepository repository = new WebRtcCallRepository();
public LiveData<Boolean> getRemoteVideoEnabled() {
return Transformations.distinctUntilChanged(remoteVideoEnabled);
}
public LiveData<Boolean> getMicrophoneEnabled() {
return Transformations.distinctUntilChanged(microphoneEnabled);
}
public LiveData<CameraState.Direction> getCameraDirection() {
return Transformations.distinctUntilChanged(cameraDirection);
}
public LiveData<Boolean> displaySquareCallCard() {
return isInPipMode;
}
public LiveData<WebRtcLocalRenderState> getLocalRenderState() {
return realLocalRenderState;
}
public LiveData<WebRtcControls> getWebRtcControls() {
return realWebRtcControls;
}
@@ -81,7 +59,15 @@ public class WebRtcCallViewModel extends ViewModel {
}
public LiveData<Long> getCallTime() {
return Transformations.map(ellapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
return Transformations.map(elapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
}
public LiveData<CallParticipantsState> getCallParticipantsState() {
return participantsState;
}
public boolean canEnterPipMode() {
return canEnterPipMode;
}
public boolean isAnswerWithVideoAvailable() {
@@ -91,6 +77,15 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread
public void setIsInPipMode(boolean isInPipMode) {
this.isInPipMode.setValue(isInPipMode);
//noinspection ConstantConditions
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), isInPipMode));
}
@MainThread
public void setIsViewingFocusedParticipant(@NonNull CallParticipantsState.SelectedPage page) {
//noinspection ConstantConditions
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page));
}
public void onDismissedVideoTooltip() {
@@ -99,27 +94,20 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled());
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
canEnterPipMode = webRtcViewModel.getState() != WebRtcViewModel.State.CALL_PRE_JOIN;
if (isValidCameraDirectionForUi(webRtcViewModel.getLocalCameraState().getActiveDirection())) {
cameraDirection.setValue(webRtcViewModel.getLocalCameraState().getActiveDirection());
}
CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled());
microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
if (enableVideo) {
showVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
showVideoForOutgoing = false;
}
//noinspection ConstantConditions
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
updateLocalRenderState(webRtcViewModel.getState());
updateWebRtcControls(webRtcViewModel.getState(),
webRtcViewModel.getLocalCameraState().isEnabled(),
localParticipant.getCameraState().isEnabled(),
webRtcViewModel.isRemoteVideoEnabled(),
webRtcViewModel.isRemoteVideoOffer(),
webRtcViewModel.getLocalCameraState().getCameraCount() > 1,
localParticipant.isMoreThanOneCameraAvailable(),
webRtcViewModel.isBluetoothAvailable(),
repository.getAudioOutput());
@@ -131,9 +119,9 @@ public class WebRtcCallViewModel extends ViewModel {
callConnectedTime = -1;
}
if (webRtcViewModel.getLocalCameraState().isEnabled()) {
if (localParticipant.getCameraState().isEnabled()) {
canDisplayTooltipIfNeeded = false;
hasEnabledLocalVideo = true;
hasEnabledLocalVideo = true;
events.setValue(Event.DISMISS_VIDEO_TOOLTIP);
}
@@ -144,34 +132,36 @@ public class WebRtcCallViewModel extends ViewModel {
}
}
private boolean isValidCameraDirectionForUi(CameraState.Direction direction) {
return direction == CameraState.Direction.FRONT || direction == CameraState.Direction.BACK;
}
private void updateLocalRenderState(WebRtcViewModel.State state) {
if (state == WebRtcViewModel.State.CALL_CONNECTED) {
localRenderState.setValue(WebRtcLocalRenderState.SMALL);
} else {
localRenderState.setValue(WebRtcLocalRenderState.LARGE);
}
}
private void updateWebRtcControls(WebRtcViewModel.State state,
private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
boolean isLocalVideoEnabled,
boolean isRemoteVideoEnabled,
boolean isRemoteVideoOffer,
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
WebRtcAudioOutput audioOutput)
@NonNull WebRtcAudioOutput audioOutput)
{
final WebRtcControls.CallState callState;
switch (state) {
case CALL_PRE_JOIN:
callState = WebRtcControls.CallState.PRE_JOIN;
break;
case CALL_INCOMING:
callState = WebRtcControls.CallState.INCOMING;
answerWithVideoAvailable = isRemoteVideoOffer;
break;
case CALL_OUTGOING:
case CALL_RINGING:
callState = WebRtcControls.CallState.OUTGOING;
break;
case CALL_ACCEPTED_ELSEWHERE:
case CALL_DECLINED_ELSEWHERE:
case CALL_ONGOING_ELSEWHERE:
case CALL_NEEDS_PERMISSION:
case CALL_BUSY:
case CALL_DISCONNECTED:
callState = WebRtcControls.CallState.ENDING;
break;
default:
callState = WebRtcControls.CallState.ONGOING;
}
@@ -180,25 +170,19 @@ public class WebRtcCallViewModel extends ViewModel {
isRemoteVideoEnabled || isRemoteVideoOffer,
isMoreThanOneCameraAvailable,
isBluetoothAvailable,
isInPipMode.getValue() == Boolean.TRUE,
Boolean.TRUE.equals(isInPipMode.getValue()),
callState,
audioOutput));
}
private @NonNull WebRtcLocalRenderState getRealLocalRenderState(boolean shouldDisplayLocalVideo, @NonNull WebRtcLocalRenderState state) {
if (shouldDisplayLocalVideo || showVideoForOutgoing) return state;
else return WebRtcLocalRenderState.GONE;
}
private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
if (isInPipMode) return WebRtcControls.PIP;
else return controls;
return isInPipMode ? WebRtcControls.PIP : controls;
}
private void startTimer() {
cancelTimer();
ellapsedTimeHandler.post(ellapsedTimeRunnable);
elapsedTimeHandler.post(elapsedTimeRunnable);
}
private void handleTick() {
@@ -208,13 +192,13 @@ public class WebRtcCallViewModel extends ViewModel {
long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000;
ellapsed.postValue(newValue);
elapsed.postValue(newValue);
ellapsedTimeHandler.postDelayed(ellapsedTimeRunnable, 1000);
elapsedTimeHandler.postDelayed(elapsedTimeRunnable, 1000);
}
private void cancelTimer() {
ellapsedTimeHandler.removeCallbacks(ellapsedTimeRunnable);
elapsedTimeHandler.removeCallbacks(elapsedTimeRunnable);
}
@Override

View File

@@ -36,24 +36,32 @@ public final class WebRtcControls {
this.audioOutput = audioOutput;
}
boolean displayStartCallControls() {
return isPreJoin();
}
boolean displayEndCall() {
return isOngoing();
return isAtLeastOutgoing();
}
boolean displayMuteAudio() {
return isOngoing();
return isPreJoin() || isAtLeastOutgoing();
}
boolean displayVideoToggle() {
return isOngoing();
return isPreJoin() || isAtLeastOutgoing();
}
boolean displayAudioToggle() {
return isOngoing() && (!isLocalVideoEnabled || isBluetoothAvailable);
return (isPreJoin() || isAtLeastOutgoing()) && (!isLocalVideoEnabled || isBluetoothAvailable);
}
boolean displayCameraToggle() {
return isOngoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
return (isPreJoin() || isAtLeastOutgoing()) && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
}
boolean displayRemoteVideoRecycler() {
return isOngoing();
}
boolean displayAnswerWithAudio() {
@@ -73,25 +81,29 @@ public final class WebRtcControls {
}
boolean isFadeOutEnabled() {
return isOngoing() && isRemoteVideoEnabled;
return isAtLeastOutgoing() && isRemoteVideoEnabled;
}
boolean displaySmallOngoingCallButtons() {
return isOngoing() && displayAudioToggle() && displayCameraToggle();
return isAtLeastOutgoing() && displayAudioToggle() && displayCameraToggle();
}
boolean displayLargeOngoingCallButtons() {
return isOngoing() && !(displayAudioToggle() && displayCameraToggle());
return isAtLeastOutgoing() && !(displayAudioToggle() && displayCameraToggle());
}
boolean displayTopViews() {
return !isInPipMode;
}
WebRtcAudioOutput getAudioOutput() {
@NonNull WebRtcAudioOutput getAudioOutput() {
return audioOutput;
}
private boolean isPreJoin() {
return callState == CallState.PRE_JOIN;
}
private boolean isOngoing() {
return callState == CallState.ONGOING;
}
@@ -100,9 +112,20 @@ public final class WebRtcControls {
return callState == CallState.INCOMING;
}
private boolean isAtLeastOutgoing() {
return callState.isAtLeast(CallState.OUTGOING);
}
public enum CallState {
NONE,
PRE_JOIN,
INCOMING,
ONGOING
OUTGOING,
ONGOING,
ENDING;
boolean isAtLeast(@NonNull CallState other) {
return compareTo(other) >= 0;
}
}
}

View File

@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.components.webrtc;
public enum WebRtcLocalRenderState {
GONE,
SMALL,
LARGE
SMALL_RECTANGLE,
SMALL_SQUARE,
LARGE,
LARGE_NO_VIDEO
}

View File

@@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.view.View;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
public class CallParticipantViewHolder extends RecipientViewHolder<CallParticipantViewState> {
public CallParticipantViewHolder(@NonNull View itemView) {
super(itemView, null);
}
@Override
public void bind(@NonNull CallParticipantViewState model) {
super.bind(model);
}
}

View File

@@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
public final class CallParticipantViewState extends RecipientMappingModel<CallParticipantViewState> {
private final CallParticipant callParticipant;
CallParticipantViewState(@NonNull CallParticipant callParticipant) {
this.callParticipant = callParticipant;
}
@Override
public @NonNull Recipient getRecipient() {
return callParticipant.getRecipient();
}
}

View File

@@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingAdapter;
public class CallParticipantsListAdapter extends MappingAdapter {
CallParticipantsListAdapter() {
registerFactory(CallParticipantsListHeader.class, new LayoutFactory<>(CallParticipantsListHeaderViewHolder::new, R.layout.call_participants_list_header));
registerFactory(CallParticipantViewState.class, new LayoutFactory<>(CallParticipantViewHolder::new, R.layout.call_participants_list_item));
}
}

View File

@@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.os.Bundle;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.MappingModel;
import java.util.ArrayList;
import java.util.List;
public class CallParticipantsListDialog extends BottomSheetDialogFragment {
private RecyclerView participantList;
private CallParticipantsListAdapter adapter;
public static void show(@NonNull FragmentManager manager) {
CallParticipantsListDialog fragment = new CallParticipantsListDialog();
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
BottomSheetUtil.show(manager, tag, this);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet);
super.onCreate(savedInstanceState);
}
@Override
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(inflater.getContext(), R.style.TextSecure_DarkTheme);
LayoutInflater themedInflater = LayoutInflater.from(contextThemeWrapper);
participantList = (RecyclerView) themedInflater.inflate(R.layout.call_participants_list_dialog, container, false);
return participantList;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final WebRtcCallViewModel viewModel = ViewModelProviders.of(requireActivity()).get(WebRtcCallViewModel.class);
initializeList();
viewModel.getCallParticipantsState().observe(getViewLifecycleOwner(), this::updateList);
}
private void initializeList() {
adapter = new CallParticipantsListAdapter();
participantList.setLayoutManager(new LinearLayoutManager(requireContext()));
participantList.setAdapter(adapter);
}
private void updateList(@NonNull CallParticipantsState callParticipantsState) {
List<MappingModel<?>> items = new ArrayList<>();
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + 1));
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
items.add(new CallParticipantViewState(callParticipant));
}
adapter.submitList(items);
}
}

View File

@@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingModel;
public class CallParticipantsListHeader implements MappingModel<CallParticipantsListHeader> {
private int participantCount;
public CallParticipantsListHeader(int participantCount) {
this.participantCount = participantCount;
}
@NonNull String getHeader(@NonNull Context context) {
return context.getResources().getQuantityString(R.plurals.CallParticipantsListDialog_in_this_call_d_people, participantCount, participantCount);
}
@Override
public boolean areItemsTheSame(@NonNull CallParticipantsListHeader newItem) {
return true;
}
@Override
public boolean areContentsTheSame(@NonNull CallParticipantsListHeader newItem) {
return participantCount == newItem.participantCount;
}
}

View File

@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingViewHolder;
public class CallParticipantsListHeaderViewHolder extends MappingViewHolder<CallParticipantsListHeader> {
private final TextView headerText;
public CallParticipantsListHeaderViewHolder(@NonNull View itemView) {
super(itemView);
headerText = findViewById(R.id.call_participants_list_header);
}
@Override
public void bind(@NonNull CallParticipantsListHeader model) {
headerText.setText(model.getHeader(getContext()));
}
}

View File

@@ -8,6 +8,8 @@ import android.graphics.drawable.LayerDrawable;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import com.amulyakhare.textdrawable.TextDrawable;
@@ -22,6 +24,8 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
private final int smallResourceId;
private final int callCardResourceId;
private ImageView.ScaleType scaleType = ImageView.ScaleType.CENTER;
public ResourceContactPhoto(@DrawableRes int resourceId) {
this(resourceId, resourceId, resourceId);
}
@@ -36,26 +40,31 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
this.smallResourceId = smallResourceId;
}
public void setScaleType(@NonNull ImageView.ScaleType scaleType) {
this.scaleType = scaleType;
}
@Override
public Drawable asDrawable(Context context, int color) {
public @NonNull Drawable asDrawable(@NonNull Context context, int color) {
return asDrawable(context, color, false);
}
@Override
public Drawable asDrawable(Context context, int color, boolean inverted) {
public @NonNull Drawable asDrawable(@NonNull Context context, int color, boolean inverted) {
return buildDrawable(context, resourceId, color, inverted);
}
@Override
public Drawable asSmallDrawable(Context context, int color, boolean inverted) {
public @NonNull Drawable asSmallDrawable(@NonNull Context context, int color, boolean inverted) {
return buildDrawable(context, smallResourceId, color, inverted);
}
private Drawable buildDrawable(Context context, int resourceId, int color, boolean inverted) {
private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, int color, boolean inverted) {
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
foreground.setScaleType(ImageView.ScaleType.CENTER);
//noinspection ConstantConditions
foreground.setScaleType(scaleType);
if (inverted) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
@@ -68,12 +77,12 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
}
@Override
public Drawable asCallCard(Context context) {
public @Nullable Drawable asCallCard(@NonNull Context context) {
return AppCompatResources.getDrawable(context, callCardResourceId);
}
private static class ExpandingLayerDrawable extends LayerDrawable {
public ExpandingLayerDrawable(Drawable[] layers) {
public ExpandingLayerDrawable(@NonNull Drawable[] layers) {
super(layers);
}

View File

@@ -1,58 +0,0 @@
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

@@ -31,6 +31,9 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Uses CDS to map E164's to UUIDs.
*/
class ContactDiscoveryV2 {
private static final String TAG = Log.tag(ContactDiscoveryV2.class);

View File

@@ -25,20 +25,21 @@ 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.MessageDatabase;
import org.thoughtcrime.securesms.database.MessageDatabase.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.database.SessionDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
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.RecipientDetails;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -50,10 +51,13 @@ 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.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.Calendar;
@@ -154,13 +158,7 @@ public class DirectoryHelper {
return RegisteredState.NOT_REGISTERED;
}
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
} else {
result = ContactDiscoveryV1.getDirectoryResult(recipient.getE164().get());
}
DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
stopwatch.split("e164-network");
@@ -179,6 +177,13 @@ public class DirectoryHelper {
} else {
recipientDatabase.markRegistered(recipient.getId());
}
} else if (recipient.hasUuid() && recipient.isRegistered() && hasCommunicatedWith(context, recipient)) {
if (isUuidRegistered(context, recipient)) {
recipientDatabase.markRegistered(recipient.getId(), recipient.requireUuid());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
stopwatch.split("e164-unlisted-network");
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
@@ -218,13 +223,7 @@ public class DirectoryHelper {
Stopwatch stopwatch = new Stopwatch("refresh");
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
} else {
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
}
DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
stopwatch.split("network");
@@ -244,6 +243,17 @@ public class DirectoryHelper {
stopwatch.split("process-cds");
UnlistedResult unlistedResult = filterForUnlistedUsers(context, inactiveIds);
inactiveIds.removeAll(unlistedResult.getPossiblyActive());
if (unlistedResult.getRetries().size() > 0) {
Log.i(TAG, "Some profile fetches failed to resolve. Assuming not-inactive for now and scheduling a retry.");
RetrieveProfileJob.enqueue(unlistedResult.getRetries());
}
stopwatch.split("handle-unlisted");
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
stopwatch.split("update-registered");
@@ -275,16 +285,10 @@ public class DirectoryHelper {
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
ProfileUtil.retrieveProfileSync(context, recipient, SignalServiceProfile.RequestType.PROFILE);
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);
} catch (NotFoundException e) {
return false;
}
}
@@ -420,6 +424,50 @@ public class DirectoryHelper {
}).collect(Collectors.toSet());
}
/**
* Users can mark themselves as 'unlisted' in CDS, meaning that even if CDS says they're
* unregistered, they might actually be registered. We need to double-check users who we already
* have UUIDs for. Also, we only want to bother doing this for users we have conversations for,
* so we will also only check for users that have a thread.
*/
private static UnlistedResult filterForUnlistedUsers(@NonNull Context context, @NonNull Set<RecipientId> inactiveIds) {
List<Recipient> possiblyUnlisted = Stream.of(inactiveIds)
.map(Recipient::resolved)
.filter(Recipient::isRegistered)
.filter(Recipient::hasUuid)
.filter(r -> hasCommunicatedWith(context, r))
.toList();
List<Pair<Recipient, ListenableFuture<ProfileAndCredential>>> futures = Stream.of(possiblyUnlisted)
.map(r -> new Pair<>(r, ProfileUtil.retrieveProfile(context, r, SignalServiceProfile.RequestType.PROFILE)))
.toList();
Set<RecipientId> potentiallyActiveIds = new HashSet<>();
Set<RecipientId> retries = new HashSet<>();
Stream.of(futures)
.forEach(pair -> {
try {
pair.second().get(5, TimeUnit.SECONDS);
potentiallyActiveIds.add(pair.first().getId());
} catch (InterruptedException | TimeoutException e) {
retries.add(pair.first().getId());
potentiallyActiveIds.add(pair.first().getId());
} catch (ExecutionException e) {
if (!(e.getCause() instanceof NotFoundException)) {
retries.add(pair.first().getId());
potentiallyActiveIds.add(pair.first().getId());
}
}
});
return new UnlistedResult(potentiallyActiveIds, retries);
}
private static boolean hasCommunicatedWith(@NonNull Context context, @NonNull Recipient recipient) {
return DatabaseFactory.getThreadDatabase(context).hasThread(recipient.getId()) ||
DatabaseFactory.getSessionDatabase(context).hasSessionFor(recipient.getId());
}
static class DirectoryResult {
private final Map<String, UUID> registeredNumbers;
private final Map<String, String> numberRewrites;
@@ -441,6 +489,24 @@ public class DirectoryHelper {
}
}
private static class UnlistedResult {
private final Set<RecipientId> possiblyActive;
private final Set<RecipientId> retries;
private UnlistedResult(@NonNull Set<RecipientId> possiblyActive, @NonNull Set<RecipientId> retries) {
this.possiblyActive = possiblyActive;
this.retries = retries;
}
@NonNull Set<RecipientId> getPossiblyActive() {
return possiblyActive;
}
@NonNull Set<RecipientId> getRetries() {
return retries;
}
}
private static class AccountHolder {
private final boolean fresh;
private final Account account;

View File

@@ -14,7 +14,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -648,7 +647,7 @@ public class Contact implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(attachment != null ? attachment.getDataUri() : null, flags);
dest.writeParcelable(attachment != null ? attachment.getUri() : null, flags);
dest.writeByte((byte) (isProfile ? 1 : 0));
}

View File

@@ -215,7 +215,7 @@ class ContactFieldAdapter extends RecyclerView.Adapter<ContactFieldAdapter.Conta
Field(@NonNull Avatar avatar) {
this.value = "";
this.iconResId = R.drawable.baseline_account_circle_white_24;
this.iconUri = avatar.getAttachment() != null ? avatar.getAttachment().getDataUri() : null;
this.iconUri = avatar.getAttachment() != null ? avatar.getAttachment().getUri() : null;
this.maxLines = 1;
this.selectable = avatar;
this.label = "";

View File

@@ -186,11 +186,11 @@ public final class ContactUtil {
intent.putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, getSystemType(contact.getPostalAddresses().get(0).getType()));
}
if (contact.getAvatarAttachment() != null && contact.getAvatarAttachment().getDataUri() != null) {
if (contact.getAvatarAttachment() != null && contact.getAvatarAttachment().getUri() != null) {
try {
ContentValues values = new ContentValues();
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.Photo.PHOTO, Util.readFully(PartAuthority.getAttachmentStream(context, contact.getAvatarAttachment().getDataUri())));
values.put(ContactsContract.CommonDataKinds.Photo.PHOTO, Util.readFully(PartAuthority.getAttachmentStream(context, contact.getAvatarAttachment().getUri())));
ArrayList<ContentValues> valuesArray = new ArrayList<>(1);
valuesArray.add(values);

View File

@@ -96,7 +96,7 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActivity {
presentContact(contact);
presentActionButtons(ContactUtil.getRecipients(this, contact));
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null);
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getUri() : null);
for (LiveRecipient recipient : activeRecipients.values()) {
recipient.observe(this, r -> presentActionButtons(Collections.singletonList(r.getId())));

View File

@@ -93,66 +93,7 @@ public class SharedContactRepository {
try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) {
VCard vcard = Ezvcard.parse(stream).first();
ezvcard.property.StructuredName vName = vcard.getStructuredName();
List<ezvcard.property.Telephone> vPhones = vcard.getTelephoneNumbers();
List<ezvcard.property.Email> vEmails = vcard.getEmails();
List<ezvcard.property.Address> vPostalAddresses = vcard.getAddresses();
String organization = vcard.getOrganization() != null && !vcard.getOrganization().getValues().isEmpty() ? vcard.getOrganization().getValues().get(0) : null;
String displayName = vcard.getFormattedName() != null ? vcard.getFormattedName().getValue() : null;
if (displayName == null && vName != null) {
displayName = vName.getGiven();
}
if (displayName == null && vcard.getOrganization() != null) {
displayName = organization;
}
if (displayName == null) {
throw new IOException("No valid name.");
}
Name name = new Name(displayName,
vName != null ? vName.getGiven() : null,
vName != null ? vName.getFamily() : null,
vName != null && !vName.getPrefixes().isEmpty() ? vName.getPrefixes().get(0) : null,
vName != null && !vName.getSuffixes().isEmpty() ? vName.getSuffixes().get(0) : null,
null);
List<Phone> phoneNumbers = new ArrayList<>(vPhones.size());
for (ezvcard.property.Telephone vEmail : vPhones) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
// Phone number is stored in the uri field in v4.0 only. In other versions, it is in the text field.
String phoneNumberFromText = vEmail.getText();
String extractedPhoneNumber = phoneNumberFromText == null ? vEmail.getUri().getNumber() : phoneNumberFromText;
phoneNumbers.add(new Phone(extractedPhoneNumber, phoneTypeFromVcardType(label), label));
}
List<Email> emails = new ArrayList<>(vEmails.size());
for (ezvcard.property.Email vEmail : vEmails) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
emails.add(new Email(vEmail.getValue(), emailTypeFromVcardType(label), label));
}
List<PostalAddress> postalAddresses = new ArrayList<>(vPostalAddresses.size());
for (ezvcard.property.Address vPostalAddress : vPostalAddresses) {
String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null;
postalAddresses.add(new PostalAddress(postalAddressTypeFromVcardType(label),
label,
vPostalAddress.getStreetAddress(),
vPostalAddress.getPoBox(),
null,
vPostalAddress.getLocality(),
vPostalAddress.getRegion(),
vPostalAddress.getPostalCode(),
vPostalAddress.getCountry()));
}
contact = new Contact(name, organization, phoneNumbers, emails, postalAddresses, null);
contact = VCardUtil.getContactFromVcard(vcard);
} catch (IOException e) {
Log.w(TAG, "Failed to parse the vcard.", e);
}
@@ -201,7 +142,7 @@ public class SharedContactRepository {
String number = ContactUtil.getNormalizedPhoneNumber(context, cursorNumber);
Phone existing = numberMap.get(number);
Phone candidate = new Phone(number, phoneTypeFromContactType(cursorType), cursorLabel);
Phone candidate = new Phone(number, VCardUtil.phoneTypeFromContactType(cursorType), cursorLabel);
if (existing == null || (existing.getType() == Phone.Type.CUSTOM && existing.getLabel() == null)) {
numberMap.put(number, candidate);
@@ -224,7 +165,7 @@ public class SharedContactRepository {
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE));
String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.LABEL));
emails.add(new Email(cursorEmail, emailTypeFromContactType(cursorType), cursorLabel));
emails.add(new Email(cursorEmail, VCardUtil.emailTypeFromContactType(cursorType), cursorLabel));
}
}
@@ -247,7 +188,7 @@ public class SharedContactRepository {
String cursorPostal = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE));
String cursorCountry = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY));
postalAddresses.add(new PostalAddress(postalAddressTypeFromContactType(cursorType),
postalAddresses.add(new PostalAddress(VCardUtil.postalAddressTypeFromContactType(cursorType),
cursorLabel,
cursorStreet,
cursorPoBox,
@@ -304,70 +245,6 @@ public class SharedContactRepository {
return null;
}
private Phone.Type phoneTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
return Phone.Type.HOME;
case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
return Phone.Type.MOBILE;
case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
return Phone.Type.WORK;
}
return Phone.Type.CUSTOM;
}
private Phone.Type phoneTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Phone.Type.HOME;
else if ("cell".equalsIgnoreCase(type)) return Phone.Type.MOBILE;
else if ("work".equalsIgnoreCase(type)) return Phone.Type.WORK;
else return Phone.Type.CUSTOM;
}
private Email.Type emailTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
return Email.Type.HOME;
case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
return Email.Type.MOBILE;
case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
return Email.Type.WORK;
}
return Email.Type.CUSTOM;
}
private Email.Type emailTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Email.Type.HOME;
else if ("cell".equalsIgnoreCase(type)) return Email.Type.MOBILE;
else if ("work".equalsIgnoreCase(type)) return Email.Type.WORK;
else return Email.Type.CUSTOM;
}
private PostalAddress.Type postalAddressTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
return PostalAddress.Type.HOME;
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
return PostalAddress.Type.WORK;
}
return PostalAddress.Type.CUSTOM;
}
private PostalAddress.Type postalAddressTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return PostalAddress.Type.HOME;
else if ("work".equalsIgnoreCase(type)) return PostalAddress.Type.WORK;
else return PostalAddress.Type.CUSTOM;
}
private String getCleanedVcardType(@Nullable String type) {
if (TextUtils.isEmpty(type)) return "";
if (type.startsWith("x-") && type.length() > 2) {
return type.substring(2);
}
return type;
}
interface ValueCallback<T> {
void onComplete(@NonNull T value);
}

View File

@@ -0,0 +1,160 @@
package org.thoughtcrime.securesms.contactshare;
import android.provider.ContactsContract;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import ezvcard.Ezvcard;
import ezvcard.VCard;
public final class VCardUtil {
private VCardUtil(){}
private static final String TAG = VCardUtil.class.getSimpleName();
public static List<Contact> parseContacts(@NonNull String vCardData) {
List<VCard> vContacts = Ezvcard.parse(vCardData).all();
List<Contact> contacts = new LinkedList<>();
for (VCard vCard: vContacts){
contacts.add(getContactFromVcard(vCard));
}
return contacts;
}
static @Nullable Contact getContactFromVcard(@NonNull VCard vcard) {
ezvcard.property.StructuredName vName = vcard.getStructuredName();
List<ezvcard.property.Telephone> vPhones = vcard.getTelephoneNumbers();
List<ezvcard.property.Email> vEmails = vcard.getEmails();
List<ezvcard.property.Address> vPostalAddresses = vcard.getAddresses();
String organization = vcard.getOrganization() != null && !vcard.getOrganization().getValues().isEmpty() ? vcard.getOrganization().getValues().get(0) : null;
String displayName = vcard.getFormattedName() != null ? vcard.getFormattedName().getValue() : null;
if (displayName == null && vName != null) {
displayName = vName.getGiven();
}
if (displayName == null && vcard.getOrganization() != null) {
displayName = organization;
}
if (displayName == null) {
Log.w(TAG, "Failed to parse the vcard: No valid name.");
return null;
}
Contact.Name name = new Contact.Name(displayName,
vName != null ? vName.getGiven() : null,
vName != null ? vName.getFamily() : null,
vName != null && !vName.getPrefixes().isEmpty() ? vName.getPrefixes().get(0) : null,
vName != null && !vName.getSuffixes().isEmpty() ? vName.getSuffixes().get(0) : null,
null);
List<Contact.Phone> phoneNumbers = new ArrayList<>(vPhones.size());
for (ezvcard.property.Telephone vEmail : vPhones) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
// Phone number is stored in the uri field in v4.0 only. In other versions, it is in the text field.
String phoneNumberFromText = vEmail.getText();
String extractedPhoneNumber = phoneNumberFromText == null ? vEmail.getUri().getNumber() : phoneNumberFromText;
phoneNumbers.add(new Contact.Phone(extractedPhoneNumber, phoneTypeFromVcardType(label), label));
}
List<Contact.Email> emails = new ArrayList<>(vEmails.size());
for (ezvcard.property.Email vEmail : vEmails) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
emails.add(new Contact.Email(vEmail.getValue(), emailTypeFromVcardType(label), label));
}
List<Contact.PostalAddress> postalAddresses = new ArrayList<>(vPostalAddresses.size());
for (ezvcard.property.Address vPostalAddress : vPostalAddresses) {
String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null;
postalAddresses.add(new Contact.PostalAddress(postalAddressTypeFromVcardType(label),
label,
vPostalAddress.getStreetAddress(),
vPostalAddress.getPoBox(),
null,
vPostalAddress.getLocality(),
vPostalAddress.getRegion(),
vPostalAddress.getPostalCode(),
vPostalAddress.getCountry()));
}
return new Contact(name, organization, phoneNumbers, emails, postalAddresses, null);
}
static Contact.Phone.Type phoneTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
return Contact.Phone.Type.HOME;
case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
return Contact.Phone.Type.MOBILE;
case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
return Contact.Phone.Type.WORK;
}
return Contact.Phone.Type.CUSTOM;
}
private static Contact.Phone.Type phoneTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Contact.Phone.Type.HOME;
else if ("cell".equalsIgnoreCase(type)) return Contact.Phone.Type.MOBILE;
else if ("work".equalsIgnoreCase(type)) return Contact.Phone.Type.WORK;
else return Contact.Phone.Type.CUSTOM;
}
static Contact.Email.Type emailTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
return Contact.Email.Type.HOME;
case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
return Contact.Email.Type.MOBILE;
case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
return Contact.Email.Type.WORK;
}
return Contact.Email.Type.CUSTOM;
}
private static Contact.Email.Type emailTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Contact.Email.Type.HOME;
else if ("cell".equalsIgnoreCase(type)) return Contact.Email.Type.MOBILE;
else if ("work".equalsIgnoreCase(type)) return Contact.Email.Type.WORK;
else return Contact.Email.Type.CUSTOM;
}
static Contact.PostalAddress.Type postalAddressTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
return Contact.PostalAddress.Type.HOME;
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
return Contact.PostalAddress.Type.WORK;
}
return Contact.PostalAddress.Type.CUSTOM;
}
private static Contact.PostalAddress.Type postalAddressTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Contact.PostalAddress.Type.HOME;
else if ("work".equalsIgnoreCase(type)) return Contact.PostalAddress.Type.WORK;
else return Contact.PostalAddress.Type.CUSTOM;
}
private static String getCleanedVcardType(@Nullable String type) {
if (TextUtils.isEmpty(type)) return "";
if (type.startsWith("x-") && type.length() > 2) {
return type.substring(2);
}
return type;
}
}

View File

@@ -1,8 +1,6 @@
package org.thoughtcrime.securesms.conversation;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
@@ -10,7 +8,6 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -19,6 +16,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.StorageUtil;
import java.util.Arrays;
import java.util.List;
@@ -84,7 +82,7 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
}
public void onMediaChanged(@NonNull List<Media> media) {
if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
if (StorageUtil.canReadFromMediaStore()) {
mediaAdapter.setMedia(media);
permissionButton.setVisibility(GONE);
permissionText.setVisibility(GONE);

View File

@@ -98,7 +98,6 @@ import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.ComposeText;
@@ -116,6 +115,7 @@ import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder;
import org.thoughtcrime.securesms.components.reminder.Reminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
@@ -163,6 +163,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupChangeResult;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.invites.InviteReminderModel;
@@ -366,6 +367,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private ConversationViewModel viewModel;
private InviteReminderModel inviteReminderModel;
private ConversationGroupViewModel groupViewModel;
private MentionsPickerViewModel mentionsViewModel;
private LiveRecipient recipient;
private long threadId;
@@ -442,8 +444,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
initializeStickerObserver();
initializeViewModel();
initializeGroupViewModel();
if (FeatureFlags.mentions()) initializeMentionsViewModel();
initializeMentionsViewModel();
initializeEnabledCheck();
initializePendingRequestsBanner();
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
@@ -556,7 +559,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
fragment.setLastSeen(System.currentTimeMillis());
markLastSeen();
AudioSlidePlayer.stopAll();
EventBus.getDefault().unregister(this);
}
@@ -676,11 +678,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
for (Media mediaItem : result.getNonUploadedMedia()) {
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull()));
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull()));
} else if (MediaUtil.isGif(mediaItem.getMimeType())) {
slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull()));
slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull()));
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), mediaItem.getMimeType(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull(), null));
slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), mediaItem.getMimeType(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull(), null));
} else {
Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping.");
}
@@ -815,10 +817,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
} else {
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
}
inflater.inflate(R.menu.conversation_active_group_options, menu);
} else if (isActiveV2Group || isActiveGroup) {
inflater.inflate(R.menu.conversation_active_group_options, menu);
}
inflater.inflate(R.menu.conversation_active_group_options, menu);
}
inflater.inflate(R.menu.conversation, menu);
@@ -865,7 +866,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isActiveV2Group) {
hideMenuItem(menu, R.id.menu_mute_notifications);
hideMenuItem(menu, R.id.menu_conversation_settings);
} else if (isActiveGroup) {
} else if (isGroupConversation()) {
hideMenuItem(menu, R.id.menu_conversation_settings);
}
@@ -1033,6 +1034,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
Permissions.with(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.onAllGranted(() -> viewModel.onAttachmentKeyboardOpen())
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.execute();
}
@@ -1527,6 +1529,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
});
}
private void initializePendingRequestsBanner() {
groupViewModel.getActionableRequestingMembers()
.observe(this, actionablePendingGroupRequests -> updateReminders());
}
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
SettableFuture<Boolean> future = new SettableFuture<>();
@@ -1680,7 +1687,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
protected void updateReminders() {
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
Integer actionableRequestingMembers = groupViewModel.getActionableRequestingMembers().getValue();
if (UnauthorizedReminder.isEligible(this)) {
reminderView.get().showReminder(new UnauthorizedReminder(this));
@@ -1698,6 +1706,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
reminderView.get().setOnActionClickListener(this::handleReminderAction);
reminderView.get().setOnDismissListener(() -> inviteReminderModel.dismissReminder());
reminderView.get().showReminder(inviteReminder.get());
} else if (actionableRequestingMembers != null && actionableRequestingMembers > 0 && FeatureFlags.groupsV2manageGroupLinks()) {
reminderView.get().showReminder(PendingGroupJoinRequestsReminder.create(this, actionableRequestingMembers));
reminderView.get().setOnActionClickListener(id -> {
if (id == R.id.reminder_action_review_join_requests) {
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(this, getRecipient().getGroupId().get().requireV2()));
}
});
} else if (reminderView.resolved()) {
reminderView.get().hide();
}
@@ -1913,13 +1928,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (previewState == null) return;
if (previewState.isLoading()) {
Log.d(TAG, "Loading link preview.");
inputPanel.setLinkPreviewLoading();
} else if (previewState.hasLinks() && !previewState.getLinkPreview().isPresent()) {
Log.d(TAG, "No preview found.");
inputPanel.setLinkPreviewNoPreview(previewState.getError());
} else {
Log.d(TAG, "Setting link preview: " + previewState.getLinkPreview().isPresent());
inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview());
}
@@ -1984,7 +1996,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private void initializeMentionsViewModel() {
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
recipient.observe(this, r -> {
if (r.isPushV2Group() && !mentionsSuggestions.resolved()) {
@@ -1994,7 +2006,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
});
composeText.setMentionQueryChangedListener(query -> {
if (getRecipient().isPushV2Group()) {
if (getRecipient().isPushV2Group() && getRecipient().isActiveGroup()) {
if (!mentionsSuggestions.resolved()) {
mentionsSuggestions.get();
}
@@ -2003,7 +2015,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
});
composeText.setMentionValidator(annotations -> {
if (!getRecipient().isPushV2Group()) {
if (!getRecipient().isPushV2Group() || !getRecipient().isActiveGroup()) {
return annotations;
}
@@ -2126,6 +2138,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (groupViewModel != null) {
groupViewModel.onRecipientChange(recipient);
}
if (mentionsViewModel != null) {
mentionsViewModel.onRecipientChange(recipient);
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
@@ -2230,6 +2246,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
private Drafts getDraftsForCurrentState() {
Drafts drafts = new Drafts();
if (recipient.get().isGroup() && !recipient.get().isActiveGroup()) {
return drafts;
}
if (!Util.isEmpty(composeText)) {
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed().toString()));
List<Mention> draftMentions = composeText.getMentions();
@@ -3039,7 +3059,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
messageRequestBottomView.setBlockOnClickListener(v -> onMessageRequestBlockClicked(viewModel));
messageRequestBottomView.setUnblockOnClickListener(v -> onMessageRequestUnblockClicked(viewModel));
viewModel.getRecipient().observe(this, this::presentMessageRequestBottomViewTo);
viewModel.getMessageData().observe(this, this::presentMessageRequestBottomViewTo);
viewModel.getMessageRequestDisplayState().observe(this, this::presentMessageRequestDisplayState);
viewModel.getFailures().observe(this, this::showGroupChangeErrorToast);
viewModel.getMessageRequestStatus().observe(this, status -> {
@@ -3084,7 +3104,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
{
reactionOverlay.setOnToolbarItemClickedListener(toolbarListener);
reactionOverlay.setOnHideListener(onHideListener);
reactionOverlay.show(this, maskTarget, messageRecord, inputAreaHeight());
reactionOverlay.show(this, maskTarget, recipient.get(), messageRecord, inputAreaHeight());
}
@Override
@@ -3272,7 +3292,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private void presentMessageRequestDisplayState(@NonNull MessageRequestViewModel.DisplayState displayState) {
if (getIntent().hasExtra(TEXT_EXTRA) || getIntent().hasExtra(MEDIA_EXTRA) || getIntent().hasExtra(STICKER_EXTRA)) {
if ((getIntent().hasExtra(TEXT_EXTRA) && !Util.isEmpty(getIntent().getStringExtra(TEXT_EXTRA))) ||
getIntent().hasExtra(MEDIA_EXTRA) ||
getIntent().hasExtra(STICKER_EXTRA))
{
Log.d(TAG, "[presentMessageRequestDisplayState] Have extra, so ignoring provided state.");
messageRequestBottomView.setVisibility(View.GONE);
} else if (isPushGroupV1Conversation() && !isActiveGroup()) {
@@ -3287,7 +3310,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
groupShareProfileView.get().setVisibility(View.GONE);
}
break;
case DISPLAY_LEGACY:
case DISPLAY_PRE_MESSAGE_REQUEST:
if (recipient.get().isGroup()) {
groupShareProfileView.get().setRecipient(recipient.get());
groupShareProfileView.get().setVisibility(View.VISIBLE);
@@ -3337,7 +3360,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
long expiresIn = recipient.get().getExpireMessages() * 1000L;
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
boolean initiating = threadId == -1;
QuoteModel quote = inputPanel.getQuote().orNull();
SlideDeck slideDeck = new SlideDeck();
if (MediaUtil.isGif(contentType)) {
@@ -3351,7 +3373,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
sendMediaMessage(isSmsForced(),
"",
slideDeck,
quote,
null,
Collections.emptyList(),
Collections.emptyList(),
composeText.getMentions(),
@@ -3359,7 +3381,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
false,
subscriptionId,
initiating,
true);
false);
}
private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
@@ -3453,10 +3475,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
}
private void presentMessageRequestBottomViewTo(@Nullable Recipient recipient) {
if (recipient == null) return;
private void presentMessageRequestBottomViewTo(@Nullable MessageRequestViewModel.MessageData messageData) {
if (messageData == null) return;
messageRequestBottomView.setRecipient(recipient);
messageRequestBottomView.setMessageData(messageData);
}
private static class KeyboardImageDetails {

View File

@@ -27,6 +27,7 @@ import androidx.annotation.LayoutRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.paging.PagedList;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
@@ -84,6 +85,7 @@ public class ConversationAdapter
private static final long FOOTER_ID = Long.MIN_VALUE + 1;
private final ItemClickListener clickListener;
private final LifecycleOwner lifecycleOwner;
private final GlideRequests glideRequests;
private final Locale locale;
private final Recipient recipient;
@@ -99,12 +101,14 @@ public class ConversationAdapter
private View headerView;
private View footerView;
ConversationAdapter(@NonNull GlideRequests glideRequests,
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable ItemClickListener clickListener,
@NonNull Recipient recipient)
{
super(new DiffCallback());
this.lifecycleOwner = lifecycleOwner;
this.glideRequests = glideRequests;
this.locale = locale;
@@ -170,8 +174,6 @@ public class ConversationAdapter
case MESSAGE_TYPE_OUTGOING_TEXT:
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
case MESSAGE_TYPE_UPDATE:
long start = System.currentTimeMillis();
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
BindableConversationItem bindable = (BindableConversationItem) itemView;
@@ -190,7 +192,6 @@ public class ConversationAdapter
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);
case MESSAGE_TYPE_PLACEHOLDER:
View v = new FrameLayout(parent.getContext());
@@ -219,7 +220,8 @@ public class ConversationAdapter
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
conversationViewHolder.getBindable().bind(conversationMessage,
conversationViewHolder.getBindable().bind(lifecycleOwner,
conversationMessage,
Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null),
Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null),
glideRequests,

View File

@@ -84,6 +84,8 @@ class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
}
}
long mentionStart = System.currentTimeMillis();
mentionHelper.fetchMentions(context);
if (!isInvalid()) {
@@ -94,7 +96,7 @@ class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
.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());
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms (mentions: " + (System.currentTimeMillis() - mentionStart) + " 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");
}
@@ -116,6 +118,8 @@ class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
}
}
long mentionStart = System.currentTimeMillis();
mentionHelper.fetchMentions(context);
List<ConversationMessage> items = Stream.of(records)
@@ -123,7 +127,7 @@ class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
.toList();
callback.onResult(items);
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms (mentions: " + (System.currentTimeMillis() - mentionStart) + " ms) | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
}
private static class MentionHelper {

View File

@@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms.conversation;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
@@ -52,6 +53,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.text.HtmlCompat;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -70,6 +72,8 @@ import org.thoughtcrime.securesms.components.ConversationScrollToView;
import org.thoughtcrime.securesms.components.ConversationTypingView;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
@@ -99,6 +103,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
@@ -115,12 +120,12 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -179,6 +184,7 @@ public class ConversationFragment extends LoggingFragment {
private Animation mentionButtonOutAnimation;
private OnScrollListener conversationScrollListener;
private int pulsePosition = -1;
private VoiceNoteMediaController voiceNoteMediaController;
public static void prepare(@NonNull Context context) {
FrameLayout parent = new FrameLayout(context);
@@ -226,7 +232,10 @@ public class ConversationFragment extends LoggingFragment {
new ConversationItemSwipeCallback(
conversationMessage -> actionMode == null &&
MenuState.canReplyToMessage(MenuState.isActionMessage(conversationMessage.getMessageRecord()), conversationMessage.getMessageRecord(), messageRequestViewModel.shouldShowMessageRequest()),
MenuState.canReplyToMessage(recipient.get(),
MenuState.isActionMessage(conversationMessage.getMessageRecord()),
conversationMessage.getMessageRecord(),
messageRequestViewModel.shouldShowMessageRequest()),
this::handleReplyMessage
).attachToRecyclerView(list);
@@ -303,6 +312,7 @@ public class ConversationFragment extends LoggingFragment {
initializeResources();
initializeMessageRequestViewModel();
initializeListAdapter();
voiceNoteMediaController = new VoiceNoteMediaController((AppCompatActivity) requireActivity());
}
@Override
@@ -345,9 +355,17 @@ public class ConversationFragment extends LoggingFragment {
actionMode.finish();
}
long oldThreadId = threadId;
initializeResources();
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
initializeListAdapter();
int startingPosition = getStartPosition();
if (startingPosition != -1 && oldThreadId == threadId) {
list.post(() -> moveToPosition(startingPosition, () -> Log.w(TAG, "Could not scroll to requested message.")));
} else {
initializeListAdapter();
}
}
public void moveToLastSeen() {
@@ -365,6 +383,10 @@ public class ConversationFragment extends LoggingFragment {
snapToTopDataObserver.requestScrollPosition(position);
}
private int getStartPosition() {
return requireActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
}
private void initializeMessageRequestViewModel() {
MessageRequestViewModel.Factory factory = new MessageRequestViewModel.Factory(requireContext());
@@ -452,7 +474,7 @@ public class ConversationFragment extends LoggingFragment {
private void initializeResources() {
long oldThreadId = threadId;
int startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
int startingPosition = getStartPosition();
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
@@ -479,7 +501,7 @@ public class ConversationFragment extends LoggingFragment {
private void initializeListAdapter() {
if (this.recipient != null && this.threadId != -1) {
Log.d(TAG, "Initializing adapter for " + recipient.getId());
ConversationAdapter adapter = new ConversationAdapter(GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
list.setAdapter(adapter);
setStickyHeaderDecoration(adapter);
ConversationAdapter.initializePool(list.getRecycledViewPool());
@@ -573,7 +595,7 @@ public class ConversationFragment extends LoggingFragment {
return;
}
MenuState menuState = MenuState.getMenuState(Stream.of(messages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet()), messageRequestViewModel.shouldShowMessageRequest());
MenuState menuState = MenuState.getMenuState(recipient.get(), Stream.of(messages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet()), messageRequestViewModel.shouldShowMessageRequest());
menu.findItem(R.id.menu_context_forward).setVisible(menuState.shouldShowForwardAction());
menu.findItem(R.id.menu_context_reply).setVisible(menuState.shouldShowReplyAction());
@@ -662,53 +684,7 @@ public class ConversationFragment extends LoggingFragment {
private void handleDeleteMessages(final Set<ConversationMessage> conversationMessages) {
Set<MessageRecord> messageRecords = Stream.of(conversationMessages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet());
if (FeatureFlags.remoteDelete()) {
buildRemoteDeleteConfirmationDialog(messageRecords).show();
} else {
buildLegacyDeleteConfirmationDialog(messageRecords).show();
}
}
private AlertDialog.Builder buildLegacyDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
int messagesCount = messageRecords.size();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIconAttribute(R.attr.dialog_alert_icon);
builder.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messagesCount, messagesCount));
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
builder.setCancelable(true);
builder.setPositiveButton(R.string.delete, (dialog, which) -> {
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
R.string.ConversationFragment_deleting,
R.string.ConversationFragment_deleting_messages)
{
@Override
protected Void doInBackground(Void... voids) {
for (MessageRecord messageRecord : messageRecords) {
boolean threadDeleted;
if (messageRecord.isMms()) {
threadDeleted = DatabaseFactory.getMmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
} else {
threadDeleted = DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
}
if (threadDeleted) {
threadId = -1;
conversationViewModel.clearThreadId();
messageCountsViewModel.clearThreadId();
listener.setThreadId(threadId);
}
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
builder.setNegativeButton(android.R.string.cancel, null);
return builder;
buildRemoteDeleteConfirmationDialog(messageRecords).show();
}
private AlertDialog.Builder buildRemoteDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
@@ -769,7 +745,7 @@ public class ConversationFragment extends LoggingFragment {
deleteForEveryone.run();
} else {
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.ConversationFragment_this_message_will_be_permanently_deleted_for_everyone)
.setMessage(R.string.ConversationFragment_this_message_will_be_deleted_for_everyone_in_the_conversation)
.setPositiveButton(R.string.ConversationFragment_delete_for_everyone, (dialog, which) -> {
SignalStore.uiHints().markHasConfirmedDeleteForEveryoneOnce();
deleteForEveryone.run();
@@ -810,7 +786,7 @@ public class ConversationFragment extends LoggingFragment {
.toList();
for (Attachment attachment : attachments) {
Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
Uri uri = attachment.getUri();
if (uri != null) {
mediaList.add(new Media(uri,
@@ -881,26 +857,40 @@ public class ConversationFragment extends LoggingFragment {
throw new AssertionError("Cannot save a view-once message.");
}
SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
.toList();
if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
return;
}
Log.w(TAG, "No slide with attachable media found, failing nicely.");
Toast.makeText(getActivity(),
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show();
SaveAttachmentTask.showWarningDialog(getActivity(), (dialog, which) -> {
if (StorageUtil.canWriteToMediaStore()) {
performSave(message);
return;
}
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> performSave(message))
.execute();
});
}
private void performSave(final MediaMmsMessageRecord message) {
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
.toList();
if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
return;
}
Log.w(TAG, "No slide with attachable media found, failing nicely.");
Toast.makeText(getActivity(),
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show();
}
private void clearHeaderIfNotTyping(ConversationAdapter adapter) {
if (adapter.getHeaderView() != typingView) {
adapter.setHeaderView(null);
@@ -1223,11 +1213,12 @@ public class ConversationFragment extends LoggingFragment {
MessageRecord messageRecord = conversationMessage.getMessageRecord();
if (messageRecord.isSecure() &&
!messageRecord.isRemoteDelete() &&
!messageRecord.isUpdate() &&
!recipient.get().isBlocked() &&
!messageRequestViewModel.shouldShowMessageRequest() &&
if (messageRecord.isSecure() &&
!messageRecord.isRemoteDelete() &&
!messageRecord.isUpdate() &&
!recipient.get().isBlocked() &&
!messageRequestViewModel.shouldShowMessageRequest() &&
(!recipient.get().isGroup() || recipient.get().isActiveGroup()) &&
((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty())
{
isReacting = true;
@@ -1397,6 +1388,31 @@ public class ConversationFragment extends LoggingFragment {
listener.onMessageWithErrorClicked(messageRecord);
}
@Override
public void onVoiceNotePause(@NonNull Uri uri) {
voiceNoteMediaController.pausePlayback(uri);
}
@Override
public void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress) {
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
}
@Override
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
voiceNoteMediaController.seekToPosition(uri, progress);
}
@Override
public void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), onPlaybackStartObserver);
}
@Override
public void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(onPlaybackStartObserver);
}
@Override
public boolean onUrlClicked(@NonNull String url) {
return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url);
@@ -1424,7 +1440,10 @@ public class ConversationFragment extends LoggingFragment {
public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView,
@Nullable ScrollRequestValidator scrollRequestValidator)
{
super(recyclerView, scrollRequestValidator);
super(recyclerView, scrollRequestValidator, () -> {
list.scrollToPosition(0);
list.post(ConversationFragment.this::postMarkAsReadRequest);
});
}
@Override

View File

@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
@@ -30,20 +31,29 @@ final class ConversationGroupViewModel extends ViewModel {
private final MutableLiveData<Recipient> liveRecipient;
private final LiveData<GroupActiveState> groupActiveState;
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
private final LiveData<Integer> actionableRequestingMembers;
private ConversationGroupViewModel() {
this.liveRecipient = new MutableLiveData<>();
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
}
void onRecipientChange(Recipient recipient) {
liveRecipient.setValue(recipient);
}
/**
* The number of pending group join requests that can be actioned by this client.
*/
LiveData<Integer> getActionableRequestingMembers() {
return actionableRequestingMembers;
}
LiveData<GroupActiveState> getGroupActiveState() {
return groupActiveState;
}
@@ -62,6 +72,20 @@ final class ConversationGroupViewModel extends ViewModel {
}
}
private static int mapToActionableRequestingMemberCount(@Nullable GroupRecord record) {
if (record != null &&
FeatureFlags.groupsV2manageGroupLinks() &&
record.isV2Group() &&
record.memberLevel(Recipient.self()) == GroupDatabase.MemberLevel.ADMINISTRATOR)
{
return record.requireV2GroupProperties()
.getDecryptedGroup()
.getRequestingMembersCount();
} else {
return 0;
}
}
private static GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) {
if (record == null) {
return null;

View File

@@ -54,6 +54,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import com.annimon.stream.Stream;
@@ -79,8 +80,6 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -116,9 +115,9 @@ 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.UrlClickHandler;
import org.thoughtcrime.securesms.util.Util;
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;
@@ -250,7 +249,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
@Override
public void bind(@NonNull ConversationMessage conversationMessage,
public void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@@ -336,6 +336,16 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
}
if (hasSharedContact(messageRecord)) {
int contactWidth = sharedContactStub.get().getMeasuredWidth();
int availableWidth = getAvailableMessageBubbleWidth(sharedContactStub.get());
if (contactWidth != availableWidth) {
sharedContactStub.get().getLayoutParams().width = availableWidth;
needsMeasure = true;
}
}
ConversationItemFooter activeFooter = getActiveFooter(messageRecord);
int availableWidth = getAvailableMessageBubbleWidth(footer);
@@ -397,6 +407,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
conversationRecipient.removeForeverObserver(this);
}
cancelPulseOutlinerAnimation();
if (eventListener != null && audioViewStub.resolved()) {
eventListener.onUnregisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
}
}
public ConversationMessage getConversationMessage() {
@@ -410,14 +423,17 @@ public class ConversationItem extends LinearLayout implements BindableConversati
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));
footer.setOnlyShowSendingStatus(false, 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));
footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord);
} else {
bodyBubble.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
footer.setOnlyShowSendingStatus(false, messageRecord);
}
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_sent_text_secondary_color));
@@ -642,7 +658,11 @@ public class ConversationItem extends LinearLayout implements BindableConversati
{
boolean showControls = !messageRecord.isFailed();
if (isViewOnceMessage(messageRecord)) {
if (eventListener != null && audioViewStub.resolved()) {
eventListener.onUnregisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
}
if (isViewOnceMessage(messageRecord) && !messageRecord.isRemoteDelete()) {
revealableStub.get().setVisibility(VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
@@ -725,10 +745,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), new AudioViewCallbacks(), showControls);
audioViewStub.get().setDownloadClickListener(singleDownloadClickListener);
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
if (eventListener != null) {
eventListener.onRegisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
}
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -892,12 +916,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
private void setSharedContactCorners(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) {
sharedContactStub.get().setSingularStyle();
} else if (current.isOutgoing()) {
sharedContactStub.get().setClusteredOutgoingStyle();
} else {
sharedContactStub.get().setClusteredIncomingStyle();
if (TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))){
if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) {
sharedContactStub.get().setSingularStyle();
} else if (current.isOutgoing()) {
sharedContactStub.get().setClusteredOutgoingStyle();
} else {
sharedContactStub.get().setClusteredIncomingStyle();
}
}
}
@@ -1075,7 +1101,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
return stickerFooter;
} else if (hasSharedContact(messageRecord)) {
} else if (hasSharedContact(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
return sharedContactStub.get().getFooter();
} else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
return mediaThumbnailStub.get().getFooter();
@@ -1442,7 +1468,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
Log.i(TAG, "Public URI: " + publicUri);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType());
intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), Intent.normalizeMimeType(slide.getContentType()));
try {
context.startActivity(intent);
} catch (ActivityNotFoundException anfe) {
@@ -1509,7 +1535,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
@Override
public void onClick(@NonNull View widget) {
if (eventListener != null && !Recipient.resolved(mentionedRecipientId).isLocalNumber()) {
if (eventListener != null && batchSelected.isEmpty()) {
VibrateUtil.vibrateTick(context);
eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId());
}
@@ -1519,6 +1545,35 @@ public class ConversationItem extends LinearLayout implements BindableConversati
public void updateDrawState(@NonNull TextPaint ds) { }
}
private final class AudioViewCallbacks implements AudioView.Callbacks {
@Override
public void onPlay(@NonNull Uri audioUri, double progress) {
if (eventListener == null) return;
eventListener.onVoiceNotePlay(audioUri, messageRecord.getId(), progress);
}
@Override
public void onPause(@NonNull Uri audioUri) {
if (eventListener == null) return;
eventListener.onVoiceNotePause(audioUri);
}
@Override
public void onSeekTo(@NonNull Uri audioUri, double progress) {
if (eventListener == null) return;
eventListener.onVoiceNoteSeekTo(audioUri, progress);
}
@Override
public void onStopAndReset(@NonNull Uri audioUri) {
throw new UnsupportedOperationException();
}
}
private void handleMessageApproval() {
final int title;
final int message;

View File

@@ -5,9 +5,7 @@ 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;
@@ -24,7 +22,6 @@ import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.core.content.ContextCompat;
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
import com.annimon.stream.Stream;
@@ -60,6 +57,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
private final PointF lastSeenDownPoint = new PointF();
private Activity activity;
private Recipient conversationRecipient;
private MessageRecord messageRecord;
private OverlayState overlayState = OverlayState.HIDDEN;
@@ -145,15 +143,21 @@ public final class ConversationReactionOverlay extends RelativeLayout {
maskView.setTargetParentTranslationY(translationY);
}
public void show(@NonNull Activity activity, @NonNull View maskTarget, @NonNull MessageRecord messageRecord, int maskPaddingBottom) {
public void show(@NonNull Activity activity,
@NonNull View maskTarget,
@NonNull Recipient conversationRecipient,
@NonNull MessageRecord messageRecord,
int maskPaddingBottom)
{
if (overlayState != OverlayState.HIDDEN) {
return;
}
this.messageRecord = messageRecord;
overlayState = OverlayState.UNINITAILIZED;
selected = -1;
this.messageRecord = messageRecord;
this.conversationRecipient = conversationRecipient;
overlayState = OverlayState.UNINITAILIZED;
selected = -1;
setupToolbarMenuItems();
setupSelectedEmoji();
@@ -498,7 +502,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
}
private void setupToolbarMenuItems() {
MenuState menuState = MenuState.getMenuState(Collections.singleton(messageRecord), false);
MenuState menuState = MenuState.getMenuState(conversationRecipient, Collections.singleton(messageRecord), false);
toolbar.getMenu().findItem(R.id.action_copy).setVisible(menuState.shouldShowCopyAction());
toolbar.getMenu().findItem(R.id.action_download).setVisible(menuState.shouldShowSaveAttachmentAction());

View File

@@ -14,6 +14,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
@@ -29,13 +30,12 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
@@ -44,9 +44,7 @@ import java.util.Set;
import java.util.concurrent.ExecutionException;
public final class ConversationUpdateItem extends LinearLayout
implements RecipientForeverObserver,
BindableConversationItem,
Observer<SpannableString>
implements BindableConversationItem
{
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
@@ -62,7 +60,8 @@ public final class ConversationUpdateItem extends LinearLayout
private Locale locale;
private LiveData<SpannableString> displayBody;
private final Debouncer bodyClearDebouncer = new Debouncer(150);
private final UpdateObserver updateObserver = new UpdateObserver();
private final SenderObserver senderObserver = new SenderObserver();
public ConversationUpdateItem(Context context) {
super(context);
@@ -85,7 +84,8 @@ public final class ConversationUpdateItem extends LinearLayout
}
@Override
public void bind(@NonNull ConversationMessage conversationMessage,
public void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@@ -97,13 +97,7 @@ public final class ConversationUpdateItem extends LinearLayout
{
this.batchSelected = batchSelected;
bind(conversationMessage, locale);
}
@Override
protected void onDetachedFromWindow() {
unbind();
super.onDetachedFromWindow();
bind(lifecycleOwner, conversationMessage, locale);
}
@Override
@@ -116,49 +110,66 @@ public final class ConversationUpdateItem extends LinearLayout
return conversationMessage;
}
private void bind(@NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
if (this.sender != null) {
this.sender.removeForeverObserver(this);
}
observeDisplayBody(null);
setBodyText(null);
private void bind(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord();
this.sender = messageRecord.getIndividualRecipient().live();
this.locale = locale;
this.sender.observeForever(this);
observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
LiveData<SpannableString> spannableStringMessage = Transformations.map(liveUpdateMessage, SpannableString::new);
LiveData<SpannableString> spannableStringMessage = toSpannable(loading(liveUpdateMessage));
present(conversationMessage);
observeDisplayBody(spannableStringMessage);
observeDisplayBody(lifecycleOwner, spannableStringMessage);
}
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) {
/** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
private @NonNull LiveData<String> loading(@NonNull LiveData<String> string) {
return LiveDataUtil.until(string, LiveDataUtil.delay(250, getContext().getString(R.string.ConversationUpdateItem_loading)));
}
private static LiveData<SpannableString> toSpannable(LiveData<String> loading) {
return Transformations.map(loading, source -> source == null ? null : new SpannableString(source));
}
@Override
public void unbind() {
}
private void observeSender(@NonNull LifecycleOwner lifecycleOwner, @Nullable Recipient recipient) {
if (sender != null) {
sender.getLiveData().removeObserver(senderObserver);
}
if (recipient != null) {
sender = recipient.live();
sender.getLiveData().observe(lifecycleOwner, senderObserver);
} else {
sender = null;
}
}
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<SpannableString> displayBody) {
if (this.displayBody != displayBody) {
if (this.displayBody != null) {
this.displayBody.removeObserver(this);
this.displayBody.removeObserver(updateObserver);
}
this.displayBody = displayBody;
if (this.displayBody != null) {
this.displayBody.observeForever(this);
this.displayBody.observe(lifecycleOwner, updateObserver);
}
}
}
private void setBodyText(@Nullable CharSequence text) {
if (text == null) {
bodyClearDebouncer.publish(() -> body.setText(null));
body.setVisibility(INVISIBLE);
} else {
bodyClearDebouncer.clear();
body.setText(text);
body.setVisibility(VISIBLE);
}
@@ -186,7 +197,7 @@ public final class ConversationUpdateItem extends LinearLayout
else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp);
else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp);
date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateReceived()));
date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateSent()));
title.setVisibility(GONE);
date.setVisibility(View.VISIBLE);
@@ -257,28 +268,25 @@ public final class ConversationUpdateItem extends LinearLayout
icon.setColorFilter(getIconTintFilter());
}
@Override
public void onRecipientChanged(@NonNull Recipient recipient) {
present(conversationMessage);
}
@Override
public void setOnClickListener(View.OnClickListener l) {
super.setOnClickListener(new InternalClickListener(l));
}
@Override
public void unbind() {
if (sender != null) {
sender.removeForeverObserver(this);
}
private final class SenderObserver implements Observer<Recipient> {
observeDisplayBody(null);
@Override
public void onChanged(Recipient recipient) {
present(conversationMessage);
}
}
@Override
public void onChanged(SpannableString update) {
setBodyText(update);
private final class UpdateObserver implements Observer<SpannableString> {
@Override
public void onChanged(SpannableString update) {
setBodyText(update);
}
}
private class InternalClickListener implements View.OnClickListener {

View File

@@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.Set;
@@ -50,7 +51,8 @@ final class MenuState {
return copy;
}
static MenuState getMenuState(@NonNull Set<MessageRecord> messageRecords,
static MenuState getMenuState(@NonNull Recipient conversationRecipient,
@NonNull Set<MessageRecord> messageRecords,
boolean shouldShowMessageRequest)
{
@@ -102,20 +104,21 @@ final class MenuState {
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null)
.shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce && !remoteDelete)
.shouldShowDetailsAction(!actionMessage)
.shouldShowReplyAction(canReplyToMessage(actionMessage, messageRecord, shouldShowMessageRequest));
.shouldShowReplyAction(canReplyToMessage(conversationRecipient, actionMessage, messageRecord, shouldShowMessageRequest));
}
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
.build();
}
static boolean canReplyToMessage(boolean actionMessage, @NonNull MessageRecord messageRecord, boolean isDisplayingMessageRequest) {
return !actionMessage &&
!messageRecord.isRemoteDelete() &&
!messageRecord.isPending() &&
!messageRecord.isFailed() &&
!isDisplayingMessageRequest &&
messageRecord.isSecure() &&
static boolean canReplyToMessage(@NonNull Recipient conversationRecipient, boolean actionMessage, @NonNull MessageRecord messageRecord, boolean isDisplayingMessageRequest) {
return !actionMessage &&
!messageRecord.isRemoteDelete() &&
!messageRecord.isPending() &&
!messageRecord.isFailed() &&
!isDisplayingMessageRequest &&
messageRecord.isSecure() &&
(!conversationRecipient.isGroup() || conversationRecipient.isActiveGroup()) &&
!messageRecord.getRecipient().isBlocked();
}

View File

@@ -1,48 +0,0 @@
package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.MappingAdapter;
import org.thoughtcrime.securesms.util.MappingViewHolder;
public class MentionViewHolder extends MappingViewHolder<MentionViewState> {
private final AvatarImageView avatar;
private final TextView name;
@Nullable private final MentionEventsListener mentionEventsListener;
public MentionViewHolder(@NonNull View itemView, @Nullable MentionEventsListener mentionEventsListener) {
super(itemView);
this.mentionEventsListener = mentionEventsListener;
avatar = findViewById(R.id.mention_recipient_avatar);
name = findViewById(R.id.mention_recipient_name);
}
@Override
public void bind(@NonNull MentionViewState model) {
avatar.setRecipient(model.getRecipient());
name.setText(model.getName(context));
itemView.setOnClickListener(v -> {
if (mentionEventsListener != null) {
mentionEventsListener.onMentionClicked(model.getRecipient());
}
});
}
public interface MentionEventsListener {
void onMentionClicked(@NonNull Recipient recipient);
}
public static MappingAdapter.Factory<MentionViewState> createFactory(@Nullable MentionEventsListener mentionEventsListener) {
return new MappingAdapter.LayoutFactory<>(view -> new MentionViewHolder(view, mentionEventsListener), R.layout.mentions_picker_recipient_list_item);
}
}

View File

@@ -1,17 +1,11 @@
package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.MappingModel;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
import java.util.Objects;
public final class MentionViewState implements MappingModel<MentionViewState> {
public final class MentionViewState extends RecipientMappingModel<MentionViewState> {
private final Recipient recipient;
@@ -19,23 +13,8 @@ public final class MentionViewState implements MappingModel<MentionViewState> {
this.recipient = recipient;
}
@NonNull String getName(@NonNull Context context) {
return recipient.getDisplayName(context);
}
@NonNull Recipient getRecipient() {
@Override
public @NonNull Recipient getRecipient() {
return recipient;
}
@Override
public boolean areItemsTheSame(@NonNull MentionViewState newItem) {
return recipient.getId().equals(newItem.recipient.getId());
}
@Override
public boolean areContentsTheSame(@NonNull MentionViewState newItem) {
Context context = ApplicationDependencies.getApplication();
return recipient.getDisplayName(context).equals(newItem.recipient.getDisplayName(context)) &&
Objects.equals(recipient.getProfileAvatar(), newItem.recipient.getProfileAvatar());
}
}

View File

@@ -3,18 +3,20 @@ package org.thoughtcrime.securesms.conversation.ui.mentions;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionViewHolder.MentionEventsListener;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingAdapter;
import org.thoughtcrime.securesms.util.MappingModel;
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder.EventListener;
import java.util.List;
public class MentionsPickerAdapter extends MappingAdapter {
private final Runnable currentListChangedListener;
public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener, @NonNull Runnable currentListChangedListener) {
public MentionsPickerAdapter(@Nullable EventListener<MentionViewState> listener, @NonNull Runnable currentListChangedListener) {
this.currentListChangedListener = currentListChangedListener;
registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener));
registerFactory(MentionViewState.class, RecipientViewHolder.createFactory(R.layout.mentions_picker_recipient_list_item, listener));
}
@Override

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -24,12 +25,14 @@ import java.util.List;
public class MentionsPickerFragment extends LoggingFragment {
private MentionsPickerAdapter adapter;
private RecyclerView list;
private View topDivider;
private View bottomDivider;
private BottomSheetBehavior<View> behavior;
private MentionsPickerViewModel viewModel;
private MentionsPickerAdapter adapter;
private RecyclerView list;
private View topDivider;
private View bottomDivider;
private BottomSheetBehavior<View> behavior;
private MentionsPickerViewModel viewModel;
private final Runnable lockSheetAfterListUpdate = () -> behavior.setHideable(false);
private final Handler handler = new Handler();
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -62,6 +65,7 @@ public class MentionsPickerFragment extends LoggingFragment {
}
private void initializeBehavior() {
behavior.setHideable(true);
behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@@ -69,6 +73,7 @@ public class MentionsPickerFragment extends LoggingFragment {
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
adapter.submitList(Collections.emptyList());
showDividers(false);
} else {
showDividers(true);
}
@@ -109,9 +114,10 @@ public class MentionsPickerFragment extends LoggingFragment {
if (isShowing) {
list.scrollToPosition(0);
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
list.post(() -> behavior.setHideable(false));
handler.post(lockSheetAfterListUpdate);
showDividers(true);
} else {
handler.removeCallbacks(lockSheetAfterListUpdate);
behavior.setHideable(true);
behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}

View File

@@ -12,8 +12,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepository.MentionQuery;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -31,12 +29,8 @@ public class MentionsPickerViewModel extends ViewModel {
private final MutableLiveData<LiveRecipient> liveRecipient;
private final MutableLiveData<Query> liveQuery;
private final MutableLiveData<Boolean> isShowing;
private final MegaphoneRepository megaphoneRepository;
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository,
@NonNull MegaphoneRepository megaphoneRepository)
{
this.megaphoneRepository = megaphoneRepository;
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) {
this.liveRecipient = new MutableLiveData<>();
this.liveQuery = new MutableLiveData<>();
this.selectedRecipient = new SingleLiveEvent<>();
@@ -44,8 +38,8 @@ public class MentionsPickerViewModel extends ViewModel {
LiveData<Recipient> recipient = Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData);
LiveData<List<RecipientId>> fullMembers = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(recipient, mentionsPickerRepository::getMembers));
LiveData<Query> query = Transformations.distinctUntilChanged(liveQuery);
LiveData<MentionQuery> mentionQuery = LiveDataUtil.combineLatest(query, fullMembers, (q, m) -> new MentionQuery(q.query, m));
LiveData<MentionQuery> mentionQuery = LiveDataUtil.combineLatest(liveQuery, fullMembers, (q, m) -> new MentionQuery(q.query, m));
this.mentionList = LiveDataUtil.mapAsync(mentionQuery, q -> Stream.of(mentionsPickerRepository.search(q)).<MappingModel<?>>map(MentionViewState::new).toList());
}
@@ -56,7 +50,6 @@ public class MentionsPickerViewModel extends ViewModel {
void onSelectionChange(@NonNull Recipient recipient) {
selectedRecipient.setValue(recipient);
megaphoneRepository.markFinished(Megaphones.Event.MENTIONS);
}
void setIsShowing(boolean isShowing) {
@@ -119,8 +112,7 @@ public class MentionsPickerViewModel extends ViewModel {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication()),
ApplicationDependencies.getMegaphoneRepository()));
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication())));
}
}
}

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