Compare commits

...

274 Commits

Author SHA1 Message Date
Greyson Parrelli
91ca19f294 Bump version to 4.76.2 2020-11-05 18:19:51 -05:00
Greyson Parrelli
71250afd2c Updated language translations. 2020-11-05 18:19:22 -05:00
Greyson Parrelli
cfdef7bca7 Only use the NATIONAL format for the US and UK. 2020-11-05 18:14:37 -05:00
Alan Evans
872f935fd5 Revert "Do not set or read quote author phone number."
This reverts commit 936e772ba0.
2020-11-05 18:56:17 -04:00
Alex Hart
0ed1f73990 Fix crash with multitouch in call screen pip. 2020-11-05 17:43:14 -04:00
Cody Henthorne
349a2f72cb Fix crash when handling call messaging failures. 2020-11-05 15:56:56 -05:00
Alex Hart
2b4a4d6109 Add support for Incoming / Outgoing Video Type. 2020-11-05 13:41:22 -04:00
Greyson Parrelli
9f882d2fbb Fix crash around creating MMS groups. 2020-11-05 10:57:31 -05:00
Greyson Parrelli
cb4a9730aa Bump version to 4.76.1 2020-11-04 20:11:55 -05:00
Greyson Parrelli
e0657d09d8 Fix issue where we weren't calling setTransactionSuccessful().
In a chain of events, this manifested by preventing the persistence of
media messages in group threads.
2020-11-04 20:07:57 -05:00
Alan Evans
01b9cb13b4 Bump version to 4.76.0 2020-11-04 16:51:23 -04:00
Alan Evans
2c7260557c Updated language translations. 2020-11-04 16:51:23 -04:00
Greyson Parrelli
9e5156ab73 Pretty-print phone numbers. 2020-11-04 16:51:23 -04:00
Alex Hart
3dc1614fbc Add basic profile spoofing detection. 2020-11-04 16:24:45 -04:00
Alan Evans
2f69a9c38e Share media from within Media Preview and share QR code image. 2020-11-04 16:05:35 -04:00
Greyson Parrelli
5e536c3fa5 Render GV1->GV2 migration event. 2020-11-04 16:05:35 -04:00
Greyson Parrelli
6bb9d27d4e Add the ability to migrate GV1 groups to GV2.
Co-authored-by: Alan Evans <alan@signal.org>
2020-11-04 16:05:35 -04:00
Greyson Parrelli
2d1bf33902 Update group table schema to support GV1->GV2 migration.
Also puts in protections to make sure we don't insert bad recipients or
groups.
2020-11-04 16:05:35 -04:00
Alan Evans
985a220fca Migrate GV1 to GV2 on to server. Allow query of group status. 2020-11-04 16:05:34 -04:00
Alex Hart
31e137cf6d Add support for MISSED_VIDEO_CALL type. 2020-11-04 16:05:34 -04:00
Alex Hart
f796447815 Add better error logging for single backup Uris. 2020-11-04 16:05:34 -04:00
Alan Evans
936e772ba0 Do not set or read quote author phone number. 2020-11-04 16:05:34 -04:00
Greyson Parrelli
ecee797d00 Always consider yourself a member of MMS groups.
Fixes #10162
2020-11-04 16:05:34 -04:00
Greyson Parrelli
357a8fc124 Remove name change for flipper and internal releases. 2020-11-04 16:02:11 -04:00
Alan Evans
1233af0ddd Add environment dimension. 2020-11-04 16:02:11 -04:00
Alex Hart
a264d10685 Fix issue with KitKat picture saves.
Fixes #10153
2020-11-04 16:01:58 -04:00
Alex Hart
ed17701a0a Remove look-behind and ding for single voice notes. 2020-11-02 11:50:37 -04:00
Greyson Parrelli
49e1ccea28 Allow more control over debug and staging signing. 2020-11-02 10:01:59 -05:00
Alan Evans
4c43b0d1e3 Update gradle plugin to 4.1.0, gradle to 6.5. 2020-11-02 10:01:59 -05:00
Greyson Parrelli
5ce09defca Include whether a user has a linked device in the debug log. 2020-11-02 10:01:59 -05:00
Greyson Parrelli
da9064b714 Bump version to 4.75.8 2020-11-02 10:00:23 -05:00
Greyson Parrelli
7bb53e4b06 Updated language translations. 2020-11-02 09:59:52 -05:00
Cody Henthorne
6a4ce1b658 Fix SMS role bug introduced for pre-Q devices. 2020-10-30 17:45:28 -04:00
Greyson Parrelli
f84595e1e8 Bump version to 4.75.7 2020-10-30 16:15:12 -04:00
Greyson Parrelli
41d5c54033 Updated language translations. 2020-10-30 16:14:30 -04:00
Greyson Parrelli
b9d6b63c09 Fix name of internal signing task. 2020-10-30 16:06:57 -04:00
Cody Henthorne
506ad0b3f1 Fix bug handling mentions in sync messages. 2020-10-30 15:13:54 -04:00
Cody Henthorne
c8302174a9 Fix mention suggestions for groups of 1.
Fixes #10152
2020-10-30 13:05:14 -04:00
Cody Henthorne
39cebfbb4e Fix SMS role request for Q+. 2020-10-30 12:34:47 -04:00
Cody Henthorne
d36ec9af47 Fix permission bug with avatar gallery selection. 2020-10-30 11:36:12 -04:00
Greyson Parrelli
5f6d971bf7 Bump version to 4.75.6 2020-10-30 08:24:14 -04:00
Greyson Parrelli
7a722d92a3 Updated language translations. 2020-10-30 08:23:25 -04:00
Greyson Parrelli
0bf0eba450 Fix NPE in BackupUtil. 2020-10-30 08:17:50 -04:00
Greyson Parrelli
d40783f794 Add signing task for internal builds. 2020-10-30 08:17:29 -04:00
Greyson Parrelli
88733473e2 Bump version to 4.75.5 2020-10-29 15:55:17 -04:00
Greyson Parrelli
7b65533095 Updated language translations. 2020-10-29 15:51:04 -04:00
Cody Henthorne
52b533c121 Add internal product flavor. 2020-10-29 15:33:15 -04:00
Greyson Parrelli
a4fa2e14fb Fix versioning issue with Dockerfile. 2020-10-29 15:31:05 -04:00
Cody Henthorne
6933f1d818 Fail call gracefully on turn server network error. 2020-10-29 13:51:30 -04:00
Greyson Parrelli
b5d6cb2a8d Notify about accidentally disabled backups. 2020-10-29 13:32:55 -04:00
Greyson Parrelli
d1478c5ce0 Reduce impact of CDS rate-limiting issues.
This will at least allow users with > RateLimit contacts to perform a successful sync. More work needs to be done here in the future to handle this better.
2020-10-29 10:16:21 -04:00
Greyson Parrelli
fbe62f0f3e Add more Huawei phones to the CameraX blacklist. 2020-10-29 08:04:29 -04:00
Greyson Parrelli
f84705b756 Include additional system properties in debuglog. 2020-10-28 17:01:34 -04:00
Cody Henthorne
cf2189c11a Ensure speakerphone is correctly enabled during call setup.
Race condition between handleStartOutgoingCall being enqueued from ringrtc and
handleSetEnableVideo being enqueued from the main thread.
2020-10-28 17:01:34 -04:00
Alex Hart
dfc4178252 Localize 'camera' folder title. 2020-10-28 17:01:34 -04:00
Greyson Parrelli
07952f2146 Bump version to 4.75.4.
Accidentally went the wrong direction with canonicalVersionCode in
4.75.3. So this release just fixes that and uses the correct
canonicalVersionCode.
2020-10-28 16:54:00 -04:00
Cody Henthorne
a90dad22a9 Bump version to 4.75.3 2020-10-28 16:22:16 -04:00
Cody Henthorne
64f7330609 Updated language translations. 2020-10-28 16:21:12 -04:00
Cody Henthorne
5e382c120b Fix security crash during directory refresh. 2020-10-28 16:14:45 -04:00
Greyson Parrelli
3eea568f5f Fix possible storage permission crash on camera. 2020-10-28 16:00:01 -04:00
Cody Henthorne
0077b29d6e Mitigate PSTN callback crash when service is in background. 2020-10-28 15:48:04 -04:00
Cody Henthorne
dfa6306b61 Bump version to 4.75.2 2020-10-26 16:08:44 -04:00
Cody Henthorne
a4bf075a1a Updated language translations. 2020-10-26 16:06:57 -04:00
Alex Hart
373d622535 Fix SMS, bad MODIFIED timestamp, and API19 beta crash. 2020-10-26 13:41:30 -03:00
Greyson Parrelli
ba1df58eb3 Do not show modern profile sharing on brand new conversations. 2020-10-26 12:08:01 -04:00
Greyson Parrelli
9fb85f7c76 Build log sections in series.
Doing them in parallel was causing possible bad blocked thread reports,
since the thread section could be built at the same time we were
building the jobs section.
2020-10-26 11:07:44 -04:00
Cody Henthorne
5e58f0a212 Bump version to 4.75.1 2020-10-23 15:45:20 -04:00
Cody Henthorne
8fa01f13e9 Updated language translations. 2020-10-23 15:44:07 -04:00
Alan Evans
4ce136be17 Fix missing message request on V2 re-invites. 2020-10-23 15:37:42 -04:00
Alan Evans
4099154dc0 Infer contact multi-select allowing assertion removal.
Hide count on invite friends.

Fixes #10125
2020-10-23 15:37:42 -04:00
Greyson Parrelli
3f983a5c82 Various UI adjustments to conversation updates. 2020-10-23 15:37:42 -04:00
Alex Hart
9743e3689a Add MimeType to MediaStore values. 2020-10-23 14:11:42 -03:00
Greyson Parrelli
1363f55f77 Fix back button behavior on OnePlus phones.
Couple things happened:
- Core issue: The device always thought the keyboard was open, so it was
always trying to dismiss the keyboard when you pressed back (instead of
actually going back)
- Big fix: Increase the tolerance of our view height differentialt that
detects if the keyboard is open
- Other fix: the getViewInset() method is always missing on Q, so as a
temp fix we fall back to the status bar height. Gets the calculation to
be closer, even if not truly correct.
2020-10-23 12:43:34 -04:00
Alex Hart
f1d98f6c7b Fix failed media saves on API < 29.
Fixes #10119
2020-10-23 13:12:07 -03:00
Alex Hart
9279a54d28 Fix bad voice note duration and listener breakage. 2020-10-23 13:00:46 -03:00
Alan Evans
81889d8130 Fix plural. 2020-10-23 11:13:37 -03:00
Cody Henthorne
6aecb8fbc1 Bump version to 4.75.0. 2020-10-22 17:04:24 -04:00
Cody Henthorne
8aa413032d Updated language translations. 2020-10-22 17:02:27 -04:00
Alan Evans
5bc4686eb8 Ignore some more ZKGroup dependent tests on mac. 2020-10-22 16:56:16 -04:00
Greyson Parrelli
f676d1c61c Enforce a configurable max envelope size. 2020-10-22 16:56:16 -04:00
Alex Hart
ac54b5cbdf Add polish to voice note bubbles. 2020-10-22 16:56:16 -04:00
Alan Evans
b4b1e5b605 Add feature flag driven group recommended size and hard size limits. 2020-10-22 16:56:16 -04:00
Greyson Parrelli
5eace49739 Improve PushProcessMessageJob logging. 2020-10-22 16:56:16 -04:00
Alex Hart
e93d7518f3 Add some polish to backups changes. 2020-10-22 16:56:16 -04:00
Greyson Parrelli
9c97cd8816 Improve conversation update message stylings. 2020-10-22 16:56:16 -04:00
Jim Gustafson
90f20c36c5 Update to RingRTC v2.7.3 2020-10-22 16:56:16 -04:00
Greyson Parrelli
9f8dd7992a Remove remote delete option for group updates. 2020-10-22 16:56:16 -04:00
Alex Hart
f4d3fe9176 Implement better backup failure notification strategy. 2020-10-22 16:56:16 -04:00
Alan Evans
ffc7c13717 Group GET 404 and PUT 409 handling. 2020-10-22 16:56:16 -04:00
Greyson Parrelli
daf93c473b Reduce verbosity of KeyboardAwareLinearLayout logs. 2020-10-22 16:56:16 -04:00
Greyson Parrelli
d21782696a Read the new GV1 Migration capability. 2020-10-22 15:55:18 -03:00
Greyson Parrelli
3357475fc4 Move capabilities into a single column. 2020-10-22 15:55:18 -03:00
Greyson Parrelli
ead64d92a5 Rename Recipient.isLocalNumber() to Recipient.isSelf() 2020-10-22 15:55:18 -03:00
Cody Henthorne
5eaac6cb17 Call handling state machine refactor. 2020-10-22 15:55:18 -03:00
Alex Hart
b3f0a44f10 Bump version to 4.74.3 2020-10-21 11:11:43 -03:00
Alex Hart
e4d0e2f730 Updated language translations. 2020-10-21 11:11:43 -03:00
Cody Henthorne
492a42883e Change Surveygizmo to Alchemer due to name change. 2020-10-21 11:11:43 -03:00
Alex Hart
b182f73415 Fix wakelock release exception. 2020-10-21 11:11:42 -03:00
Alan Evans
e766b9737e Do not enable admin approval on group links by default. 2020-10-20 19:39:51 -03:00
Alan Evans
2335f93579 Staging CDS enclave change. 2020-10-20 19:20:01 -03:00
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
690 changed files with 32987 additions and 10564 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

4
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.classpath
captures/
project.properties
keystore.debug.properties
keystore.staging.properties
.project
.settings
bin/
@@ -23,5 +25,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

@@ -15,7 +15,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
}
@@ -80,8 +80,8 @@ protobuf {
}
}
def canonicalVersionCode = 707
def canonicalVersionName = "4.71.4"
def canonicalVersionCode = 735
def canonicalVersionName = "4.76.2"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@@ -90,22 +90,35 @@ def abiPostFix = ['universal' : 0,
'x86' : 3,
'x86_64' : 4]
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
android {
flavorDimensions "none"
compileSdkVersion 28
buildToolsVersion '28.0.3'
flavorDimensions 'distribution', 'environment'
compileSdkVersion 29
buildToolsVersion '29.0.3'
useLibrary 'org.apache.http.legacy'
dexOptions {
javaMaxHeapSize "4g"
}
signingConfigs {
if (keystores.debug != null) {
debug {
storeFile file("${project.rootDir}/${keystores.debug.storeFile}")
storePassword keystores.debug.storePassword
keyAlias keystores.debug.keyAlias
keyPassword keystores.debug.keyPassword
}
}
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion 19
targetSdkVersion 28
targetSdkVersion 29
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -123,9 +136,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,11 +179,15 @@ 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 {
debug {
if (keystores['debug'] != null) {
signingConfig signingConfigs.debug
}
isDefault true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard/proguard-firebase-messaging.pro',
@@ -193,23 +211,9 @@ android {
testProguardFiles 'proguard/proguard-automation.pro',
'proguard/proguard.cfg'
}
staging {
initWith debug
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_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 "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
}
flipper {
initWith debug
isDefault false
minifyEnabled false
}
release {
@@ -220,18 +224,52 @@ android {
productFlavors {
play {
dimension "none"
dimension 'distribution'
isDefault true
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
}
website {
dimension "none"
dimension 'distribution'
ext.websiteUpdateUrl = "https://updates.signal.org/android"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
}
internal {
dimension 'distribution'
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
}
prod {
dimension 'environment'
isDefault true
}
staging {
dimension 'environment'
applicationIdSuffix ".staging"
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
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=\""
}
}
android.applicationVariants.all { variant ->
@@ -301,6 +339,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 +349,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.5.1'
implementation 'org.signal:ringrtc-android:2.7.3'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
@@ -371,17 +410,18 @@ 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'
}
dependencyVerification {
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
}
@@ -428,7 +468,13 @@ def signProductionRelease = { variant ->
task signProductionPlayRelease {
doLast {
signProductionRelease(android.applicationVariants.find { (it.name == 'playRelease') })
signProductionRelease(android.applicationVariants.find { (it.name == 'playProdRelease') })
}
}
task signProductionInternalRelease {
doLast {
signProductionRelease(android.applicationVariants.find { (it.name == 'internalProdRelease') })
}
}
@@ -440,16 +486,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 {
@@ -471,3 +507,14 @@ tasks.withType(Test) {
showStackTraces true
}
}
def loadKeystoreProperties(filename) {
def keystorePropertiesFile = file("${project.rootDir}/${filename}")
if (keystorePropertiesFile.exists()) {
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
return keystoreProperties;
} else {
return null;
}
}

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

@@ -1,4 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="app_name">Signal (Flipper)</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/core_red_shade"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

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,20 @@
<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.BlobContentProvider"
android:authorities="${applicationId}.blob"
android:exported="false"
android:grantUriPermissions="true" />
<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 +693,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,7 @@
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 +9,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, FeatureFlags.groupsV1AutoMigration());
}
}

View File

@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
@@ -137,7 +138,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 +155,8 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
Log.i(TAG, "App is now visible.");
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(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,26 +76,16 @@ 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));
Recipient.live(recipientId).observe(this, recipient -> {
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isSelf() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();
Resources resources = this.getResources();
@@ -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,12 @@ 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);
void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);

View File

@@ -21,6 +21,7 @@ import android.Manifest;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -29,7 +30,6 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.CycleInterpolator;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.TextView;
@@ -56,6 +56,7 @@ import com.google.android.material.chip.ChipGroup;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.components.emoji.WarningTextView;
import org.thoughtcrime.securesms.contacts.ContactChip;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
@@ -63,6 +64,8 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
@@ -82,7 +85,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
@@ -103,11 +105,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
public static final int NO_LIMIT = Integer.MAX_VALUE;
public static final String DISPLAY_MODE = "display_mode";
public static final String MULTI_SELECT = "multi_select";
public static final String REFRESHABLE = "refreshable";
public static final String RECENTS = "recents";
public static final String SELECTION_LIMIT = "selection_limit";
public static final String SELECTION_LIMITS = "selection_limits";
public static final String CURRENT_SELECTION = "current_selection";
public static final String HIDE_COUNT = "hide_count";
private ConstraintLayout constraintLayout;
private TextView emptyText;
@@ -123,15 +125,17 @@ public final class ContactSelectionListFragment extends LoggingFragment
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private TextView groupLimit;
private WarningTextView groupLimit;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback;
private GlideRequests glideRequests;
private int selectionLimit;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean hideCount;
@Override
public void onAttach(@NonNull Context context) {
@@ -206,9 +210,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
});
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
Intent intent = requireActivity().getIntent();
swipeRefresh.setEnabled(intent.getBooleanExtra(REFRESHABLE, true));
hideCount = intent.getBooleanExtra(HIDE_COUNT, false);
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
isMulti = selectionLimit != null;
if (!isMulti) {
selectionLimit = SelectionLimits.NO_LIMITS;
}
selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
currentSelection = getCurrentSelection();
updateGroupLimit(getChipCount());
@@ -217,12 +230,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
private void updateGroupLimit(int chipCount) {
if (selectionLimit != NO_LIMIT) {
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit));
groupLimit.setVisibility(View.VISIBLE);
} else {
groupLimit.setVisibility(View.GONE);
}
int members = currentSelection.size() + chipCount;
groupLimit.setText(getResources().getQuantityString(R.plurals.ContactSelectionListFragment_d_members, members, members));
groupLimit.setVisibility(isMulti && !hideCount ? View.VISIBLE : View.GONE);
groupLimit.setWarning(selectionWarningLimitExceeded());
}
@Override
@@ -254,7 +265,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
public boolean isMulti() {
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
return isMulti;
}
private void initializeCursor() {
@@ -264,7 +275,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
glideRequests,
null,
new ListClickListener(),
isMulti(),
isMulti,
currentSelection);
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
@@ -450,15 +461,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
if (isMulti() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
if (isMulti && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return;
}
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (selectionLimitReached()) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
groupLimit.animate().scaleX(1.3f).scaleY(1.3f).setInterpolator(new CycleInterpolator(0.5f)).start();
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (selectionHardLimitReached()) {
GroupLimitDialog.showHardLimitMessage(requireContext());
return;
}
@@ -486,7 +496,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
new AlertDialog.Builder(requireContext())
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
.setPositiveButton(R.string.ContactSelectionListFragment_okay, (dialog, which) -> dialog.dismiss())
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
}
});
@@ -508,16 +518,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
}
}}
}
}
}
private boolean selectionLimitReached() {
return getChipCount() + currentSelection.size() >= selectionLimit;
private boolean selectionHardLimitReached() {
return getChipCount() + currentSelection.size() >= selectionLimit.getHardLimit();
}
private boolean selectionWarningLimitReachedExactly() {
return getChipCount() + currentSelection.size() == selectionLimit.getRecommendedLimit();
}
private boolean selectionWarningLimitExceeded() {
return getChipCount() + currentSelection.size() > selectionLimit.getRecommendedLimit();
}
private void markContactSelected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
if (isMulti()) {
if (isMulti) {
addChipForSelectedContact(selectedContact);
}
}
@@ -588,6 +607,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip);
updateGroupLimit(getChipCount());
if (selectionWarningLimitReachedExactly()) {
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
}
}
private int getChipCount() {

View File

@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChange
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
@@ -62,7 +63,8 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_SMS);
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS);
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
setContentView(R.layout.invite_activity);

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

@@ -18,12 +18,14 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
@@ -31,14 +33,13 @@ 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;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ShareCompat;
import androidx.core.util.Pair;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
@@ -63,14 +64,17 @@ import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
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 +123,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 +138,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 +152,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);
@@ -196,7 +198,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (threadRecipient != null) {
if (mediaItem.outgoing || threadRecipient.isGroup()) {
if (threadRecipient.isLocalNumber()) {
if (threadRecipient.isSelf()) {
from = getString(R.string.note_to_self);
} else {
to = threadRecipient.getDisplayName(this);
@@ -261,6 +263,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 +276,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() {
@@ -379,6 +382,27 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
}
}
private void share() {
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem != null) {
Uri publicUri = PartAuthority.getAttachmentPublicUri(mediaItem.uri);
String mimeType = Intent.normalizeMimeType(mediaItem.type);
Intent shareIntent = ShareCompat.IntentBuilder.from(this)
.setStream(publicUri)
.setType(mimeType)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
startActivity(shareIntent);
} catch (ActivityNotFoundException e) {
Log.w(TAG, "No activity existed to share the media.", e);
Toast.makeText(this, R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show();
}
}
}
@SuppressWarnings("CodeBlock2Expr")
@SuppressLint("InlinedApi")
private void saveToDisk() {
@@ -386,21 +410,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();
@@ -447,6 +480,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
menu.findItem(R.id.delete).setVisible(false);
}
// Restricted to API26 because of MemoryFileUtil not supporting lower API levels well
menu.findItem(R.id.media_preview__share).setVisible(Build.VERSION.SDK_INT >= 26);
if (cameFromAllMedia) {
menu.findItem(R.id.media_preview__overview).setVisible(false);
}
@@ -456,16 +492,17 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.media_preview__overview: showOverview(); return true;
case R.id.media_preview__forward: forward(); return true;
case R.id.save: saveToDisk(); return true;
case R.id.delete: deleteMedia(); return true;
case android.R.id.home: finish(); return true;
}
int itemId = item.getItemId();
if (itemId == R.id.media_preview__overview) { showOverview(); return true; }
if (itemId == R.id.media_preview__forward) { forward(); return true; }
if (itemId == R.id.media_preview__share) { share(); return true; }
if (itemId == R.id.save) { saveToDisk(); return true; }
if (itemId == R.id.delete) { deleteMedia(); return true; }
if (itemId == android.R.id.home) { finish(); return true; }
return false;
}
@@ -546,7 +583,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
@Override
public boolean singleTapOnMedia() {
toggleUiVisibility();
fullscreenHelper.toggleUiVisibility();
return true;
}
@@ -556,32 +593,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 +708,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 +785,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

@@ -42,7 +42,6 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
@Override
protected void onCreate(Bundle icicle, boolean ready) {
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
super.onCreate(icicle, ready);
initializeToolbar();

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);
@@ -466,7 +451,8 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
public void onSendAnywayAfterSafetyNumberChange() {
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_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.AUDIO_CALL.getCode());
startService(intent);
}
@@ -493,32 +479,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 +513,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 +595,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,10 @@ 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.os.Build;
import android.provider.DocumentsContract;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
@@ -13,10 +17,14 @@ 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;
@@ -26,25 +34,49 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
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 +107,42 @@ 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);
if (Build.VERSION.SDK_INT >= 26) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SignalStore.settings().getLatestSignalBackupDirectory());
}
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

@@ -0,0 +1,77 @@
package org.thoughtcrime.securesms.backup;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import java.io.IOException;
public enum BackupFileIOError {
ACCESS_ERROR(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_directory_has_been_deleted_or_moved),
FILE_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_file_is_too_large),
NOT_ENOUGH_SPACE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_there_is_not_enough_space),
UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups);
private static final short BACKUP_FAILED_ID = 31321;
private final @StringRes int titleId;
private final @StringRes int messageId;
BackupFileIOError(@StringRes int titleId, @StringRes int messageId) {
this.titleId = titleId;
this.messageId = messageId;
}
public static void clearNotification(@NonNull Context context) {
NotificationManagerCompat.from(context).cancel(BACKUP_FAILED_ID);
}
public void postNotification(@NonNull Context context) {
Intent intent = new Intent(context, ApplicationPreferencesActivity.class);
intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_BACKUPS_FRAGMENT, true);
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, intent, 0);
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
.setSmallIcon(R.drawable.ic_signal_backup)
.setContentTitle(context.getString(titleId))
.setContentText(context.getString(messageId))
.setContentIntent(pendingIntent)
.build();
NotificationManagerCompat.from(context)
.notify(BACKUP_FAILED_ID, backupFailedNotification);
}
public static void postNotificationForException(@NonNull Context context, @NonNull IOException e, int runAttempt) {
BackupFileIOError error = getFromException(e);
if (error != null) {
error.postNotification(context);
}
if (error == null && runAttempt > 0) {
UNKNOWN.postNotification(context);
}
}
private static @Nullable BackupFileIOError getFromException(@NonNull IOException e) {
if (e.getMessage() != null) {
if (e.getMessage().contains("EFBIG")) return FILE_TOO_LARGE;
else if (e.getMessage().contains("ENOSPC")) return NOT_ENOUGH_SPACE;
}
return null;
}
}

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,7 @@ import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Observer;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
@@ -28,19 +30,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();
@@ -60,13 +60,17 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
@ColorInt private final int waveFormPlayedBarsColor;
@ColorInt private final int waveFormUnplayedBarsColor;
@ColorInt private final int waveFormThumbTint;
@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);
@@ -103,6 +107,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
this.waveFormThumbTint = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE);
progressAndPlay.getBackground().setColorFilter(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK), PorterDuff.Mode.SRC_IN);
} finally {
if (typedArray != null) {
typedArray.recycle();
@@ -122,11 +129,23 @@ 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 boolean showControls)
final @Nullable Callbacks callbacks,
final boolean showControls,
final boolean forceHideDuration)
{
this.callbacks = callbacks;
if (duration != null) {
duration.setVisibility(View.VISIBLE);
}
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);
@@ -139,30 +158,29 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
seekBar.setEnabled(false);
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
circleProgress.setVisibility(View.GONE);
} else if (showControls && audio.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
controlToggle.displayQuick(progressAndPlay);
seekBar.setEnabled(false);
circleProgress.setVisibility(View.VISIBLE);
circleProgress.spin();
} else {
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;
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor);
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor, waveFormThumbTint);
if (android.os.Build.VERSION.SDK_INT >= 23) {
new AudioWaveForm(getContext(), audio).getWaveForm(
data -> {
if (duration != null) {
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
updateProgress(0, 0);
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
updateProgress(0, 0);
if (!forceHideDuration && duration != null) {
duration.setVisibility(VISIBLE);
}
waveFormView.setWaveData(data.getWaveForm());
@@ -175,11 +193,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
}
}
}
}
public void cleanup() {
if (this.audioSlidePlayer != null && isPlaying) {
this.audioSlidePlayer.stop();
if (forceHideDuration && duration != null) {
duration.setVisibility(View.GONE);
}
}
@@ -187,23 +203,84 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
this.downloadListener = listener;
}
@Override
public void onStart() {
public @Nullable Uri getAudioSlideUri() {
if (audioSlide != null) return audioSlide.getUri();
else return null;
}
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
onDuration(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getTrackDuration());
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset());
onProgress(voiceNotePlaybackState.getUri(),
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
voiceNotePlaybackState.getPlayheadPositionMillis());
}
private void onDuration(@NonNull Uri uri, long durationMillis) {
if (isTarget(uri)) {
this.durationMillis = durationMillis;
}
}
private void onStart(@NonNull Uri uri, boolean autoReset) {
if (!isTarget(uri)) {
if (hasAudioUri()) {
onStop(audioSlide.getUri(), autoReset);
}
return;
}
if (isPlaying) {
return;
}
isPlaying = true;
togglePlayToPause();
}
@Override
public void onStop() {
private void onStop(@NonNull Uri uri, boolean autoReset) {
if (!isTarget(uri)) {
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();
}
}
private void onProgress(@NonNull Uri uri, double progress, long millis) {
if (!isTarget(uri)) {
return;
}
int seekProgress = (int) Math.floor(progress * seekBar.getMax());
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
backwardsCounter = 0;
seekBar.setProgress(seekProgress);
updateProgress((float) progress, millis);
} else {
backwardsCounter++;
}
}
private boolean isTarget(@NonNull Uri uri) {
return hasAudioUri() && Objects.equals(uri, audioSlide.getUri());
}
private boolean hasAudioUri() {
return audioSlide != null && audioSlide.getUri() != null;
}
@Override
public void setFocusable(boolean focusable) {
super.setFocusable(focusable);
@@ -230,20 +307,11 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
this.downloadButton.setEnabled(enabled);
}
@Override
public void onProgress(double progress, long millis) {
int seekProgress = (int) Math.floor(progress * seekBar.getMax());
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
backwardsCounter = 0;
seekBar.setProgress(seekProgress);
updateProgress((float) progress, millis);
} else {
backwardsCounter++;
}
}
private void updateProgress(float progress, long millis) {
if (callbacks != null) {
callbacks.onProgressUpdated(durationMillis, millis);
}
if (duration != null && durationMillis > 0) {
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis);
duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
@@ -306,41 +374,31 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
if (!smallView || seekBar.getProgress() == 0) {
circleProgress.setInstantProgress(1);
}
circleProgress.setVisibility(VISIBLE);
circleProgress.setVisibility(GONE);
playPauseButton.setVisibility(VISIBLE);
controlToggle.displayQuick(progressAndPlay);
}
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);
}
} else {
Log.d(TAG, "pausebutton onClick");
if (audioSlidePlayer != null) {
togglePauseToPlay();
audioSlidePlayer.stop();
if (autoRewind) {
rewind();
}
if (audioSlide == null || audioSlide.getUri() == null) return;
if (callbacks != null) {
if (lottieDirection == REVERSE) {
callbacks.onPlay(audioSlide.getUri(), getProgress());
} else {
callbacks.onPause(audioSlide.getUri());
}
}
}
@@ -370,28 +428,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 +463,16 @@ 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);
void onProgressUpdated(long durationMillis, long playheadMillis);
}
}

View File

@@ -117,7 +117,7 @@ public final class AvatarImageView extends AppCompatImageView {
* Shows self as the actual profile picture.
*/
public void setRecipient(@NonNull Recipient recipient) {
if (recipient.isLocalNumber()) {
if (recipient.isSelf()) {
setAvatar(GlideApp.with(this), null, false);
AvatarUtil.loadIconIntoImageView(recipient, this);
} else {

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

@@ -5,6 +5,7 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
@@ -15,18 +16,26 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
import org.thoughtcrime.securesms.ApplicationContext;
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.permissions.Permissions;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class ConversationItemFooter extends LinearLayout {
@@ -35,6 +44,10 @@ public class ConversationItemFooter extends LinearLayout {
private ExpirationTimerView timerView;
private ImageView insecureIndicatorView;
private DeliveryStatusView deliveryStatusView;
private boolean onlyShowSendingStatus;
private View audioSpace;
private TextView audioDuration;
private LottieAnimationView revealDot;
public ConversationItemFooter(Context context) {
super(context);
@@ -59,11 +72,15 @@ public class ConversationItemFooter extends LinearLayout {
timerView = findViewById(R.id.footer_expiration_timer);
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
deliveryStatusView = findViewById(R.id.footer_delivery_status);
audioDuration = findViewById(R.id.footer_audio_duration);
audioSpace = findViewById(R.id.footer_audio_duration_space);
revealDot = findViewById(R.id.footer_revealed_dot);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white)));
setIconColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_icon_color, getResources().getColor(R.color.core_white)));
setRevealDotColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_reveal_dot_color, getResources().getColor(R.color.core_white)));
typedArray.recycle();
}
}
@@ -80,11 +97,18 @@ public class ConversationItemFooter extends LinearLayout {
presentTimer(messageRecord);
presentInsecureIndicator(messageRecord);
presentDeliveryStatus(messageRecord);
hideAudioDurationViews();
}
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(totalDurationMillis - currentPostionMillis);
audioDuration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
}
public void setTextColor(int color) {
dateView.setTextColor(color);
simView.setTextColor(color);
audioDuration.setTextColor(color);
}
public void setIconColor(int color) {
@@ -93,6 +117,19 @@ public class ConversationItemFooter extends LinearLayout {
deliveryStatusView.setTint(color);
}
public void setRevealDotColor(int color) {
revealDot.addValueCallback(
new KeyPath("**"),
LottieProperty.COLOR_FILTER,
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
);
}
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 +210,89 @@ 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();
}
}
}
private void presentAudioDuration(@NonNull MessageRecord messageRecord) {
if (messageRecord.isMms()) {
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
if (mmsMessageRecord.getSlideDeck().getAudioSlide() != null) {
if (messageRecord.isOutgoing()) {
moveAudioViewsForOutgoing();
} else {
moveAudioViewsForIncoming();
}
showAudioDurationViews();
} else {
hideAudioDurationViews();
}
} else {
hideAudioDurationViews();
}
}
private void moveAudioViewsForOutgoing() {
removeView(audioSpace);
removeView(audioDuration);
removeView(revealDot);
addView(audioSpace, 0);
addView(revealDot, 0);
addView(audioDuration, 0);
int padStart = ViewUtil.dpToPx(60);
int padLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR ? padStart : 0;
int padRight = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? padStart : 0;
audioDuration.setPadding(padLeft, 0, padRight, 0);
}
private void moveAudioViewsForIncoming() {
removeView(audioSpace);
removeView(audioDuration);
removeView(revealDot);
addView(audioSpace);
addView(revealDot);
addView(audioDuration);
audioDuration.setPadding(0, 0, 0, 0);
}
private void showAudioDurationViews() {
audioSpace.setVisibility(View.VISIBLE);
audioDuration.setVisibility(View.VISIBLE);
if (FeatureFlags.viewedReceipts()) {
revealDot.setVisibility(View.VISIBLE);
}
}
private void hideAudioDurationViews() {
audioSpace.setVisibility(View.GONE);
audioDuration.setVisibility(View.GONE);
revealDot.setVisibility(View.GONE);
}
}

View File

@@ -4,22 +4,16 @@ import android.content.Context;
import android.graphics.Typeface;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.util.AttributeSet;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ResUtil;
import org.thoughtcrime.securesms.util.spans.CenterAlignedRelativeSizeSpan;
public class FromTextView extends EmojiTextView {
@@ -59,7 +53,7 @@ public class FromTextView extends EmojiTextView {
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
if (recipient.isLocalNumber()) {
if (recipient.isSelf()) {
builder.append(getContext().getString(R.string.note_to_self));
} else {
builder.append(fromSpan);

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

@@ -23,6 +23,7 @@ import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.preference.PreferenceManager;
import androidx.appcompat.widget.LinearLayoutCompat;
import android.util.AttributeSet;
import org.thoughtcrime.securesms.logging.Log;
import android.view.Surface;
@@ -31,6 +32,7 @@ import android.view.View;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.lang.reflect.Field;
import java.util.HashSet;
@@ -69,17 +71,17 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
public KeyboardAwareLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final int statusBarRes = getResources().getIdentifier("status_bar_height", "dimen", "android");
minKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
minCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
defaultCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
minCustomKeyboardTopMarginPortrait = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
minCustomKeyboardTopMarginLandscape = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
statusBarHeight = statusBarRes > 0 ? getResources().getDimensionPixelSize(statusBarRes) : 0;
statusBarHeight = ViewUtil.getStatusBarHeight(this);
viewInset = getViewInset();
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateRotation();
updateKeyboardState();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -100,7 +102,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
getWindowVisibleDisplayFrame(rect);
final int availableHeight = getAvailableHeight();
final int keyboardHeight = availableHeight - (rect.bottom - rect.top);
final int keyboardHeight = availableHeight - rect.bottom;
if (keyboardHeight > minKeyboardSize) {
if (getKeyboardHeight() != keyboardHeight) {
@@ -128,19 +130,19 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
Field stableInsetsField = attachInfo.getClass().getDeclaredField("mStableInsets");
stableInsetsField.setAccessible(true);
Rect insets = (Rect)stableInsetsField.get(attachInfo);
return insets.bottom;
if (insets != null) {
return insets.bottom;
}
}
} catch (NoSuchFieldException nsfe) {
Log.w(TAG, "field reflection error when measuring view inset", nsfe);
} catch (IllegalAccessException iae) {
Log.w(TAG, "access reflection error when measuring view inset", iae);
} catch (NoSuchFieldException | IllegalAccessException e) {
// Do nothing
}
return 0;
return statusBarHeight;
}
private int getAvailableHeight() {
final int availableHeight = this.getRootView().getHeight() - viewInset - (!isFullscreen ? statusBarHeight : 0);
final int availableWidth = this.getRootView().getWidth() - (!isFullscreen ? statusBarHeight : 0);
final int availableHeight = this.getRootView().getHeight() - viewInset;
final int availableWidth = this.getRootView().getWidth();
if (isLandscape() && availableHeight > availableWidth) {
//noinspection SuspiciousNameCombination

View File

@@ -190,8 +190,8 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
private void setQuoteAuthor(@NonNull Recipient author) {
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
authorView.setText(author.isLocalNumber() ? getContext().getString(R.string.QuoteView_you)
: author.getDisplayName(getContext()));
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
: author.getDisplayName(getContext()));
// We use the raw color resource because Android 4.x was struggling with tints here
quoteBarView.setImageResource(author.getColor().toQuoteBarColorResource(getContext(), outgoing));
@@ -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

@@ -42,7 +42,7 @@ public class TypingStatusRepository {
}
public synchronized void onTypingStarted(@NonNull Context context, long threadId, @NonNull Recipient author, int device) {
if (author.isLocalNumber()) {
if (author.isSelf()) {
return;
}
@@ -66,7 +66,7 @@ public class TypingStatusRepository {
}
public synchronized void onTypingStopped(@NonNull Context context, long threadId, @NonNull Recipient author, int device, boolean isReplacedByIncomingMessage) {
if (author.isLocalNumber()) {
if (author.isSelf()) {
return;
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.animation.Interpolator;
@@ -13,6 +14,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.appcompat.widget.AppCompatSeekBar;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.R;
@@ -65,9 +67,12 @@ public final class WaveFormSeekBarView extends AppCompatSeekBar {
barWidth = getResources().getDimensionPixelSize(R.dimen.wave_form_bar_width);
}
public void setColors(@ColorInt int playedBarColor, @ColorInt int unplayedBarColor) {
public void setColors(@ColorInt int playedBarColor, @ColorInt int unplayedBarColor, @ColorInt int thumbTint) {
this.playedBarColor = playedBarColor;
this.unplayedBarColor = unplayedBarColor;
getThumb().setColorFilter(thumbTint, PorterDuff.Mode.SRC_IN);
invalidate();
}

View File

@@ -0,0 +1,49 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import androidx.annotation.ColorInt;
import androidx.appcompat.widget.AppCompatTextView;
import org.thoughtcrime.securesms.R;
public final class WarningTextView extends AppCompatTextView {
@ColorInt private final int originalTextColor;
@ColorInt private final int warningTextColor;
private boolean warning;
public WarningTextView(Context context) {
this(context, null);
}
public WarningTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WarningTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.WarningTextView, 0, 0);
warningTextColor = styledAttributes.getColor(R.styleable.WarningTextView_warning_text_color, 0);
styledAttributes.recycle();
styledAttributes = context.obtainStyledAttributes(attrs, new int[]{ android.R.attr.textColor });
originalTextColor = styledAttributes.getColor(0, 0);
styledAttributes.recycle();
}
public void setWarning(boolean warning) {
if (this.warning != warning) {
this.warning = warning;
setTextColor(warning ? warningTextColor : originalTextColor);
invalidate();
}
}
}

View File

@@ -1,37 +1,34 @@
package org.thoughtcrime.securesms.components.reminder;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.os.Build.VERSION_CODES;
import android.provider.Telephony;
import android.view.View;
import android.view.View.OnClickListener;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.SmsUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
public class DefaultSmsReminder extends Reminder {
@TargetApi(VERSION_CODES.KITKAT)
public DefaultSmsReminder(final Context context) {
super(context.getString(R.string.reminder_header_sms_default_title),
context.getString(R.string.reminder_header_sms_default_text));
public DefaultSmsReminder(@NonNull Fragment fragment, short requestCode) {
super(fragment.getString(R.string.reminder_header_sms_default_title),
fragment.getString(R.string.reminder_header_sms_default_text));
final OnClickListener okListener = new OnClickListener() {
@Override
public void onClick(View v) {
TextSecurePreferences.setPromptedDefaultSmsProvider(context, true);
Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT);
intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, context.getPackageName());
context.startActivity(intent);
TextSecurePreferences.setPromptedDefaultSmsProvider(fragment.requireContext(), true);
fragment.startActivityForResult(SmsUtil.getSmsRoleIntent(fragment.requireContext()), requestCode);
}
};
final OnClickListener dismissListener = new OnClickListener() {
@Override
public void onClick(View v) {
TextSecurePreferences.setPromptedDefaultSmsProvider(context, true);
TextSecurePreferences.setPromptedDefaultSmsProvider(fragment.requireContext(), true);
}
};
setOkListener(okListener);

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,265 @@
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 previousState = voiceNotePlaybackState.getValue();
long position = mediaController.getPlaybackState().getPosition();
long duration = mediaMetadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
if (previousState != null && Objects.equals(mediaUri, previousState.getUri())) {
if (position < 0 && previousState.getPlayheadPositionMillis() >= 0) {
position = previousState.getPlayheadPositionMillis();
}
if (duration <= 0 && previousState.getTrackDuration() > 0) {
duration = previousState.getTrackDuration();
}
}
if (duration > 0 && position >= 0 && position <= duration) {
voiceNotePlaybackState.postValue(new VoiceNotePlaybackState(mediaUri, position, 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) {
Log.d(TAG, "onPrepareFromUri: " + uri);
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);
if (!queueDataAdapter.isEmpty()) {
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) {
if (dataSource.getSize() > 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)) {
queueDataAdapter.remove(lastIndex);
dataSource.removeMediaSource(lastIndex);
if (queueDataAdapter.size() > 1) {
MediaDescriptionCompat end = createEndClone(last);
queueDataAdapter.add(lastIndex, end);
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> recordsAfter = DatabaseFactory.getMmsSmsDatabase(context).getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
return Stream.of(buildFilteredMessageRecordList(recordsAfter))
.map(record -> VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context, record))
.toList();
} catch (NoSuchMessageException e) {
Log.w(TAG, "Could not find message.", e);
return Collections.emptyList();
}
}
private static @NonNull List<MessageRecord> buildFilteredMessageRecordList(@NonNull List<MessageRecord> recordsAfter) {
return Stream.of(recordsAfter)
.takeWhile(MessageRecordUtil::hasAudio)
.toList();
}
}

View File

@@ -0,0 +1,250 @@
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.MediaDescriptionCompat;
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, queueDataAdapter);
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();
if (currentWindowIndex == C.INDEX_UNSET) {
return;
}
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(currentWindowIndex);
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + mediaDescriptionCompat.getMediaUri());
}
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,142 @@
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 android.support.v4.media.MediaDescriptionCompat;
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.dependencies.ApplicationDependencies;
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 final VoiceNoteQueueDataAdapter queueDataAdapter;
private long startTime;
VoiceNoteProximityManager(@NonNull Context context,
@NonNull SimpleExoPlayer player,
@NonNull VoiceNoteQueueDataAdapter queueDataAdapter)
{
this.player = player;
this.audioManager = ServiceUtil.getAudioManager(context);
this.sensorManager = ServiceUtil.getSensorManager(context);
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
this.queueDataAdapter = queueDataAdapter;
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);
final long threadId;
final int windowIndex = player.getCurrentWindowIndex();
if (queueDataAdapter.isEmpty() || windowIndex == C.INDEX_UNSET) {
threadId = -1;
} else {
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(windowIndex);
threadId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID, -1);
}
if (desiredStreamType == AudioManager.STREAM_VOICE_CALL &&
desiredStreamType != currentStreamType &&
!audioManager.isWiredHeadsetOn() &&
threadId != -1 &&
ApplicationDependencies.getMessageNotifier().getVisibleThread() == threadId)
{
if (wakeLock != null && !wakeLock.isHeld()) {
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) {
if (wakeLock.isHeld()) {
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_INCOMING && 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) {
@@ -50,6 +58,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
GestureDetectorCompat gestureDetector = new GestureDetectorCompat(child.getContext(), helper);
parent.setOnInterceptTouchEventListener((event) -> {
final int action = event.getAction();
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
if (pointerIndex > 0) {
return false;
}
if (helper.velocityTracker == null) {
helper.velocityTracker = VelocityTracker.obtain();
}
@@ -95,6 +110,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 +122,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,19 +136,59 @@ 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();
lastTouchX = e.getX(0) + child.getX();
lastTouchY = e.getY(0) + child.getY();
isDragging = true;
pipWidth = child.getMeasuredWidth();
pipHeight = child.getMeasuredHeight();
interpolator = FLING_INTERPOLATOR;
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
int pointerIndex = e2.findPointerIndex(activePointerId);
int pointerIndex = e2.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
fling();
return false;
}
float x = e2.getX(pointerIndex) + child.getX();
float y = e2.getY(pointerIndex) + child.getY();
float dx = x - lastTouchX;
@@ -167,6 +220,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 +238,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,84 @@ 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.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 +101,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 +119,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 +197,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 +237,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 +339,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 +375,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 +419,7 @@ public class WebRtcCallView extends FrameLayout {
if (webRtcControls.displayEndCall()) {
visibleViewSet.add(hangup);
visibleViewSet.add(ongoingFooterGradient);
visibleViewSet.add(footerGradient);
}
if (webRtcControls.displayMuteAudio()) {
@@ -358,6 +436,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 +454,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 +462,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 +509,50 @@ 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.endTransitions(parent);
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();
@@ -412,11 +563,14 @@ public class WebRtcCallView extends FrameLayout {
}
constraintSet.applyTo(parent);
layoutParticipants();
}
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) {
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.endTransitions(parent);
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();
@@ -458,40 +612,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 +628,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 +645,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

@@ -13,6 +13,7 @@ import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
@@ -64,6 +65,10 @@ public class ContactRepository {
String phone = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.EMAIL));
if (phone != null) {
phone = PhoneNumberFormatter.prettyPrint(phone);
}
return Util.getFirstNonEmpty(phone, email);
}));

View File

@@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.contacts.avatars;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
/**
* Fallback resource based contact photo with a 20dp icon
*/
public final class FallbackPhoto20dp implements FallbackContactPhoto {
@DrawableRes private final int drawable20dp;
public FallbackPhoto20dp(@DrawableRes int drawable20dp) {
this.drawable20dp = drawable20dp;
}
@Override
public Drawable asDrawable(Context context, int color) {
return buildDrawable(context, color);
}
@Override
public Drawable asDrawable(Context context, int color, boolean inverted) {
return buildDrawable(context, color);
}
@Override
public Drawable asSmallDrawable(Context context, int color, boolean inverted) {
return buildDrawable(context, color);
}
@Override
public Drawable asCallCard(Context context) {
throw new UnsupportedOperationException();
}
private @NonNull Drawable buildDrawable(@NonNull Context context, int color) {
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate();
Drawable foreground = AppCompatResources.getDrawable(context, drawable20dp);
Drawable gradient = ThemeUtil.getThemedDrawable(context, R.attr.resource_placeholder_gradient);
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
int foregroundInset = ViewUtil.dpToPx(2);
DrawableCompat.setTint(background, color);
drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset);
return drawable;
}
}

View File

@@ -58,7 +58,7 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
return new LayerDrawable(new Drawable[] { base, gradient });
}
return new ResourceContactPhoto(fallbackResId).asDrawable(context, color, inverted);
return newFallbackDrawable(context, color, inverted);
}
@Override
@@ -66,6 +66,14 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
return asDrawable(context, color, inverted);
}
protected @DrawableRes int getFallbackResId() {
return fallbackResId;
}
protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) {
return new ResourceContactPhoto(fallbackResId).asDrawable(context, color, inverted);
}
private @Nullable String getAbbreviation(String name) {
String[] parts = name.split(" ");
StringBuilder builder = new StringBuilder();

View File

@@ -7,13 +7,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
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.whispersystems.libsignal.util.ByteUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
@@ -67,7 +64,7 @@ public class ProfileContactPhoto implements ContactPhoto {
}
private long getFileLastModified() {
if (!recipient.isLocalNumber()) {
if (!recipient.isSelf()) {
return 0;
}

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

@@ -26,15 +26,23 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
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);
private static final int MAX_NUMBERS = 20_500;
@WorkerThread
static DirectoryResult getDirectoryResult(@NonNull Context context,
@NonNull Set<String> databaseNumbers,
@@ -44,7 +52,14 @@ class ContactDiscoveryV2 {
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
Set<String> sanitizedNumbers = sanitizeNumbers(inputResult.getNumbers());
Set<String> ignoredNumbers = new HashSet<>();
if (sanitizedNumbers.size() > MAX_NUMBERS) {
Set<String> randomlySelected = randomlySelect(sanitizedNumbers, MAX_NUMBERS);
ignoredNumbers = SetUtil.difference(sanitizedNumbers, randomlySelected);
sanitizedNumbers = randomlySelected;
}
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
KeyStore iasKeyStore = getIasKeyStore(context);
@@ -53,7 +68,7 @@ class ContactDiscoveryV2 {
Map<String, UUID> results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE);
FuzzyPhoneNumberHelper.OutputResultV2 outputResult = FuzzyPhoneNumberHelper.generateOutputV2(results, inputResult);
return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites());
return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers);
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException e) {
Log.w(TAG, "Attestation error.", e);
throw new IOException(e);
@@ -74,6 +89,13 @@ class ContactDiscoveryV2 {
}).collect(Collectors.toSet());
}
private static @NonNull Set<String> randomlySelect(@NonNull Set<String> numbers, int max) {
List<String> list = new ArrayList<>(numbers);
Collections.shuffle(list);
return new HashSet<>(list.subList(0, max));
}
private static KeyStore getIasKeyStore(@NonNull Context context) {
try {
TrustStore contactTrustStore = new IasTrustStore(context);

View File

@@ -25,35 +25,36 @@ 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.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;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.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;
@@ -82,7 +83,7 @@ public class DirectoryHelper {
return;
}
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
if (!Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
Log.w(TAG, "No contact permissions. Skipping.");
return;
}
@@ -154,13 +155,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 +174,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 +220,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");
@@ -239,11 +235,23 @@ public class DirectoryHelper {
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
.filterNot(activeNumbers::contains)
.filterNot(n -> result.getNumberRewrites().containsKey(n))
.filterNot(n -> result.getIgnoredNumbers().contains(n))
.map(recipientDatabase::getOrInsertFromE164)
.collect(Collectors.toSet());
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 +283,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;
}
}
@@ -394,7 +396,7 @@ public class DirectoryHelper {
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isLocalNumber()) {
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isSelf()) {
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
@@ -420,15 +422,62 @@ 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;
private final Set<String> ignoredNumbers;
DirectoryResult(@NonNull Map<String, UUID> registeredNumbers,
@NonNull Map<String, String> numberRewrites)
@NonNull Map<String, String> numberRewrites,
@NonNull Set<String> ignoredNumbers)
{
this.registeredNumbers = registeredNumbers;
this.numberRewrites = numberRewrites;
this.ignoredNumbers = ignoredNumbers;
}
@@ -439,6 +488,28 @@ public class DirectoryHelper {
@NonNull Map<String, String> getNumberRewrites() {
return numberRewrites;
}
@NonNull Set<String> getIgnoredNumbers() {
return ignoredNumbers;
}
}
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 {

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

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