Compare commits

...

213 Commits

Author SHA1 Message Date
Alan Evans
0068d62122 Bump version to 4.76.3 2020-11-09 14:39:30 -04:00
Alan Evans
3f1fa59e09 Updated language translations. 2020-11-09 14:38:11 -04:00
Greyson Parrelli
df5114c62c Fix website signing task. 2020-11-09 14:21:56 -04:00
Greyson Parrelli
956e3924ff Log the version in our PersistentLogger. 2020-11-09 12:47:10 -05:00
Greyson Parrelli
20ad166e0f Fix issue where we were double-logging job info. 2020-11-09 12:18:45 -05:00
Greyson Parrelli
12ea88f409 Improve logging around deletions. 2020-11-09 12:18:27 -05:00
Alan Evans
0c5648bfb1 Hide "Read More" when long message is remote deleted. 2020-11-09 10:24:11 -04:00
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
521 changed files with 27477 additions and 8357 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

3
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.classpath
captures/
project.properties
keystore.debug.properties
keystore.staging.properties
.project
.settings
bin/
@@ -23,6 +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 = 711
def canonicalVersionName = "4.72.2"
def canonicalVersionCode = 736
def canonicalVersionName = "4.76.3"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@@ -90,10 +90,12 @@ 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 {
@@ -101,11 +103,13 @@ android {
}
signingConfigs {
staging {
storeFile file("${project.rootDir}/dev.keystore")
storePassword 'android'
keyAlias 'staging'
keyPassword 'android'
if (keystores.debug != null) {
debug {
storeFile file("${project.rootDir}/${keystores.debug.storeFile}")
storePassword keystores.debug.storePassword
keyAlias keystores.debug.keyAlias
keyPassword keystores.debug.keyPassword
}
}
}
@@ -114,7 +118,7 @@ android {
versionName canonicalVersionName
minSdkVersion 19
targetSdkVersion 28
targetSdkVersion 29
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -132,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('", "') + '"}'
@@ -179,6 +184,10 @@ android {
buildTypes {
debug {
if (keystores['debug'] != null) {
signingConfig signingConfigs.debug
}
isDefault true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard/proguard-firebase-messaging.pro',
@@ -202,25 +211,9 @@ android {
testProguardFiles 'proguard/proguard-automation.pro',
'proguard/proguard.cfg'
}
staging {
initWith debug
applicationIdSuffix ".staging"
signingConfig signingConfigs.staging
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
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 {
@@ -231,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 ->
@@ -312,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'
@@ -321,7 +349,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.7.0'
implementation 'org.signal:ringrtc-android:2.7.3'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
@@ -382,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'
}
@@ -439,28 +468,24 @@ 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') })
}
}
task signProductionWebsiteRelease {
doLast {
def variant = android.applicationVariants.find { (it.name == 'websiteRelease') }
def variant = android.applicationVariants.find { (it.name == 'websiteProdRelease') }
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
assembleWebsiteDescriptor(variant, signedRelease)
}
}
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 {
@@ -482,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

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

@@ -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" />
@@ -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" />
@@ -533,6 +542,18 @@
<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" >
@@ -652,6 +673,11 @@
android:exported="false"
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"

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.account.AccountAttributes;
public final class AppCapabilities {
@@ -15,6 +16,6 @@ public final class AppCapabilities {
* asking if the user has set a Signal PIN or not.
*/
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable);
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;
@@ -155,6 +156,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
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

@@ -82,10 +82,10 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
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();

View File

@@ -1,11 +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;
@@ -51,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;
@@ -37,6 +39,7 @@ 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;
@@ -61,6 +64,7 @@ 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;
@@ -70,6 +74,7 @@ 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;
@@ -193,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);
@@ -258,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);
@@ -376,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() {
@@ -383,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();
@@ -444,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);
}
@@ -453,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;
}

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

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

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

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());
@@ -221,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

@@ -100,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;
@@ -192,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() {
@@ -261,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

@@ -32,7 +32,7 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
}
@Override
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
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);

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

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

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

@@ -198,7 +198,7 @@ public final class CallParticipantsState {
} else {
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
}
} else if (callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
localRenderState = WebRtcLocalRenderState.LARGE;
}
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {

View File

@@ -58,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();
}
@@ -163,8 +170,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
@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();
@@ -175,7 +182,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
@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;

View File

@@ -22,7 +22,6 @@ import androidx.constraintlayout.widget.Guideline;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.AutoTransition;
import androidx.transition.ChangeBounds;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
import androidx.transition.TransitionSet;
@@ -455,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()));
}
}
@@ -510,7 +509,7 @@ public class WebRtcCallView extends FrameLayout {
private void fadeInControls() {
fadeControls(ConstraintSet.VISIBLE);
pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), videoToggle.getTop());
pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop());
scheduleFadeOut();
}
@@ -553,6 +552,7 @@ public class WebRtcCallView extends FrameLayout {
Transition transition = new AutoTransition().setOrdering(TransitionSet.ORDERING_TOGETHER)
.setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.endTransitions(parent);
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();
@@ -570,6 +570,7 @@ public class WebRtcCallView extends FrameLayout {
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();

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

@@ -26,7 +26,10 @@ 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;
@@ -38,6 +41,8 @@ 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,
@@ -47,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);
@@ -56,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);
@@ -77,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

@@ -29,8 +29,6 @@ import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
@@ -45,7 +43,6 @@ 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;
@@ -86,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;
}
@@ -238,6 +235,7 @@ 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());
@@ -398,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);
@@ -471,12 +469,15 @@ public class DirectoryHelper {
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;
}
@@ -487,6 +488,10 @@ public class DirectoryHelper {
@NonNull Map<String, String> getNumberRewrites() {
return numberRewrites;
}
@NonNull Set<String> getIgnoredNumbers() {
return ignoredNumbers;
}
}
private static class UnlistedResult {

View File

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

View File

@@ -39,10 +39,10 @@ import android.os.Bundle;
import android.os.Vibrator;
import android.provider.Browser;
import android.provider.ContactsContract;
import android.provider.Telephony;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -69,8 +69,10 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.ViewModelProviders;
@@ -98,7 +100,6 @@ import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.ComposeText;
@@ -116,6 +117,7 @@ import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder;
import org.thoughtcrime.securesms.components.reminder.Reminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
@@ -163,10 +165,12 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupChangeResult;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.invites.InviteReminderModel;
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
@@ -206,6 +210,9 @@ import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
@@ -242,8 +249,11 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageUtil;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SmsUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
@@ -340,6 +350,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
protected Stub<ReminderView> reminderView;
private Stub<UnverifiedBannerView> unverifiedBannerView;
private Stub<GroupShareProfileView> groupShareProfileView;
private Stub<ReviewBannerView> reviewBanner;
private TypingStatusTextWatcher typingTextWatcher;
private ConversationSearchBottomBar searchNav;
private MenuItem searchViewItem;
@@ -366,6 +377,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private ConversationViewModel viewModel;
private InviteReminderModel inviteReminderModel;
private ConversationGroupViewModel groupViewModel;
private MentionsPickerViewModel mentionsViewModel;
private LiveRecipient recipient;
private long threadId;
@@ -442,12 +454,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
initializeStickerObserver();
initializeViewModel();
initializeGroupViewModel();
if (FeatureFlags.mentions()) initializeMentionsViewModel();
initializeMentionsViewModel();
initializeEnabledCheck();
initializePendingRequestsBanner();
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
initializeProfiles();
initializeGv1Migration();
initializeDraft().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean loadedDraft) {
@@ -556,7 +570,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
fragment.setLastSeen(System.currentTimeMillis());
markLastSeen();
AudioSlidePlayer.stopAll();
EventBus.getDefault().unregister(this);
}
@@ -815,10 +828,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
} else {
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
}
inflater.inflate(R.menu.conversation_active_group_options, menu);
} else if (isActiveV2Group || isActiveGroup) {
inflater.inflate(R.menu.conversation_active_group_options, menu);
}
inflater.inflate(R.menu.conversation_active_group_options, menu);
}
inflater.inflate(R.menu.conversation, menu);
@@ -836,7 +848,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
inflater.inflate(R.menu.conversation_add_to_contacts, menu);
}
if (recipient != null && recipient.get().isLocalNumber()) {
if (recipient != null && recipient.get().isSelf()) {
if (isSecureText) {
hideMenuItem(menu, R.id.menu_call_secure);
hideMenuItem(menu, R.id.menu_video_secure);
@@ -865,7 +877,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isActiveV2Group) {
hideMenuItem(menu, R.id.menu_mute_notifications);
hideMenuItem(menu, R.id.menu_conversation_settings);
} else if (isActiveGroup) {
} else if (isGroupConversation()) {
hideMenuItem(menu, R.id.menu_conversation_settings);
}
@@ -978,9 +990,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public void onBackPressed() {
Log.d(TAG, "onBackPressed()");
if (reactionOverlay.isShowing()) reactionOverlay.hide();
else if (container.isInputOpen()) container.hideCurrentInput(composeText);
else super.onBackPressed();
if (reactionOverlay.isShowing()) {
reactionOverlay.hide();
} else if (container.isInputOpen()) {
container.hideCurrentInput(composeText);
} else {
super.onBackPressed();
}
}
@Override
@@ -1033,6 +1049,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
Permissions.with(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.onAllGranted(() -> viewModel.onAttachmentKeyboardOpen())
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.execute();
}
@@ -1123,9 +1140,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@TargetApi(Build.VERSION_CODES.KITKAT)
private void handleMakeDefaultSms() {
Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT);
intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, getPackageName());
startActivityForResult(intent, SMS_DEFAULT);
startActivityForResult(SmsUtil.getSmsRoleIntent(this), SMS_DEFAULT);
}
private void handleRegisterForSignal() {
@@ -1221,7 +1236,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@NonNull Recipient recipient)
{
IconCompat icon = IconCompat.createWithAdaptiveBitmap(bitmap);
String name = recipient.isLocalNumber() ? context.getString(R.string.note_to_self)
String name = recipient.isSelf() ? context.getString(R.string.note_to_self)
: recipient.getDisplayName(context);
ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, recipient.getId().serialize() + '-' + System.currentTimeMillis())
@@ -1527,6 +1542,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
});
}
private void initializePendingRequestsBanner() {
groupViewModel.getActionableRequestingMembers()
.observe(this, actionablePendingGroupRequests -> updateReminders());
}
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
SettableFuture<Boolean> future = new SettableFuture<>();
@@ -1680,7 +1700,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
protected void updateReminders() {
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
Integer actionableRequestingMembers = groupViewModel.getActionableRequestingMembers().getValue();
if (UnauthorizedReminder.isEligible(this)) {
reminderView.get().showReminder(new UnauthorizedReminder(this));
@@ -1698,6 +1719,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
reminderView.get().setOnActionClickListener(this::handleReminderAction);
reminderView.get().setOnDismissListener(() -> inviteReminderModel.dismissReminder());
reminderView.get().showReminder(inviteReminder.get());
} else if (actionableRequestingMembers != null && actionableRequestingMembers > 0 && FeatureFlags.groupsV2manageGroupLinks()) {
reminderView.get().showReminder(PendingGroupJoinRequestsReminder.create(this, actionableRequestingMembers));
reminderView.get().setOnActionClickListener(id -> {
if (id == R.id.reminder_action_review_join_requests) {
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(this, getRecipient().getGroupId().get().requireV2()));
}
});
} else if (reminderView.resolved()) {
reminderView.get().hide();
}
@@ -1810,6 +1838,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
groupShareProfileView = ViewUtil.findStubById(this, R.id.group_share_profile_view_stub);
reviewBanner = ViewUtil.findStubById(this, R.id.review_banner_stub);
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
@@ -1978,10 +2007,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
groupViewModel = ViewModelProviders.of(this, new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class);
recipient.observe(this, groupViewModel::onRecipientChange);
groupViewModel.getGroupActiveState().observe(this, unused -> invalidateOptionsMenu());
groupViewModel.getReviewState().observe(this, this::presentGroupReviewBanner);
}
private void initializeMentionsViewModel() {
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
recipient.observe(this, r -> {
if (r.isPushV2Group() && !mentionsSuggestions.resolved()) {
@@ -2106,6 +2136,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
RetrieveProfileJob.enqueueAsync(recipient.getId());
}
private void initializeGv1Migration() {
GroupV1MigrationJob.enqueuePossibleAutoMigrate(recipient.getId());
}
private void onRecipientChanged(@NonNull Recipient recipient) {
Log.i(TAG, "onModified(" + recipient.getId() + ") " + recipient.getRegistered());
titleView.setTitle(glideRequests, recipient);
@@ -2123,6 +2157,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (groupViewModel != null) {
groupViewModel.onRecipientChange(recipient);
}
if (mentionsViewModel != null) {
mentionsViewModel.onRecipientChange(recipient);
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
@@ -2227,6 +2265,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
private Drafts getDraftsForCurrentState() {
Drafts drafts = new Drafts();
if (recipient.get().isGroup() && !recipient.get().isActiveGroup()) {
return drafts;
}
if (!Util.isEmpty(composeText)) {
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed().toString()));
List<Mention> draftMentions = composeText.getMentions();
@@ -2377,7 +2419,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (!TextSecurePreferences.isPushRegistered(this)) return false;
if (recipient.get().isGroup()) return false;
return recipient.get().isLocalNumber();
return recipient.get().isSelf();
}
private boolean isGroupConversation() {
@@ -3036,7 +3078,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
messageRequestBottomView.setBlockOnClickListener(v -> onMessageRequestBlockClicked(viewModel));
messageRequestBottomView.setUnblockOnClickListener(v -> onMessageRequestUnblockClicked(viewModel));
viewModel.getRecipient().observe(this, this::presentMessageRequestBottomViewTo);
viewModel.getRequestReviewDisplayState().observe(this, this::presentRequestReviewBanner);
viewModel.getMessageData().observe(this, this::presentMessageRequestBottomViewTo);
viewModel.getMessageRequestDisplayState().observe(this, this::presentMessageRequestDisplayState);
viewModel.getFailures().observe(this, this::showGroupChangeErrorToast);
viewModel.getMessageRequestStatus().observe(this, status -> {
@@ -3061,6 +3104,42 @@ public class ConversationActivity extends PassphraseRequiredActivity
});
}
private void presentRequestReviewBanner(@NonNull MessageRequestViewModel.RequestReviewDisplayState state) {
switch (state) {
case SHOWN:
reviewBanner.get().setVisibility(View.VISIBLE);
CharSequence message = new SpannableStringBuilder().append(SpanUtil.bold(getString(R.string.ConversationFragment__review_requests_carefully)))
.append(" ")
.append(getString(R.string.ConversationFragment__signal_found_another_contact_with_the_same_name));
reviewBanner.get().setBannerMessage(message);
Drawable drawable = Objects.requireNonNull(ThemeUtil.getThemedDrawable(this, R.attr.menu_info_icon)).mutate();
DrawableCompat.setTint(drawable, ThemeUtil.getThemedColor(this, R.attr.icon_tint));
reviewBanner.get().setBannerIcon(drawable);
reviewBanner.get().setOnClickListener(unused -> handleReviewRequest(recipient.getId()));
break;
case HIDDEN:
reviewBanner.get().setVisibility(View.GONE);
break;
default:
break;
}
}
private void presentGroupReviewBanner(@NonNull ConversationGroupViewModel.ReviewState groupReviewState) {
if (groupReviewState.getCount() > 0) {
reviewBanner.get().setVisibility(View.VISIBLE);
reviewBanner.get().setBannerMessage(getString(R.string.ConversationFragment__d_group_members_have_the_same_name, groupReviewState.getCount()));
reviewBanner.get().setBannerRecipient(groupReviewState.getRecipient());
reviewBanner.get().setOnClickListener(unused -> handleReviewGroupMembers(groupReviewState.getGroupId()));
} else if (reviewBanner.resolved()) {
reviewBanner.get().setVisibility(View.GONE);
}
}
private void showMessageRequestBusy() {
messageRequestBottomView.showBusy();
}
@@ -3069,6 +3148,24 @@ public class ConversationActivity extends PassphraseRequiredActivity
messageRequestBottomView.hideBusy();
}
private void handleReviewGroupMembers(@Nullable GroupId.V2 groupId) {
if (groupId == null) {
return;
}
ReviewCardDialogFragment.createForReviewMembers(groupId)
.show(getSupportFragmentManager(), null);
}
private void handleReviewRequest(@NonNull RecipientId recipientId) {
if (recipientId == Recipient.UNKNOWN.getId()) {
return;
}
ReviewCardDialogFragment.createForReviewRequest(recipientId)
.show(getSupportFragmentManager(), null);
}
private void showGroupChangeErrorToast(@NonNull GroupChangeFailureReason e) {
Toast.makeText(this, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show();
}
@@ -3081,7 +3178,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
{
reactionOverlay.setOnToolbarItemClickedListener(toolbarListener);
reactionOverlay.setOnHideListener(onHideListener);
reactionOverlay.show(this, maskTarget, messageRecord, inputAreaHeight());
reactionOverlay.show(this, maskTarget, recipient.get(), messageRecord, inputAreaHeight());
}
@Override
@@ -3269,7 +3366,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private void presentMessageRequestDisplayState(@NonNull MessageRequestViewModel.DisplayState displayState) {
if (getIntent().hasExtra(TEXT_EXTRA) || getIntent().hasExtra(MEDIA_EXTRA) || getIntent().hasExtra(STICKER_EXTRA)) {
if ((getIntent().hasExtra(TEXT_EXTRA) && !Util.isEmpty(getIntent().getStringExtra(TEXT_EXTRA))) ||
getIntent().hasExtra(MEDIA_EXTRA) ||
getIntent().hasExtra(STICKER_EXTRA))
{
Log.d(TAG, "[presentMessageRequestDisplayState] Have extra, so ignoring provided state.");
messageRequestBottomView.setVisibility(View.GONE);
} else if (isPushGroupV1Conversation() && !isActiveGroup()) {
@@ -3284,7 +3384,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
groupShareProfileView.get().setVisibility(View.GONE);
}
break;
case DISPLAY_LEGACY:
case DISPLAY_PRE_MESSAGE_REQUEST:
if (recipient.get().isGroup()) {
groupShareProfileView.get().setRecipient(recipient.get());
groupShareProfileView.get().setVisibility(View.VISIBLE);
@@ -3449,10 +3549,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
}
private void presentMessageRequestBottomViewTo(@Nullable Recipient recipient) {
if (recipient == null) return;
private void presentMessageRequestBottomViewTo(@Nullable MessageRequestViewModel.MessageData messageData) {
if (messageData == null) return;
messageRequestBottomView.setRecipient(recipient);
messageRequestBottomView.setMessageData(messageData);
}
private static class KeyboardImageDetails {

View File

@@ -16,12 +16,12 @@
*/
package org.thoughtcrime.securesms.conversation;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
@@ -52,6 +52,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.text.HtmlCompat;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -70,6 +71,8 @@ import org.thoughtcrime.securesms.components.ConversationScrollToView;
import org.thoughtcrime.securesms.components.ConversationTypingView;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
@@ -86,6 +89,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationBottomSheetDialogFragment;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -99,6 +103,8 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
@@ -115,12 +121,12 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -179,6 +185,7 @@ public class ConversationFragment extends LoggingFragment {
private Animation mentionButtonOutAnimation;
private OnScrollListener conversationScrollListener;
private int pulsePosition = -1;
private VoiceNoteMediaController voiceNoteMediaController;
public static void prepare(@NonNull Context context) {
FrameLayout parent = new FrameLayout(context);
@@ -226,7 +233,10 @@ public class ConversationFragment extends LoggingFragment {
new ConversationItemSwipeCallback(
conversationMessage -> actionMode == null &&
MenuState.canReplyToMessage(MenuState.isActionMessage(conversationMessage.getMessageRecord()), conversationMessage.getMessageRecord(), messageRequestViewModel.shouldShowMessageRequest()),
MenuState.canReplyToMessage(recipient.get(),
MenuState.isActionMessage(conversationMessage.getMessageRecord()),
conversationMessage.getMessageRecord(),
messageRequestViewModel.shouldShowMessageRequest()),
this::handleReplyMessage
).attachToRecyclerView(list);
@@ -303,6 +313,7 @@ public class ConversationFragment extends LoggingFragment {
initializeResources();
initializeMessageRequestViewModel();
initializeListAdapter();
voiceNoteMediaController = new VoiceNoteMediaController((AppCompatActivity) requireActivity());
}
@Override
@@ -345,9 +356,17 @@ public class ConversationFragment extends LoggingFragment {
actionMode.finish();
}
long oldThreadId = threadId;
initializeResources();
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
initializeListAdapter();
int startingPosition = getStartPosition();
if (startingPosition != -1 && oldThreadId == threadId) {
list.post(() -> moveToPosition(startingPosition, () -> Log.w(TAG, "Could not scroll to requested message.")));
} else {
initializeListAdapter();
}
}
public void moveToLastSeen() {
@@ -365,6 +384,10 @@ public class ConversationFragment extends LoggingFragment {
snapToTopDataObserver.requestScrollPosition(position);
}
private int getStartPosition() {
return requireActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
}
private void initializeMessageRequestViewModel() {
MessageRequestViewModel.Factory factory = new MessageRequestViewModel.Factory(requireContext());
@@ -411,7 +434,7 @@ public class ConversationFragment extends LoggingFragment {
} else if (isSelf) {
conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation));
} else {
String subtitle = recipient.getE164().orNull();
String subtitle = recipient.getE164().transform(PhoneNumberFormatter::prettyPrint).orNull();
if (subtitle == null || subtitle.equals(title)) {
conversationBanner.hideSubtitle();
@@ -452,7 +475,7 @@ public class ConversationFragment extends LoggingFragment {
private void initializeResources() {
long oldThreadId = threadId;
int startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
int startingPosition = getStartPosition();
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
@@ -573,7 +596,7 @@ public class ConversationFragment extends LoggingFragment {
return;
}
MenuState menuState = MenuState.getMenuState(Stream.of(messages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet()), messageRequestViewModel.shouldShowMessageRequest());
MenuState menuState = MenuState.getMenuState(recipient.get(), Stream.of(messages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet()), messageRequestViewModel.shouldShowMessageRequest());
menu.findItem(R.id.menu_context_forward).setVisible(menuState.shouldShowForwardAction());
menu.findItem(R.id.menu_context_reply).setVisible(menuState.shouldShowReplyAction());
@@ -662,53 +685,7 @@ public class ConversationFragment extends LoggingFragment {
private void handleDeleteMessages(final Set<ConversationMessage> conversationMessages) {
Set<MessageRecord> messageRecords = Stream.of(conversationMessages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet());
if (FeatureFlags.remoteDelete()) {
buildRemoteDeleteConfirmationDialog(messageRecords).show();
} else {
buildLegacyDeleteConfirmationDialog(messageRecords).show();
}
}
private AlertDialog.Builder buildLegacyDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
int messagesCount = messageRecords.size();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIconAttribute(R.attr.dialog_alert_icon);
builder.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messagesCount, messagesCount));
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
builder.setCancelable(true);
builder.setPositiveButton(R.string.delete, (dialog, which) -> {
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
R.string.ConversationFragment_deleting,
R.string.ConversationFragment_deleting_messages)
{
@Override
protected Void doInBackground(Void... voids) {
for (MessageRecord messageRecord : messageRecords) {
boolean threadDeleted;
if (messageRecord.isMms()) {
threadDeleted = DatabaseFactory.getMmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
} else {
threadDeleted = DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
}
if (threadDeleted) {
threadId = -1;
conversationViewModel.clearThreadId();
messageCountsViewModel.clearThreadId();
listener.setThreadId(threadId);
}
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
builder.setNegativeButton(android.R.string.cancel, null);
return builder;
buildRemoteDeleteConfirmationDialog(messageRecords).show();
}
private AlertDialog.Builder buildRemoteDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
@@ -769,7 +746,7 @@ public class ConversationFragment extends LoggingFragment {
deleteForEveryone.run();
} else {
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.ConversationFragment_this_message_will_be_permanently_deleted_for_everyone)
.setMessage(R.string.ConversationFragment_this_message_will_be_deleted_for_everyone_in_the_conversation)
.setPositiveButton(R.string.ConversationFragment_delete_for_everyone, (dialog, which) -> {
SignalStore.uiHints().markHasConfirmedDeleteForEveryoneOnce();
deleteForEveryone.run();
@@ -881,26 +858,40 @@ public class ConversationFragment extends LoggingFragment {
throw new AssertionError("Cannot save a view-once message.");
}
SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
.toList();
if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
return;
}
Log.w(TAG, "No slide with attachable media found, failing nicely.");
Toast.makeText(getActivity(),
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show();
SaveAttachmentTask.showWarningDialog(getActivity(), (dialog, which) -> {
if (StorageUtil.canWriteToMediaStore()) {
performSave(message);
return;
}
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> performSave(message))
.execute();
});
}
private void performSave(final MediaMmsMessageRecord message) {
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
.map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateReceived(), s.getFileName().orNull()))
.toList();
if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
return;
}
Log.w(TAG, "No slide with attachable media found, failing nicely.");
Toast.makeText(getActivity(),
getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
Toast.LENGTH_LONG).show();
}
private void clearHeaderIfNotTyping(ConversationAdapter adapter) {
if (adapter.getHeaderView() != typingView) {
adapter.setHeaderView(null);
@@ -1223,11 +1214,12 @@ public class ConversationFragment extends LoggingFragment {
MessageRecord messageRecord = conversationMessage.getMessageRecord();
if (messageRecord.isSecure() &&
!messageRecord.isRemoteDelete() &&
!messageRecord.isUpdate() &&
!recipient.get().isBlocked() &&
!messageRequestViewModel.shouldShowMessageRequest() &&
if (messageRecord.isSecure() &&
!messageRecord.isRemoteDelete() &&
!messageRecord.isUpdate() &&
!recipient.get().isBlocked() &&
!messageRequestViewModel.shouldShowMessageRequest() &&
(!recipient.get().isGroup() || recipient.get().isActiveGroup()) &&
((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty())
{
isReacting = true;
@@ -1397,10 +1389,40 @@ public class ConversationFragment extends LoggingFragment {
listener.onMessageWithErrorClicked(messageRecord);
}
@Override
public void onVoiceNotePause(@NonNull Uri uri) {
voiceNoteMediaController.pausePlayback(uri);
}
@Override
public void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress) {
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
}
@Override
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
voiceNoteMediaController.seekToPosition(uri, progress);
}
@Override
public void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), onPlaybackStartObserver);
}
@Override
public void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver) {
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(onPlaybackStartObserver);
}
@Override
public boolean onUrlClicked(@NonNull String url) {
return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url);
}
@Override
public void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients) {
GroupsV1MigrationBottomSheetDialogFragment.showForLearnMore(requireFragmentManager(), pendingRecipients);
}
}
@Override

View File

@@ -10,40 +10,73 @@ import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
final class ConversationGroupViewModel extends ViewModel {
private final MutableLiveData<Recipient> liveRecipient;
private final LiveData<GroupActiveState> groupActiveState;
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
private final LiveData<Integer> actionableRequestingMembers;
private final LiveData<ReviewState> reviewState;
private ConversationGroupViewModel() {
this.liveRecipient = new MutableLiveData<>();
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
LiveData<List<Recipient>> duplicates = LiveDataUtil.mapAsync(groupRecord, record -> {
if (record != null && record.isV2Group()) {
return Stream.of(ReviewUtil.getDuplicatedRecipients(record.getId().requireV2()))
.map(ReviewRecipient::getRecipient)
.toList();
} else {
return Collections.emptyList();
}
});
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
this.reviewState = LiveDataUtil.combineLatest(groupRecord,
duplicates,
(record, dups) -> dups.isEmpty()
? ReviewState.EMPTY
: new ReviewState(record.getId().requireV2(), dups.get(0), dups.size()));
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
}
void onRecipientChange(Recipient recipient) {
liveRecipient.setValue(recipient);
}
/**
* The number of pending group join requests that can be actioned by this client.
*/
LiveData<Integer> getActionableRequestingMembers() {
return actionableRequestingMembers;
}
LiveData<GroupActiveState> getGroupActiveState() {
return groupActiveState;
}
@@ -52,6 +85,10 @@ final class ConversationGroupViewModel extends ViewModel {
return selfMembershipLevel;
}
public LiveData<ReviewState> getReviewState() {
return reviewState;
}
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
if (recipient != null && recipient.isGroup()) {
Application context = ApplicationDependencies.getApplication();
@@ -62,6 +99,20 @@ final class ConversationGroupViewModel extends ViewModel {
}
}
private static int mapToActionableRequestingMemberCount(@Nullable GroupRecord record) {
if (record != null &&
FeatureFlags.groupsV2manageGroupLinks() &&
record.isV2Group() &&
record.memberLevel(Recipient.self()) == GroupDatabase.MemberLevel.ADMINISTRATOR)
{
return record.requireV2GroupProperties()
.getDecryptedGroup()
.getRequestingMembersCount();
} else {
return 0;
}
}
private static GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) {
if (record == null) {
return null;
@@ -93,6 +144,33 @@ final class ConversationGroupViewModel extends ViewModel {
});
}
static final class ReviewState {
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);
private final GroupId.V2 groupId;
private final Recipient recipient;
private final int count;
ReviewState(@Nullable GroupId.V2 groupId, @NonNull Recipient recipient, int count) {
this.groupId = groupId;
this.recipient = recipient;
this.count = count;
}
public @Nullable GroupId.V2 getGroupId() {
return groupId;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
public int getCount() {
return count;
}
}
static final class GroupActiveState {
private final boolean isActive;
private final boolean isActiveV2;

View File

@@ -93,6 +93,7 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.PartAuthority;
@@ -112,6 +113,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
@@ -127,6 +129,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import static org.thoughtcrime.securesms.util.ThemeUtil.isDarkTheme;
@@ -420,14 +423,17 @@ public class ConversationItem extends LinearLayout implements BindableConversati
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
footer.setOnlyShowSendingStatus(false, messageRecord);
} else if (messageRecord.isRemoteDelete() || (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord))) {
bodyBubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_reveal_viewed_background_color), PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord);
} else {
bodyBubble.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
footer.setOnlyShowSendingStatus(false, messageRecord);
}
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_sent_text_secondary_color));
@@ -620,6 +626,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
bodyText.setText(italics);
bodyText.setVisibility(View.VISIBLE);
bodyText.setOverflowText(null);
} else if (isCaptionlessMms(messageRecord)) {
bodyText.setVisibility(View.GONE);
} else {
@@ -652,7 +659,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
{
boolean showControls = !messageRecord.isFailed();
if (isViewOnceMessage(messageRecord)) {
if (eventListener != null && audioViewStub.resolved()) {
Log.d(TAG, "setMediaAttributes: unregistering voice note callbacks for audio slide " + audioViewStub.get().getAudioSlideUri());
eventListener.onUnregisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
}
if (isViewOnceMessage(messageRecord) && !messageRecord.isRemoteDelete()) {
revealableStub.get().setVisibility(VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
@@ -734,11 +746,17 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
audioViewStub.get().setAudio(Objects.requireNonNull(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide()), new AudioViewCallbacks(), showControls, false);
audioViewStub.get().setDownloadClickListener(singleDownloadClickListener);
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
if (eventListener != null) {
Log.d(TAG, "setMediaAttributes: registered listener for audio slide " + audioViewStub.get().getAudioSlideUri());
eventListener.onRegisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
} else {
Log.w(TAG, "setMediaAttributes: could not register listener for audio slide " + audioViewStub.get().getAudioSlideUri());
}
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -1521,7 +1539,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
@Override
public void onClick(@NonNull View widget) {
if (eventListener != null) {
if (eventListener != null && batchSelected.isEmpty()) {
VibrateUtil.vibrateTick(context);
eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId());
}
@@ -1531,6 +1549,40 @@ public class ConversationItem extends LinearLayout implements BindableConversati
public void updateDrawState(@NonNull TextPaint ds) { }
}
private final class AudioViewCallbacks implements AudioView.Callbacks {
@Override
public void onPlay(@NonNull Uri audioUri, double progress) {
if (eventListener == null) return;
eventListener.onVoiceNotePlay(audioUri, messageRecord.getId(), progress);
}
@Override
public void onPause(@NonNull Uri audioUri) {
if (eventListener == null) return;
eventListener.onVoiceNotePause(audioUri);
}
@Override
public void onSeekTo(@NonNull Uri audioUri, double progress) {
if (eventListener == null) return;
eventListener.onVoiceNoteSeekTo(audioUri, progress);
}
@Override
public void onStopAndReset(@NonNull Uri audioUri) {
throw new UnsupportedOperationException();
}
@Override
public void onProgressUpdated(long durationMillis, long playheadMillis) {
footer.setAudioDuration(durationMillis, playheadMillis);
}
}
private void handleMessageApproval() {
final int title;
final int message;

View File

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

View File

@@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -97,7 +96,7 @@ public class ConversationTitleView extends RelativeLayout {
startDrawable = R.drawable.ic_volume_off_white_18dp;
}
if (recipient != null && recipient.isSystemContact() && !recipient.isLocalNumber()) {
if (recipient != null && recipient.isSystemContact() && !recipient.isSelf()) {
endDrawable = R.drawable.ic_profile_circle_outline_16;
}
@@ -125,7 +124,7 @@ public class ConversationTitleView extends RelativeLayout {
private void setRecipientTitle(Recipient recipient) {
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
else if (recipient.isLocalNumber()) setSelfTitle();
else if (recipient.isSelf()) setSelfTitle();
else setIndividualRecipientTitle(recipient);
}
@@ -145,8 +144,8 @@ public class ConversationTitleView extends RelativeLayout {
private void setGroupRecipientTitle(Recipient recipient) {
this.title.setText(recipient.getDisplayName(getContext()));
this.subtitle.setText(Stream.of(recipient.getParticipants())
.sorted((a, b) -> Boolean.compare(a.isLocalNumber(), b.isLocalNumber()))
.map(r -> r.isLocalNumber() ? getResources().getString(R.string.ConversationTitleView_you)
.sorted((a, b) -> Boolean.compare(a.isSelf(), b.isSelf()))
.map(r -> r.isSelf() ? getResources().getString(R.string.ConversationTitleView_you)
: r.getDisplayName(getContext()))
.collect(Collectors.joining(", ")));

View File

@@ -1,23 +1,18 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.text.Spannable;
import android.text.SpannableString;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
@@ -30,10 +25,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -50,15 +42,14 @@ public final class ConversationUpdateItem extends LinearLayout
private Set<ConversationMessage> batchSelected;
private ImageView icon;
private TextView title;
private TextView body;
private TextView date;
private LiveRecipient sender;
private ConversationMessage conversationMessage;
private MessageRecord messageRecord;
private Locale locale;
private LiveData<SpannableString> displayBody;
private TextView body;
private TextView actionButton;
private LiveRecipient sender;
private ConversationMessage conversationMessage;
private Optional<MessageRecord> nextMessageRecord;
private MessageRecord messageRecord;
private LiveData<Spannable> displayBody;
private EventListener eventListener;
private final UpdateObserver updateObserver = new UpdateObserver();
private final SenderObserver senderObserver = new SenderObserver();
@@ -74,11 +65,8 @@ public final class ConversationUpdateItem extends LinearLayout
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.icon = findViewById(R.id.conversation_update_icon);
this.title = findViewById(R.id.conversation_update_title);
this.body = findViewById(R.id.conversation_update_body);
this.date = findViewById(R.id.conversation_update_date);
this.body = findViewById(R.id.conversation_update_body);
this.actionButton = findViewById(R.id.conversation_update_action);
this.setOnClickListener(new InternalClickListener(null));
}
@@ -97,12 +85,12 @@ public final class ConversationUpdateItem extends LinearLayout
{
this.batchSelected = batchSelected;
bind(lifecycleOwner, conversationMessage, locale);
bind(lifecycleOwner, conversationMessage, nextMessageRecord);
}
@Override
public void setEventListener(@Nullable EventListener listener) {
// No events to report yet
this.eventListener = listener;
}
@Override
@@ -110,29 +98,28 @@ public final class ConversationUpdateItem extends LinearLayout
return conversationMessage;
}
private void bind(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
private void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> nextMessageRecord)
{
this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord();
this.locale = locale;
this.nextMessageRecord = nextMessageRecord;
observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
LiveData<SpannableString> spannableStringMessage = toSpannable(loading(liveUpdateMessage));
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription);
LiveData<Spannable> spannableMessage = loading(liveUpdateMessage);
present(conversationMessage);
present(conversationMessage, nextMessageRecord);
observeDisplayBody(lifecycleOwner, spannableStringMessage);
observeDisplayBody(lifecycleOwner, spannableMessage);
}
/** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
private @NonNull LiveData<String> loading(@NonNull LiveData<String> string) {
return LiveDataUtil.until(string, LiveDataUtil.delay(250, getContext().getString(R.string.ConversationUpdateItem_loading)));
}
private static LiveData<SpannableString> toSpannable(LiveData<String> loading) {
return Transformations.map(loading, source -> source == null ? null : new SpannableString(source));
private @NonNull LiveData<Spannable> loading(@NonNull LiveData<Spannable> string) {
return LiveDataUtil.until(string, LiveDataUtil.delay(250, new SpannableString(getContext().getString(R.string.ConversationUpdateItem_loading))));
}
@Override
@@ -152,7 +139,7 @@ public final class ConversationUpdateItem extends LinearLayout
}
}
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<SpannableString> displayBody) {
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<Spannable> displayBody) {
if (this.displayBody != displayBody) {
if (this.displayBody != null) {
this.displayBody.removeObserver(updateObserver);
@@ -175,97 +162,24 @@ public final class ConversationUpdateItem extends LinearLayout
}
}
private void present(ConversationMessage conversationMessage) {
MessageRecord messageRecord = conversationMessage.getMessageRecord();
if (messageRecord.isGroupAction()) setGroupRecord();
else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
else if (messageRecord.isJoined()) setJoinedRecord();
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
else if (messageRecord.isEndSession()) setEndSessionRecord();
else if (messageRecord.isIdentityUpdate()) setIdentityRecord();
else if (messageRecord.isIdentityVerified() ||
messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord);
else if (messageRecord.isProfileChange()) setProfileNameChangeRecord();
else throw new AssertionError("Neither group nor log nor joined.");
private void present(ConversationMessage conversationMessage, @NonNull Optional<MessageRecord> nextMessageRecord) {
if (batchSelected.contains(conversationMessage)) setSelected(true);
else setSelected(false);
}
private void setCallRecord(MessageRecord messageRecord) {
if (messageRecord.isIncomingCall()) icon.setImageResource(R.drawable.ic_call_received_grey600_24dp);
else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp);
else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp);
date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateSent()));
title.setVisibility(GONE);
date.setVisibility(View.VISIBLE);
}
private void setTimerRecord(final MessageRecord messageRecord) {
if (messageRecord.getExpiresIn() > 0) {
icon.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_timer_24));
if (conversationMessage.getMessageRecord().isGroupV1MigrationEvent() &&
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isGroupV1MigrationEvent()))
{
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationEventInvites());
}
});
} else {
icon.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_timer_disabled_24));
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
}
icon.setColorFilter(getIconTintFilter());
title.setText(ExpirationUtil.getExpirationDisplayValue(getContext(), (int)(messageRecord.getExpiresIn() / 1000)));
title.setVisibility(VISIBLE);
date.setVisibility(GONE);
}
private ColorFilter getIconTintFilter() {
return new PorterDuffColorFilter(ThemeUtil.getThemedColor(getContext(), R.attr.icon_tint), PorterDuff.Mode.SRC_IN);
}
private void setIdentityRecord() {
icon.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.safety_number_icon));
icon.setColorFilter(getIconTintFilter());
title.setVisibility(GONE);
date.setVisibility(GONE);
}
private void setIdentityVerifyUpdate(final MessageRecord messageRecord) {
if (messageRecord.isIdentityVerified()) icon.setImageResource(R.drawable.ic_check_white_24dp);
else icon.setImageResource(R.drawable.ic_info_outline_white_24);
icon.setColorFilter(getIconTintFilter());
title.setVisibility(GONE);
date.setVisibility(GONE);
}
private void setProfileNameChangeRecord() {
icon.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_profile_outline_20));
icon.setColorFilter(getIconTintFilter());
title.setVisibility(GONE);
date.setVisibility(GONE);
}
private void setGroupRecord() {
icon.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.menu_group_icon));
icon.clearColorFilter();
title.setVisibility(GONE);
date.setVisibility(GONE);
}
private void setJoinedRecord() {
icon.setImageResource(R.drawable.ic_favorite_grey600_24dp);
icon.clearColorFilter();
title.setVisibility(GONE);
date.setVisibility(GONE);
}
private void setEndSessionRecord() {
icon.setImageResource(R.drawable.ic_refresh_white_24dp);
icon.setColorFilter(getIconTintFilter());
}
@Override
@@ -277,14 +191,14 @@ public final class ConversationUpdateItem extends LinearLayout
@Override
public void onChanged(Recipient recipient) {
present(conversationMessage);
present(conversationMessage, nextMessageRecord);
}
}
private final class UpdateObserver implements Observer<SpannableString> {
private final class UpdateObserver implements Observer<Spannable> {
@Override
public void onChanged(SpannableString update) {
public void onChanged(Spannable update) {
setBodyText(update);
}
}

View File

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

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -24,13 +25,14 @@ import java.util.List;
public class MentionsPickerFragment extends LoggingFragment {
private MentionsPickerAdapter adapter;
private RecyclerView list;
private View topDivider;
private View bottomDivider;
private BottomSheetBehavior<View> behavior;
private MentionsPickerViewModel viewModel;
private Runnable lockSheetAfterListUpdate = () -> behavior.setHideable(false);
private MentionsPickerAdapter adapter;
private RecyclerView list;
private View topDivider;
private View bottomDivider;
private BottomSheetBehavior<View> behavior;
private MentionsPickerViewModel viewModel;
private final Runnable lockSheetAfterListUpdate = () -> behavior.setHideable(false);
private final Handler handler = new Handler();
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -112,10 +114,10 @@ public class MentionsPickerFragment extends LoggingFragment {
if (isShowing) {
list.scrollToPosition(0);
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
list.post(lockSheetAfterListUpdate);
handler.post(lockSheetAfterListUpdate);
showDividers(true);
} else {
list.getHandler().removeCallbacks(lockSheetAfterListUpdate);
handler.removeCallbacks(lockSheetAfterListUpdate);
behavior.setHideable(true);
behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}

View File

@@ -40,7 +40,7 @@ final class MentionsPickerRepository {
@WorkerThread
@NonNull List<Recipient> search(@NonNull MentionQuery mentionQuery) {
if (mentionQuery.query == null) {
if (mentionQuery.query == null || mentionQuery.members.isEmpty()) {
return Collections.emptyList();
}

View File

@@ -54,14 +54,12 @@ import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.lifecycle.ViewModelProviders;
import androidx.paging.PagedList;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -149,6 +147,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
MegaphoneActionController
{
public static final short MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME = 32562;
public static final short SMS_ROLE_REQUEST_CODE = 32563;
private static final String TAG = Log.tag(ConversationListFragment.class);
@@ -590,7 +589,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
} else if (OutdatedBuildReminder.isEligible()) {
return Optional.of(new OutdatedBuildReminder(context));
} else if (DefaultSmsReminder.isEligible(context)) {
return Optional.of(new DefaultSmsReminder(context));
return Optional.of(new DefaultSmsReminder(this, SMS_ROLE_REQUEST_CODE));
} else if (Util.isDefaultSmsProvider(context) && SystemSmsImportReminder.isEligible(context)) {
return Optional.of((new SystemSmsImportReminder(context)));
} else if (PushRegistrationReminder.isEligible(context)) {

View File

@@ -29,6 +29,7 @@ import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
@@ -171,7 +172,7 @@ public final class ConversationListItem extends RelativeLayout
this.recipient.observeForever(this);
if (highlightSubstring != null) {
String name = recipient.get().isLocalNumber() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
String name = recipient.get().isSelf() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
this.fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), name, highlightSubstring));
} else {
@@ -365,7 +366,11 @@ public final class ConversationListItem extends RelativeLayout
}
private void setStatusIcons(ThreadRecord thread) {
if (!thread.isOutgoing() || thread.isOutgoingCall() || thread.isVerificationStatusChange()) {
if (!thread.isOutgoing() ||
thread.isOutgoingAudioCall() ||
thread.isOutgoingVideoCall() ||
thread.isVerificationStatusChange())
{
deliveryStatusIndicator.setNone();
alertView.setNone();
} else if (thread.isFailed()) {
@@ -377,10 +382,23 @@ public final class ConversationListItem extends RelativeLayout
} else {
alertView.setNone();
if (thread.isPending()) deliveryStatusIndicator.setPending();
else if (thread.isRemoteRead()) deliveryStatusIndicator.setRead();
else if (thread.isDelivered()) deliveryStatusIndicator.setDelivered();
else deliveryStatusIndicator.setSent();
if (thread.getExtra() != null && thread.getExtra().isRemoteDelete()) {
if (thread.isPending()) {
deliveryStatusIndicator.setPending();
} else {
deliveryStatusIndicator.setNone();
}
} else {
if (thread.isPending()) {
deliveryStatusIndicator.setPending();
} else if (thread.isRemoteRead()) {
deliveryStatusIndicator.setRead();
} else if (thread.isDelivered()) {
deliveryStatusIndicator.setDelivered();
} else {
deliveryStatusIndicator.setSent();
}
}
}
}
@@ -409,71 +427,68 @@ public final class ConversationListItem extends RelativeLayout
}
private static @NonNull LiveData<SpannableString> getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) {
if (thread.getGroupAddedBy() != null) {
return emphasisAdded(recipientToStringAsync(thread.getGroupAddedBy(),
r -> context.getString(thread.isGv2Invite() ? R.string.ThreadRecord_s_invited_you_to_the_group
: R.string.ThreadRecord_s_added_you_to_the_group,
r.getDisplayName(context))));
} else if (!thread.isMessageRequestAccepted()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_message_request));
if (!thread.isMessageRequestAccepted()) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_request));
} else if (SmsDatabase.Types.isGroupUpdate(thread.getType())) {
if (thread.getRecipient().isPushV2Group()) {
return emphasisAdded(MessageRecord.getGv2ChangeDescription(context, thread.getBody()));
return emphasisAdded(context, MessageRecord.getGv2ChangeDescription(context, thread.getBody()));
} else {
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_group_updated));
}
} else if (SmsDatabase.Types.isGroupQuit(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_left_the_group));
} else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) {
return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message));
return emphasisAdded(context, context.getString(R.string.ConversationListItem_key_exchange_message));
} else if (SmsDatabase.Types.isFailedDecryptType(thread.getType())) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (SmsDatabase.Types.isNoRemoteSessionType(thread.getType())) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
} else if (SmsDatabase.Types.isEndSessionType(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_secure_session_reset));
} else if (MmsSmsColumns.Types.isLegacyType(thread.getType())) {
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
return emphasisAdded(context, context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
} else if (MmsSmsColumns.Types.isDraftMessageType(thread.getType())) {
String draftText = context.getString(R.string.ThreadRecord_draft);
return emphasisAdded(draftText + " " + thread.getBody());
} else if (SmsDatabase.Types.isOutgoingCall(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_called));
} else if (SmsDatabase.Types.isIncomingCall(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_called_you));
} else if (SmsDatabase.Types.isMissedCall(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_missed_call));
return emphasisAdded(context, draftText + " " + thread.getBody());
} else if (SmsDatabase.Types.isOutgoingAudioCall(thread.getType()) || SmsDatabase.Types.isOutgoingVideoCall(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_called));
} else if (SmsDatabase.Types.isIncomingAudioCall(thread.getType()) || SmsDatabase.Types.isIncomingVideoCall(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_called_you));
} else if (SmsDatabase.Types.isMissedAudioCall(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_audio_call));
} else if (SmsDatabase.Types.isMissedVideoCall(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_video_call));
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context))));
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> new SpannableString(context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context)))));
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
int seconds = (int)(thread.getExpiresIn() / 1000);
if (seconds <= 0) {
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
}
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
} else if (SmsDatabase.Types.isIdentityUpdate(thread.getType())) {
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> {
if (r.isGroup()) {
return context.getString(R.string.ThreadRecord_safety_number_changed);
return new SpannableString(context.getString(R.string.ThreadRecord_safety_number_changed));
} else {
return context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context));
return new SpannableString(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)));
}
}));
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_verified));
} else if (SmsDatabase.Types.isIdentityDefault(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_unverified));
} else if (SmsDatabase.Types.isUnsupportedMessageType(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_message_could_not_be_processed));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed));
} else if (SmsDatabase.Types.isProfileChange(thread.getType())) {
return emphasisAdded("");
return emphasisAdded(context, "");
} else {
ThreadDatabase.Extra extra = thread.getExtra();
if (extra != null && extra.isViewOnce()) {
return emphasisAdded(getViewOnceDescription(context, thread.getContentType()));
return emphasisAdded(context, getViewOnceDescription(context, thread.getContentType()));
} else if (extra != null && extra.isRemoteDelete()) {
return emphasisAdded(context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted));
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted));
} else {
return LiveDataUtil.just(new SpannableString(removeNewlines(thread.getBody())));
}
@@ -492,15 +507,15 @@ public final class ConversationListItem extends RelativeLayout
}
}
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull String string) {
return emphasisAdded(UpdateDescription.staticDescription(string));
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull Context context, @NonNull String string) {
return emphasisAdded(context, UpdateDescription.staticDescription(string, 0, 0));
}
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull UpdateDescription description) {
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(description));
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull Context context, @NonNull UpdateDescription description) {
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(context, description));
}
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<String> description) {
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<Spannable> description) {
return Transformations.map(description, sequence -> {
SpannableString spannable = new SpannableString(sequence);
spannable.setSpan(new StyleSpan(Typeface.ITALIC),

View File

@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
@@ -62,6 +63,9 @@ public final class GroupDatabase extends Database {
private static final String TIMESTAMP = "timestamp";
static final String ACTIVE = "active";
static final String MMS = "mms";
private static final String EXPECTED_V2_ID = "expected_v2_id";
private static final String FORMER_V1_MEMBERS = "former_v1_members";
/* V2 Group columns */
/** 32 bytes serialized {@link GroupMasterKey} */
@@ -71,32 +75,33 @@ public final class GroupDatabase extends Database {
/** Serialized {@link DecryptedGroup} protobuf */
private static final String V2_DECRYPTED_GROUP = "decrypted_group";
public static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME +
" (" + ID + " INTEGER PRIMARY KEY, " +
GROUP_ID + " TEXT, " +
RECIPIENT_ID + " INTEGER, " +
TITLE + " TEXT, " +
MEMBERS + " TEXT, " +
AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " +
AVATAR_CONTENT_TYPE + " TEXT, " +
AVATAR_RELAY + " TEXT, " +
TIMESTAMP + " INTEGER, " +
ACTIVE + " INTEGER DEFAULT 1, " +
AVATAR_DIGEST + " BLOB, " +
MMS + " INTEGER DEFAULT 0, " +
V2_MASTER_KEY + " BLOB, " +
V2_REVISION + " BLOB, " +
V2_DECRYPTED_GROUP + " BLOB);";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
GROUP_ID + " TEXT, " +
RECIPIENT_ID + " INTEGER, " +
TITLE + " TEXT, " +
MEMBERS + " TEXT, " +
AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " +
AVATAR_CONTENT_TYPE + " TEXT, " +
AVATAR_RELAY + " TEXT, " +
TIMESTAMP + " INTEGER, " +
ACTIVE + " INTEGER DEFAULT 1, " +
AVATAR_DIGEST + " BLOB, " +
MMS + " INTEGER DEFAULT 0, " +
V2_MASTER_KEY + " BLOB, " +
V2_REVISION + " BLOB, " +
V2_DECRYPTED_GROUP + " BLOB, " +
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
FORMER_V1_MEMBERS + " TEXT DEFAULT NULL);";
public static final String[] CREATE_INDEXS = {
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
"CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
"CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");"
};
private static final String[] GROUP_PROJECTION = {
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, FORMER_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP
};
@@ -129,7 +134,7 @@ public final class GroupDatabase extends Database {
}
}
public boolean findGroup(@NonNull GroupId groupId) {
public boolean groupExists(@NonNull GroupId groupId) {
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?",
new String[] {groupId.toString()},
null, null, null))
@@ -138,6 +143,27 @@ public final class GroupDatabase extends Database {
}
}
/**
* @return A gv1 group whose expected v2 ID matches the one provided.
*/
public Optional<GroupRecord> getGroupV1ByExpectedV2(@NonNull GroupId.V2 gv2Id) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
try (Cursor cursor = db.query(TABLE_NAME, GROUP_PROJECTION, EXPECTED_V2_ID + " = ?", SqlUtil.buildArgs(gv2Id), null, null, null)) {
if (cursor.moveToFirst()) {
return getGroup(cursor);
} else {
return Optional.absent();
}
}
}
public void clearFormerV1Members(@NonNull GroupId.V2 id) {
ContentValues values = new ContentValues();
values.putNull(FORMER_V1_MEMBERS);
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(id));
}
Optional<GroupRecord> getGroup(Cursor cursor) {
Reader reader = new Reader(cursor);
return Optional.fromNullable(reader.getCurrent());
@@ -305,7 +331,7 @@ public final class GroupDatabase extends Database {
for (RecipientId member : currentMembers) {
Recipient resolved = Recipient.resolved(member);
if (memberSet.includeSelf || !resolved.isLocalNumber()) {
if (memberSet.includeSelf || !resolved.isSelf()) {
recipients.add(resolved);
}
}
@@ -320,6 +346,9 @@ public final class GroupDatabase extends Database {
@Nullable SignalServiceAttachmentPointer avatar,
@Nullable String relay)
{
if (groupExists(groupId.deriveV2MigrationGroupId())) {
throw new LegacyGroupInsertException(groupId);
}
create(groupId, title, members, avatar, relay, null, null);
}
@@ -334,6 +363,10 @@ public final class GroupDatabase extends Database {
{
GroupId.V2 groupId = GroupId.v2(groupMasterKey);
if (getGroupV1ByExpectedV2(groupId).isPresent()) {
throw new MissedGroupMigrationInsertException(groupId);
}
create(groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState);
return groupId;
@@ -376,6 +409,9 @@ public final class GroupDatabase extends Database {
if (groupId.isV2()) {
contentValues.put(ACTIVE, groupState != null && gv2GroupActive(groupState) ? 1 : 0);
} else if (groupId.isV1()) {
contentValues.put(ACTIVE, 1);
contentValues.put(EXPECTED_V2_ID, groupId.requireV1().deriveV2MigrationGroupId().toString());
} else {
contentValues.put(ACTIVE, 1);
}
@@ -434,6 +470,48 @@ public final class GroupDatabase extends Database {
notifyConversationListListeners();
}
/**
* Migrates a V1 group to a V2 group.
*/
public @NonNull GroupId.V2 migrateToV2(@NonNull GroupId.V1 groupIdV1, @NonNull DecryptedGroup decryptedGroup) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
GroupId.V2 groupIdV2 = groupIdV1.deriveV2MigrationGroupId();
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
db.beginTransaction();
try {
GroupRecord record = getGroup(groupIdV1).get();
ContentValues contentValues = new ContentValues();
contentValues.put(GROUP_ID, groupIdV2.toString());
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
contentValues.putNull(EXPECTED_V2_ID);
List<RecipientId> newMembers = Stream.of(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList())).map(u -> RecipientId.from(u, null)).toList();
newMembers.addAll(Stream.of(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList())).map(u -> RecipientId.from(u, null)).toList());
if (record.getMembers().size() > newMembers.size() || !newMembers.containsAll(record.getMembers())) {
contentValues.put(FORMER_V1_MEMBERS, RecipientId.toSerializedList(record.getMembers()));
}
int updated = db.update(TABLE_NAME, contentValues, GROUP_ID + " = ?", SqlUtil.buildArgs(groupIdV1.toString()));
if (updated != 1) {
throw new AssertionError();
}
DatabaseFactory.getRecipientDatabase(context).updateGroupId(groupIdV1, groupIdV2);
update(groupMasterKey, decryptedGroup);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return groupIdV2;
}
public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) {
update(GroupId.v2(groupMasterKey), decryptedGroup);
}
@@ -630,20 +708,21 @@ public final class GroupDatabase extends Database {
return null;
}
return new GroupRecord(GroupId.parseOrThrow(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID))),
RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))),
cursor.getString(cursor.getColumnIndexOrThrow(TITLE)),
cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)),
cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)),
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)),
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)),
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY)),
cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1,
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)),
cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1,
cursor.getBlob(cursor.getColumnIndexOrThrow(V2_MASTER_KEY)),
cursor.getInt(cursor.getColumnIndexOrThrow(V2_REVISION)),
cursor.getBlob(cursor.getColumnIndexOrThrow(V2_DECRYPTED_GROUP)));
return new GroupRecord(GroupId.parseOrThrow(CursorUtil.requireString(cursor, GROUP_ID)),
RecipientId.from(CursorUtil.requireString(cursor, RECIPIENT_ID)),
CursorUtil.requireString(cursor, TITLE),
CursorUtil.requireString(cursor, MEMBERS),
CursorUtil.requireString(cursor, FORMER_V1_MEMBERS),
CursorUtil.requireLong(cursor, AVATAR_ID),
CursorUtil.requireBlob(cursor, AVATAR_KEY),
CursorUtil.requireString(cursor, AVATAR_CONTENT_TYPE),
CursorUtil.requireString(cursor, AVATAR_RELAY),
CursorUtil.requireBoolean(cursor, ACTIVE),
CursorUtil.requireBlob(cursor, AVATAR_DIGEST),
CursorUtil.requireBoolean(cursor, MMS),
CursorUtil.requireBlob(cursor, V2_MASTER_KEY),
CursorUtil.requireInt(cursor, V2_REVISION),
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP));
}
@Override
@@ -659,6 +738,7 @@ public final class GroupDatabase extends Database {
private final RecipientId recipientId;
private final String title;
private final List<RecipientId> members;
private final List<RecipientId> formerV1Members;
private final long avatarId;
private final byte[] avatarKey;
private final byte[] avatarDigest;
@@ -668,10 +748,21 @@ public final class GroupDatabase extends Database {
private final boolean mms;
@Nullable private final V2GroupProperties v2GroupProperties;
public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members,
long avatarId, byte[] avatarKey, String avatarContentType,
String relay, boolean active, byte[] avatarDigest, boolean mms,
@Nullable byte[] groupMasterKeyBytes, int groupRevision, @Nullable byte[] decryptedGroupBytes)
public GroupRecord(@NonNull GroupId id,
@NonNull RecipientId recipientId,
String title,
String members,
String formerV1Members,
long avatarId,
byte[] avatarKey,
String avatarContentType,
String relay,
boolean active,
byte[] avatarDigest,
boolean mms,
@Nullable byte[] groupMasterKeyBytes,
int groupRevision,
@Nullable byte[] decryptedGroupBytes)
{
this.id = id;
this.recipientId = recipientId;
@@ -696,8 +787,17 @@ public final class GroupDatabase extends Database {
}
this.v2GroupProperties = v2GroupProperties;
if (!TextUtils.isEmpty(members)) this.members = RecipientId.fromSerializedList(members);
else this.members = new LinkedList<>();
if (!TextUtils.isEmpty(members)) {
this.members = RecipientId.fromSerializedList(members);
} else {
this.members = Collections.emptyList();
}
if (!TextUtils.isEmpty(formerV1Members)) {
this.formerV1Members = RecipientId.fromSerializedList(formerV1Members);
} else {
this.formerV1Members = Collections.emptyList();
}
}
public GroupId getId() {
@@ -712,10 +812,14 @@ public final class GroupDatabase extends Database {
return title;
}
public List<RecipientId> getMembers() {
public @NonNull List<RecipientId> getMembers() {
return members;
}
public @NonNull List<RecipientId> getFormerV1Members() {
return formerV1Members;
}
public boolean hasAvatar() {
return avatarId != 0;
}
@@ -771,6 +875,8 @@ public final class GroupDatabase extends Database {
public MemberLevel memberLevel(@NonNull Recipient recipient) {
if (isV2Group()) {
return requireV2GroupProperties().memberLevel(recipient);
} else if (isMms() && recipient.isSelf()) {
return MemberLevel.FULL_MEMBER;
} else {
return members.contains(recipient.getId()) ? MemberLevel.FULL_MEMBER
: MemberLevel.NOT_A_MEMBER;
@@ -950,4 +1056,16 @@ public final class GroupDatabase extends Database {
return inGroup;
}
}
public static class LegacyGroupInsertException extends IllegalStateException {
public LegacyGroupInsertException(@Nullable GroupId id) {
super("Tried to create a new GV1 entry when we already had a migrated GV2! " + id);
}
}
public static class MissedGroupMigrationInsertException extends IllegalStateException {
public MissedGroupMigrationInsertException(@Nullable GroupId id) {
super("Tried to create a new GV2 entry when we already had a V1 group that mapped to the new ID! " + id);
}
}
}

View File

@@ -125,6 +125,25 @@ public class MentionDatabase extends Database {
}
}
void deleteMentionsForMessage(long messageId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = MESSAGE_ID + " = ?";
db.delete(TABLE_NAME, where, SqlUtil.buildArgs(messageId));
}
void deleteAbandonedMentions() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = MESSAGE_ID + " NOT IN (SELECT " + MmsDatabase.ID + " FROM " + MmsDatabase.TABLE_NAME + ") OR " + THREAD_ID + " NOT IN (SELECT " + ThreadDatabase.ID + " FROM " + ThreadDatabase.TABLE_NAME + ")";
db.delete(TABLE_NAME, where, null);
}
void deleteAllMentions() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
private @NonNull Map<Long, List<Mention>> readMentions(@Nullable Cursor cursor) {
Map<Long, List<Mention>> mentions = new HashMap<>();
while (cursor != null && cursor.moveToNext()) {

View File

@@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -24,6 +23,8 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
public final class MentionUtil {
@@ -68,14 +69,13 @@ public final class MentionUtil {
return new UpdatedBodyAndMentions(body, mentions);
}
SortedSet<Mention> sortedMentions = new TreeSet<>(mentions);
SpannableStringBuilder updatedBody = new SpannableStringBuilder();
List<Mention> updatedMentions = new ArrayList<>();
Collections.sort(mentions);
int bodyIndex = 0;
for (Mention mention : mentions) {
for (Mention mention : sortedMentions) {
updatedBody.append(body.subSequence(bodyIndex, mention.getStart()));
CharSequence replaceWith = replacementTextGenerator.apply(mention);
Mention updatedMention = new Mention(mention.getRecipientId(), updatedBody.length(), replaceWith.length());

View File

@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList;
import org.thoughtcrime.securesms.insights.InsightsConstants;
import org.thoughtcrime.securesms.logging.Log;
@@ -83,6 +84,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract boolean hasReceivedAnyCallsSince(long threadId, long timestamp);
public abstract @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage();
public abstract boolean isSent(long messageId);
public abstract List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp);
public abstract void markExpireStarted(long messageId);
public abstract void markExpireStarted(long messageId, long startTime);
@@ -108,7 +110,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract void markUnidentified(long messageId, boolean unidentified);
public abstract void markAsSending(long messageId);
public abstract void markAsRemoteDelete(long messageId);
public abstract void markAsMissedCall(long id);
public abstract void markAsMissedCall(long id, boolean isVideoOffer);
public abstract void markAsNotified(long id);
public abstract void markSmsStatus(long id, int status);
public abstract void markDownloadState(long messageId, long state);
@@ -124,9 +126,9 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract void addFailures(long messageId, List<NetworkFailure> failure);
public abstract void removeFailure(long messageId, NetworkFailure failure);
public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address);
public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address);
public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp);
public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer);
public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer);
public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer);
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type);
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message);
@@ -137,6 +139,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName);
public abstract void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients);
public abstract boolean deleteMessage(long messageId);
abstract void deleteThread(long threadId);
@@ -145,6 +148,8 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
abstract void deleteAllThreads();
abstract void deleteAbandonedMessages();
public abstract List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit);
public abstract SQLiteDatabase beginTransaction();
public abstract void endTransaction(SQLiteDatabase database);
public abstract void setTransactionSuccessful();

View File

@@ -82,6 +82,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -364,7 +365,7 @@ public class MmsDatabase extends MessageDatabase {
}
@Override
public void markAsMissedCall(long id) {
public void markAsMissedCall(long id, boolean isVideoOffer) {
throw new UnsupportedOperationException();
}
@@ -379,17 +380,17 @@ public class MmsDatabase extends MessageDatabase {
}
@Override
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address) {
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) {
throw new UnsupportedOperationException();
}
@Override
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address) {
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer) {
throw new UnsupportedOperationException();
}
@Override
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp) {
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer) {
throw new UnsupportedOperationException();
}
@@ -413,6 +414,11 @@ public class MmsDatabase extends MessageDatabase {
throw new UnsupportedOperationException();
}
@Override
public void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients) {
throw new UnsupportedOperationException();
}
@Override
public void endTransaction(SQLiteDatabase database) {
database.endTransaction();
@@ -587,7 +593,7 @@ public class MmsDatabase extends MessageDatabase {
private long getThreadIdFor(@NonNull IncomingMediaMessage retrieved) {
if (retrieved.getGroupId() != null) {
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(retrieved.getGroupId());
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(retrieved.getGroupId());
Recipient groupRecipients = Recipient.resolved(groupRecipientId);
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
} else {
@@ -605,11 +611,25 @@ public class MmsDatabase extends MessageDatabase {
}
private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) {
return rawQuery(where, arguments, false, 0);
}
private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments, boolean reverse, long limit) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
return database.rawQuery("SELECT " + Util.join(MMS_PROJECTION, ",") +
" FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME +
" ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" +
" WHERE " + where + " GROUP BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, arguments);
String rawQueryString = "SELECT " + Util.join(MMS_PROJECTION, ",") +
" FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME +
" ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" +
" WHERE " + where + " GROUP BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID;
if (reverse) {
rawQueryString += " ORDER BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " DESC";
}
if (limit > 0) {
rawQueryString += " LIMIT " + limit;
}
return database.rawQuery(rawQueryString, arguments);
}
private Cursor internalGetMessage(long messageId) {
@@ -704,6 +724,7 @@ public class MmsDatabase extends MessageDatabase {
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) });
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentsForMessage(messageId);
DatabaseFactory.getMentionDatabase(context).deleteMentionsForMessage(messageId);
long threadId = getThreadIdForMessage(messageId);
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
@@ -934,6 +955,8 @@ public class MmsDatabase extends MessageDatabase {
}
}
DatabaseFactory.getMentionDatabase(context).deleteAbandonedMentions();
try (Cursor cursor = database.query(ThreadDatabase.TABLE_NAME, new String[] { ThreadDatabase.ID }, ThreadDatabase.EXPIRES_IN + " > 0", null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
DatabaseFactory.getThreadDatabase(context).update(cursor.getLong(0), false);
@@ -1396,7 +1419,7 @@ public class MmsDatabase extends MessageDatabase {
AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context);
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
boolean mentionsSelf = Stream.of(mentions).filter(m -> Recipient.resolved(m.getRecipientId()).isLocalNumber()).findFirst().isPresent();
boolean mentionsSelf = Stream.of(mentions).filter(m -> Recipient.resolved(m.getRecipientId()).isSelf()).findFirst().isPresent();
List<Attachment> allAttachments = new LinkedList<>();
List<Attachment> contactAttachments = Stream.of(sharedContacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList();
@@ -1460,6 +1483,8 @@ public class MmsDatabase extends MessageDatabase {
@Override
public boolean deleteMessage(long messageId) {
Log.d(TAG, "deleteMessage(" + messageId + ")");
long threadId = getThreadIdForMessage(messageId);
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
attachmentDatabase.deleteAttachmentsForMessage(messageId);
@@ -1467,6 +1492,9 @@ public class MmsDatabase extends MessageDatabase {
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
groupReceiptDatabase.deleteRowsForMessage(messageId);
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
mentionDatabase.deleteMentionsForMessage(messageId);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
@@ -1478,6 +1506,7 @@ public class MmsDatabase extends MessageDatabase {
@Override
public void deleteThread(long threadId) {
Log.d(TAG, "deleteThread(" + threadId + ")");
Set<Long> singleThreadSet = new HashSet<>();
singleThreadSet.add(threadId);
deleteThreads(singleThreadSet);
@@ -1556,8 +1585,15 @@ public class MmsDatabase extends MessageDatabase {
return false;
}
@Override
public List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp) {
throw new UnsupportedOperationException();
}
@Override
void deleteThreads(@NonNull Set<Long> threadIds) {
Log.d(TAG, "deleteThreads(count: " + threadIds.size() + ")");
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = "";
Cursor cursor = null;
@@ -1597,10 +1633,29 @@ public class MmsDatabase extends MessageDatabase {
db.delete(TABLE_NAME, where, null);
}
@Override
public List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit) {
String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " +
TABLE_NAME + "." + getDateReceivedColumnName() + " >= ?";
String[] args = SqlUtil.buildArgs(threadId, timestamp);
try (Reader reader = readerFor(rawQuery(where, args, false, limit))) {
List<MessageRecord> results = new ArrayList<>(reader.cursor.getCount());
while (reader.getNext() != null) {
results.add(reader.getCurrent());
}
return results;
}
}
@Override
public void deleteAllThreads() {
Log.d(TAG, "deleteAllThreads()");
DatabaseFactory.getAttachmentDatabase(context).deleteAllAttachments();
DatabaseFactory.getGroupReceiptDatabase(context).deleteAllRows();
DatabaseFactory.getMentionDatabase(context).deleteAllMentions();
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, null, null);

View File

@@ -32,13 +32,17 @@ public interface MmsSmsColumns {
// Base Types
protected static final long BASE_TYPE_MASK = 0x1F;
protected static final long INCOMING_CALL_TYPE = 1;
protected static final long OUTGOING_CALL_TYPE = 2;
protected static final long MISSED_CALL_TYPE = 3;
protected static final long INCOMING_AUDIO_CALL_TYPE = 1;
protected static final long OUTGOING_AUDIO_CALL_TYPE = 2;
protected static final long MISSED_AUDIO_CALL_TYPE = 3;
protected static final long JOINED_TYPE = 4;
protected static final long UNSUPPORTED_MESSAGE_TYPE = 5;
protected static final long INVALID_MESSAGE_TYPE = 6;
protected static final long PROFILE_CHANGE_TYPE = 7;
protected static final long MISSED_VIDEO_CALL_TYPE = 8;
protected static final long GV1_MIGRATION_TYPE = 9;
protected static final long INCOMING_VIDEO_CALL_TYPE = 10;
protected static final long OUTGOING_VIDEO_CALL_TYPE = 11;
protected static final long BASE_INBOX_TYPE = 20;
protected static final long BASE_OUTBOX_TYPE = 21;
@@ -53,7 +57,7 @@ public interface MmsSmsColumns {
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
BASE_PENDING_SECURE_SMS_FALLBACK,
BASE_PENDING_INSECURE_SMS_FALLBACK,
OUTGOING_CALL_TYPE};
OUTGOING_AUDIO_CALL_TYPE, OUTGOING_VIDEO_CALL_TYPE};
// Message attributes
protected static final long MESSAGE_ATTRIBUTE_MASK = 0xE0;
@@ -204,23 +208,41 @@ public interface MmsSmsColumns {
}
public static boolean isCallLog(long type) {
return type == INCOMING_CALL_TYPE || type == OUTGOING_CALL_TYPE || type == MISSED_CALL_TYPE;
return isIncomingAudioCall(type) ||
isIncomingVideoCall(type) ||
isOutgoingAudioCall(type) ||
isOutgoingVideoCall(type) ||
isMissedAudioCall(type) ||
isMissedVideoCall(type);
}
public static boolean isExpirationTimerUpdate(long type) {
return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0;
}
public static boolean isIncomingCall(long type) {
return type == INCOMING_CALL_TYPE;
public static boolean isIncomingAudioCall(long type) {
return type == INCOMING_AUDIO_CALL_TYPE;
}
public static boolean isOutgoingCall(long type) {
return type == OUTGOING_CALL_TYPE;
public static boolean isIncomingVideoCall(long type) {
return type == INCOMING_VIDEO_CALL_TYPE;
}
public static boolean isMissedCall(long type) {
return type == MISSED_CALL_TYPE;
public static boolean isOutgoingAudioCall(long type) {
return type == OUTGOING_AUDIO_CALL_TYPE;
}
public static boolean isOutgoingVideoCall(long type) {
return type == OUTGOING_VIDEO_CALL_TYPE;
}
public static boolean isMissedAudioCall(long type) {
return type == MISSED_AUDIO_CALL_TYPE;
}
public static boolean isMissedVideoCall(long type) {
return type == MISSED_VIDEO_CALL_TYPE;
}
public static boolean isGroupUpdate(long type) {
@@ -260,6 +282,10 @@ public interface MmsSmsColumns {
return type == PROFILE_CHANGE_TYPE;
}
public static boolean isGroupV1MigrationEvent(long type) {
return type == GV1_MIGRATION_TYPE;
}
public static long translateFromSystemBaseType(long theirType) {
// public static final int NONE_TYPE = 0;
// public static final int INBOX_TYPE = 1;

View File

@@ -22,18 +22,23 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteQueryBuilder;
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.util.Pair;
import java.io.Closeable;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class MmsSmsDatabase extends Database {
@@ -147,8 +152,8 @@ public class MmsSmsDatabase extends Database {
MessageRecord messageRecord;
while ((messageRecord = reader.getNext()) != null) {
if ((Recipient.resolved(author).isLocalNumber() && messageRecord.isOutgoing()) ||
(!Recipient.resolved(author).isLocalNumber() && messageRecord.getIndividualRecipient().getId().equals(author)))
if ((Recipient.resolved(author).isSelf() && messageRecord.isOutgoing()) ||
(!Recipient.resolved(author).isSelf() && messageRecord.getIndividualRecipient().getId().equals(author)))
{
return messageRecord;
}
@@ -158,6 +163,18 @@ public class MmsSmsDatabase extends Database {
return null;
}
public @NonNull List<MessageRecord> getMessagesAfterVoiceNoteInclusive(long messageId, long limit) throws NoSuchMessageException {
MessageRecord origin = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
List<MessageRecord> mms = DatabaseFactory.getMmsDatabase(context).getMessagesInThreadAfterInclusive(origin.getThreadId(), origin.getDateReceived(), limit);
List<MessageRecord> sms = DatabaseFactory.getSmsDatabase(context).getMessagesInThreadAfterInclusive(origin.getThreadId(), origin.getDateReceived(), limit);
mms.addAll(sms);
Collections.sort(mms, (a, b) -> Long.compare(a.getDateReceived(), b.getDateReceived()));
return Stream.of(mms).limit(limit).toList();
}
public Cursor getConversation(long threadId, long offset, long limit) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
@@ -314,10 +331,10 @@ public class MmsSmsDatabase extends Database {
public int getQuotedMessagePosition(long threadId, long quoteId, @NonNull RecipientId recipientId) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.REMOTE_DELETED + " = 0";
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.RECIPIENT_ID}, selection, order, null)) {
boolean isOwnNumber = Recipient.resolved(recipientId).isLocalNumber();
boolean isOwnNumber = Recipient.resolved(recipientId).isSelf();
while (cursor != null && cursor.moveToNext()) {
boolean quoteIdMatches = cursor.getLong(0) == quoteId;
@@ -333,10 +350,10 @@ public class MmsSmsDatabase extends Database {
public int getMessagePositionInConversation(long threadId, long receivedTimestamp, @NonNull RecipientId recipientId) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.REMOTE_DELETED + " = 0";
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.RECIPIENT_ID}, selection, order, null)) {
boolean isOwnNumber = Recipient.resolved(recipientId).isLocalNumber();
boolean isOwnNumber = Recipient.resolved(recipientId).isSelf();
while (cursor != null && cursor.moveToNext()) {
boolean timestampMatches = cursor.getLong(0) == receivedTimestamp;
@@ -364,7 +381,9 @@ public class MmsSmsDatabase extends Database {
*/
public int getMessagePositionInConversation(long threadId, long receivedTimestamp) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + receivedTimestamp;
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " +
MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + receivedTimestamp + " AND " +
MmsSmsColumns.REMOTE_DELETED + " = 0";
try (Cursor cursor = queryTables(new String[]{ "COUNT(*)" }, selection, order, null)) {
if (cursor != null && cursor.moveToFirst()) {
@@ -388,11 +407,13 @@ public class MmsSmsDatabase extends Database {
}
public void deleteMessagesInThreadBeforeDate(long threadId, long trimBeforeDate) {
Log.d(TAG, "deleteMessagesInThreadBeforeData(" + threadId + ", " + trimBeforeDate + ")");
DatabaseFactory.getSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate);
DatabaseFactory.getMmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate);
}
public void deleteAbandonedMessages() {
Log.d(TAG, "deleteAbandonedMessages()");
DatabaseFactory.getSmsDatabase(context).deleteAbandonedMessages();
DatabaseFactory.getMmsDatabase(context).deleteAbandonedMessages();
}

View File

@@ -15,7 +15,6 @@ import net.sqlcipher.database.SQLiteConstraintException;
import net.sqlcipher.database.SQLiteDatabase;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
@@ -23,6 +22,7 @@ import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -31,17 +31,20 @@ import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.RecordUpdate;
import org.thoughtcrime.securesms.storage.StorageSyncModels;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Bitmask;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.StringUtil;
@@ -62,6 +65,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -113,8 +117,7 @@ public class RecipientDatabase extends Database {
private static final String LAST_PROFILE_FETCH = "last_profile_fetch";
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
private static final String UUID_CAPABILITY = "uuid_supported";
private static final String GROUPS_V2_CAPABILITY = "gv2_capability";
private static final String CAPABILITIES = "capabilities";
private static final String STORAGE_SERVICE_ID = "storage_service_key";
private static final String DIRTY = "dirty";
private static final String PROFILE_GIVEN_NAME = "signal_profile_name";
@@ -128,8 +131,15 @@ public class RecipientDatabase extends Database {
private static final String IDENTITY_STATUS = "identity_status";
private static final String IDENTITY_KEY = "identity_key";
private static final class Capabilities {
static final int BIT_LENGTH = 2;
static final int GROUPS_V2 = 0;
static final int GROUPS_V1_MIGRATION = 1;
}
private static final String[] RECIPIENT_PROJECTION = new String[] {
UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
ID, UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
@@ -137,7 +147,7 @@ public class RecipientDatabase extends Database {
NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION,
UUID_CAPABILITY, GROUPS_V2_CAPABILITY,
CAPABILITIES,
STORAGE_SERVICE_ID, DIRTY,
MENTION_SETTING
};
@@ -145,20 +155,13 @@ public class RecipientDatabase extends Database {
private static final String[] ID_PROJECTION = new String[]{ID};
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME};
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
private static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
.map(columnName -> TABLE_NAME + "." + columnName)
.toList().toArray(new String[0]);
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
static final String[] TYPED_RECIPIENT_PROJECTION_NO_ID = Arrays.copyOfRange(TYPED_RECIPIENT_PROJECTION, 1, TYPED_RECIPIENT_PROJECTION.length);
private static final String[] RECIPIENT_FULL_PROJECTION = Stream.of(
new String[] { TABLE_NAME + "." + ID,
TABLE_NAME + "." + STORAGE_PROTO },
TYPED_RECIPIENT_PROJECTION,
new String[] {
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY
}).flatMap(Stream::of).toArray(String[]::new);
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
public static final String[] CREATE_INDEXS = new String[] {
"CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
@@ -335,12 +338,11 @@ public class RecipientDatabase extends Database {
LAST_PROFILE_FETCH + " INTEGER DEFAULT 0, " +
UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " +
FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " +
UUID_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
GROUPS_V2_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " +
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " +
MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() + ", " +
STORAGE_PROTO + " TEXT DEFAULT NULL);";
STORAGE_PROTO + " TEXT DEFAULT NULL, " +
CAPABILITIES + " INTEGER DEFAULT 0);";
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
" FROM " + TABLE_NAME +
@@ -376,8 +378,8 @@ public class RecipientDatabase extends Database {
return getByColumn(EMAIL, email);
}
public @NonNull Optional<RecipientId> getByGroupId(@NonNull String groupId) {
return getByColumn(GROUP_ID, groupId);
public @NonNull Optional<RecipientId> getByGroupId(@NonNull GroupId groupId) {
return getByColumn(GROUP_ID, groupId.toString());
}
@@ -509,6 +511,7 @@ public class RecipientDatabase extends Database {
if (transactionSuccessful) {
if (recipientNeedingRefresh != null) {
Recipient.live(recipientNeedingRefresh).refresh();
RetrieveProfileJob.enqueue(recipientNeedingRefresh);
}
if (remapped != null) {
@@ -552,27 +555,94 @@ public class RecipientDatabase extends Database {
}
public @NonNull RecipientId getOrInsertFromGroupId(@NonNull GroupId groupId) {
GetOrInsertResult result = getOrInsertByColumn(GROUP_ID, groupId.toString());
Optional<RecipientId> existing = getByGroupId(groupId);
if (result.neededInsert) {
if (existing.isPresent()) {
return existing.get();
} else if (groupId.isV1() && DatabaseFactory.getGroupDatabase(context).groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
throw new GroupDatabase.LegacyGroupInsertException(groupId);
} else if (groupId.isV2() && DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2()).isPresent()) {
throw new GroupDatabase.MissedGroupMigrationInsertException(groupId);
} else {
ContentValues values = new ContentValues();
values.put(GROUP_ID, groupId.toString());
if (groupId.isMms()) {
values.put(GROUP_TYPE, GroupType.MMS.getId());
} else {
if (groupId.isV2()) {
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
long id = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
if (id < 0) {
existing = getByColumn(GROUP_ID, groupId.toString());
if (existing.isPresent()) {
return existing.get();
} else if (groupId.isV1() && DatabaseFactory.getGroupDatabase(context).groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
throw new GroupDatabase.LegacyGroupInsertException(groupId);
} else if (groupId.isV2() && DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2()).isPresent()) {
throw new GroupDatabase.MissedGroupMigrationInsertException(groupId);
} else {
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
throw new AssertionError("Failed to insert recipient!");
}
values.put(DIRTY, DirtyState.INSERT.getId());
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
} else {
ContentValues groupUpdates = new ContentValues();
if (groupId.isMms()) {
groupUpdates.put(GROUP_TYPE, GroupType.MMS.getId());
} else {
if (groupId.isV2()) {
groupUpdates.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
} else {
groupUpdates.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
}
groupUpdates.put(DIRTY, DirtyState.INSERT.getId());
groupUpdates.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
}
RecipientId recipientId = RecipientId.from(id);
update(recipientId, groupUpdates);
return recipientId;
}
}
}
/**
* See {@link Recipient#externalPossiblyMigratedGroup(Context, GroupId)}.
*/
public @NonNull RecipientId getOrInsertFromPossiblyMigratedGroupId(@NonNull GroupId groupId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
Optional<RecipientId> existing = getByColumn(GROUP_ID, groupId.toString());
if (existing.isPresent()) {
db.setTransactionSuccessful();
return existing.get();
}
update(result.recipientId, values);
}
if (groupId.isV1()) {
Optional<RecipientId> v2 = getByGroupId(groupId.requireV1().deriveV2MigrationGroupId());
if (v2.isPresent()) {
db.setTransactionSuccessful();
return v2.get();
}
}
return result.recipientId;
if (groupId.isV2()) {
Optional<GroupDatabase.GroupRecord> v1 = DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2());
if (v1.isPresent()) {
db.setTransactionSuccessful();
return v1.get().getRecipientId();
}
}
RecipientId id = getOrInsertFromGroupId(groupId);
db.setTransactionSuccessful();
return id;
} finally {
db.endTransaction();
}
}
public Cursor getBlocked() {
@@ -596,11 +666,10 @@ public class RecipientDatabase extends Database {
public @NonNull RecipientSettings getRecipientSettings(@NonNull RecipientId id) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID;
String query = TABLE_NAME + "." + ID + " = ?";
String query = ID + " = ?";
String[] args = new String[] { id.serialize() };
try (Cursor cursor = database.query(table, RECIPIENT_FULL_PROJECTION, query, args, null, null, null)) {
try (Cursor cursor = database.query(TABLE_NAME, RECIPIENT_PROJECTION, query, args, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
return getRecipientSettings(context, cursor);
} else {
@@ -675,6 +744,20 @@ public class RecipientDatabase extends Database {
return null;
}
public void markNeedsSync(@NonNull Collection<RecipientId> recipientIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
for (RecipientId recipientId : recipientIds) {
markDirty(recipientId, DirtyState.UPDATE);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void markNeedsSync(@NonNull RecipientId recipientId) {
markDirty(recipientId, DirtyState.UPDATE);
}
@@ -697,6 +780,10 @@ public class RecipientDatabase extends Database {
} finally {
db.endTransaction();
}
for (RecipientId id : storageIds.keySet()) {
Recipient.live(id).refresh();
}
}
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
@@ -771,7 +858,7 @@ public class RecipientDatabase extends Database {
}
}
threadDatabase.setArchived(recipientId, insert.isArchived());
threadDatabase.applyStorageSyncUpdate(recipientId, insert);
needsRefresh.add(recipientId);
}
@@ -811,12 +898,12 @@ public class RecipientDatabase extends Database {
Optional<IdentityRecord> newIdentityRecord = identityDatabase.getIdentity(recipientId);
if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED) &&
(!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED))
if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) &&
(!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED))
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true);
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED) &&
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED))
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED) &&
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED))
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true);
}
@@ -824,16 +911,16 @@ public class RecipientDatabase extends Database {
Log.w(TAG, "Failed to process identity key during update! Skipping.", e);
}
threadDatabase.setArchived(recipientId, update.getNew().isArchived());
threadDatabase.applyStorageSyncUpdate(recipientId, update.getNew());
needsRefresh.add(recipientId);
}
for (SignalGroupV1Record insert : groupV1Inserts) {
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(insert.getGroupId()));
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(insert.getGroupId()));
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
needsRefresh.add(recipient.getId());
}
@@ -845,21 +932,26 @@ public class RecipientDatabase extends Database {
throw new AssertionError("Had an update, but it didn't match any rows!");
}
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(update.getOld().getGroupId()));
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(update.getOld().getGroupId()));
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
needsRefresh.add(recipient.getId());
}
for (SignalGroupV2Record insert : groupV2Inserts) {
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV2(insert));
GroupMasterKey masterKey = insert.getMasterKeyOrThrow();
GroupId.V2 groupId = GroupId.v2(masterKey);
Recipient recipient = Recipient.externalGroup(context, groupId);
ContentValues values = getValuesForStorageGroupV2(insert);
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
Recipient recipient = Recipient.externalGroupExact(context, groupId);
if (id < 0) {
Log.w(TAG, String.format("Recipient %s is already linked to group %s", recipient.getId(), groupId));
} else {
Log.i(TAG, String.format("Inserted recipient %s for group %s", recipient.getId(), groupId));
}
Log.i(TAG, "Creating restore placeholder for " + groupId);
DatabaseFactory.getGroupDatabase(context)
.create(masterKey,
DecryptedGroup.newBuilder()
@@ -870,7 +962,7 @@ public class RecipientDatabase extends Database {
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId));
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
needsRefresh.add(recipient.getId());
}
@@ -883,9 +975,9 @@ public class RecipientDatabase extends Database {
}
GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow();
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(masterKey));
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey));
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
needsRefresh.add(recipient.getId());
}
@@ -934,6 +1026,8 @@ public class RecipientDatabase extends Database {
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
}
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(Recipient.self().getId(), update);
Recipient.self().live().refresh();
}
@@ -1046,14 +1140,22 @@ public class RecipientDatabase extends Database {
private List<RecipientSettings> getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID;
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID
+ " LEFT OUTER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
List<RecipientSettings> out = new ArrayList<>();
String[] columns = Stream.of(RECIPIENT_FULL_PROJECTION,
new String[]{GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY }).flatMap(Stream::of).toArray(String[]::new);
String[] columns = Stream.of(TYPED_RECIPIENT_PROJECTION,
new String[]{ RecipientDatabase.TABLE_NAME + "." + STORAGE_PROTO,
GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY,
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ARCHIVED,
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.READ,
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY })
.flatMap(Stream::of)
.toArray(String[]::new);
try (Cursor cursor = db.query(table, columns, query, args, null, null, null)) {
try (Cursor cursor = db.query(table, columns, query, args, TABLE_NAME + "." + ID, null, null)) {
while (cursor != null && cursor.moveToNext()) {
out.add(getRecipientSettings(context, cursor));
}
@@ -1094,7 +1196,7 @@ public class RecipientDatabase extends Database {
}
for (GroupId.V2 id : DatabaseFactory.getGroupDatabase(context).getAllGroupV2Ids()) {
Recipient recipient = Recipient.externalGroup(context, id);
Recipient recipient = Recipient.externalGroupExact(context, id);
RecipientId recipientId = recipient.getId();
RecipientSettings recipientSettingsForSync = getRecipientSettingsForSync(recipientId);
@@ -1147,27 +1249,9 @@ public class RecipientDatabase extends Database {
String notificationChannel = CursorUtil.requireString(cursor, NOTIFICATION_CHANNEL);
int unidentifiedAccessMode = CursorUtil.requireInt(cursor, UNIDENTIFIED_ACCESS_MODE);
boolean forceSmsSelection = CursorUtil.requireBoolean(cursor, FORCE_SMS_SELECTION);
int uuidCapabilityValue = CursorUtil.requireInt(cursor, UUID_CAPABILITY);
int groupsV2CapabilityValue = CursorUtil.requireInt(cursor, GROUPS_V2_CAPABILITY);
long capabilities = CursorUtil.requireLong(cursor, CAPABILITIES);
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
String storageProtoRaw = CursorUtil.getString(cursor, STORAGE_PROTO).orNull();
Optional<String> identityKeyRaw = CursorUtil.getString(cursor, IDENTITY_KEY);
Optional<Integer> identityStatusRaw = CursorUtil.getInt(cursor, IDENTITY_STATUS);
int masterKeyIndex = cursor.getColumnIndex(GroupDatabase.V2_MASTER_KEY);
GroupMasterKey groupMasterKey = null;
try {
if (masterKeyIndex != -1) {
byte[] blob = cursor.getBlob(masterKeyIndex);
if (blob != null) {
groupMasterKey = new GroupMasterKey(blob);
}
}
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
MaterialColor color;
byte[] profileKey = null;
@@ -1198,30 +1282,57 @@ public class RecipientDatabase extends Database {
}
}
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
byte[] identityKey = identityKeyRaw.transform(Base64::decodeOrThrow).orNull();
byte[] storageProto = storageProtoRaw != null ? Base64.decodeOrThrow(storageProtoRaw) : null;
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
IdentityDatabase.VerifiedStatus identityStatus = identityStatusRaw.transform(IdentityDatabase.VerifiedStatus::forState).or(IdentityDatabase.VerifiedStatus.DEFAULT);
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, groupMasterKey, GroupType.fromId(groupType), blocked, muteUntil,
return new RecipientSettings(RecipientId.from(id),
uuid,
username,
e164,
email,
groupId,
GroupType.fromId(groupType),
blocked,
muteUntil,
VibrateState.fromId(messageVibrateState),
VibrateState.fromId(callVibrateState),
Util.uri(messageRingtone), Util.uri(callRingtone),
color, defaultSubscriptionId, expireMessages,
Util.uri(messageRingtone),
Util.uri(callRingtone),
color,
defaultSubscriptionId,
expireMessages,
RegisteredState.fromId(registeredState),
profileKey, profileKeyCredential,
systemDisplayName, systemContactPhoto,
systemPhoneLabel, systemContactUri,
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar,
AvatarHelper.hasAvatar(context, RecipientId.from(id)), profileSharing, lastProfileFetch,
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
profileKey,
profileKeyCredential,
systemDisplayName,
systemContactPhoto,
systemPhoneLabel,
systemContactUri,
ProfileName.fromParts(profileGivenName, profileFamilyName),
signalProfileAvatar,
AvatarHelper.hasAvatar(context, RecipientId.from(id)),
profileSharing,
lastProfileFetch,
notificationChannel,
UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
forceSmsSelection,
Recipient.Capability.deserialize(uuidCapabilityValue),
Recipient.Capability.deserialize(groupsV2CapabilityValue),
capabilities,
InsightsBannerTier.fromId(insightsBannerTier),
storageKey, identityKey, identityStatus, MentionSetting.fromId(mentionSettingId),
storageProto);
storageKey,
MentionSetting.fromId(mentionSettingId),
getSyncExtras(cursor));
}
private static @NonNull RecipientSettings.SyncExtras getSyncExtras(@NonNull Cursor cursor) {
String storageProtoRaw = CursorUtil.getString(cursor, STORAGE_PROTO).orNull();
byte[] storageProto = storageProtoRaw != null ? Base64.decodeOrThrow(storageProtoRaw) : null;
boolean archived = CursorUtil.getBoolean(cursor, ThreadDatabase.ARCHIVED).or(false);
boolean forcedUnread = CursorUtil.getInt(cursor, ThreadDatabase.READ).transform(status -> status == ThreadDatabase.ReadStatus.FORCED_UNREAD.serialize()).or(false);
GroupMasterKey groupMasterKey = CursorUtil.getBlob(cursor, GroupDatabase.V2_MASTER_KEY).transform(GroupUtil::requireMasterKey).orNull();
byte[] identityKey = CursorUtil.getString(cursor, IDENTITY_KEY).transform(Base64::decodeOrThrow).orNull();
VerifiedStatus identityStatus = CursorUtil.getInt(cursor, IDENTITY_STATUS).transform(VerifiedStatus::forState).or(VerifiedStatus.DEFAULT);
return new RecipientSettings.SyncExtras(storageProto, groupMasterKey, identityKey, identityStatus, archived, forcedUnread);
}
public BulkOperationsHandle beginBulkSystemContactUpdate() {
@@ -1367,9 +1478,14 @@ public class RecipientDatabase extends Database {
}
public void setCapabilities(@NonNull RecipientId id, @NonNull SignalServiceProfile.Capabilities capabilities) {
ContentValues values = new ContentValues(2);
values.put(UUID_CAPABILITY, Recipient.Capability.fromBoolean(capabilities.isUuid()).serialize());
values.put(GROUPS_V2_CAPABILITY, Recipient.Capability.fromBoolean(capabilities.isGv2()).serialize());
long value = 0;
value = Bitmask.update(value, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv2()).serialize());
value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration()).serialize());
ContentValues values = new ContentValues(1);
values.put(CAPABILITIES, value);
if (update(id, values)) {
Recipient.live(id).refresh();
}
@@ -1402,7 +1518,7 @@ public class RecipientDatabase extends Database {
valuesToSet.putNull(PROFILE_KEY_CREDENTIAL);
valuesToSet.put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.getMode());
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare);
SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare);
if (update(updateQuery, valuesToSet)) {
markDirty(id, DirtyState.UPDATE);
@@ -1451,7 +1567,7 @@ public class RecipientDatabase extends Database {
values.put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(profileKeyCredential.serialize()));
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
if (update(updateQuery, values)) {
// TODO [greyson] If we sync this in future, mark dirty
@@ -1519,6 +1635,27 @@ public class RecipientDatabase extends Database {
return updated;
}
public @NonNull List<RecipientId> getSimilarRecipientIds(@NonNull Recipient recipient) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String[] projection = SqlUtil.buildArgs(ID, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ") AS checked_name");
String where = "checked_name = ?";
String[] arguments = SqlUtil.buildArgs(recipient.getProfileName().toString());
try (Cursor cursor = db.query(TABLE_NAME, projection, where, arguments, null, null, null)) {
if (cursor == null || cursor.getCount() == 0) {
return Collections.emptyList();
}
List<RecipientId> results = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
results.add(RecipientId.from(CursorUtil.requireLong(cursor, ID)));
}
return results;
}
}
public void setProfileName(@NonNull RecipientId id, @NonNull ProfileName profileName) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
@@ -2086,7 +2223,7 @@ public class RecipientDatabase extends Database {
}
return Stream.of(recipientsWithinInteractionThreshold)
.filterNot(Recipient::isLocalNumber)
.filterNot(Recipient::isSelf)
.filter(r -> r.getLastProfileFetchTime() < lastProfileFetchThreshold)
.limit(limit)
.map(Recipient::getId)
@@ -2168,6 +2305,10 @@ public class RecipientDatabase extends Database {
} finally {
db.endTransaction();
}
for (RecipientId id : keys.keySet()) {
Recipient.live(id).refresh();
}
}
public void clearDirtyState(@NonNull List<RecipientId> recipients) {
@@ -2221,14 +2362,30 @@ public class RecipientDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, query, args);
}
/**
* Updates a group recipient with a new V2 group ID. Should only be done as a part of GV1->GV2
* migration.
*/
void updateGroupId(@NonNull GroupId.V1 v1Id, @NonNull GroupId.V2 v2Id) {
ContentValues values = new ContentValues();
values.put(GROUP_ID, v2Id.toString());
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
SqlUtil.Query query = SqlUtil.buildTrueUpdateQuery(GROUP_ID + " = ?", SqlUtil.buildArgs(v1Id), values);
if (update(query, values)) {
RecipientId id = getByGroupId(v2Id).get();
markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
}
}
/**
* Will update the database with the content values you specified. It will make an intelligent
* query such that this will only return true if a row was *actually* updated.
*/
private boolean update(@NonNull RecipientId id, @NonNull ContentValues contentValues) {
String selection = ID + " = ?";
String[] args = new String[]{id.serialize()};
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, contentValues);
SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(id), contentValues);
return update(updateQuery, contentValues);
}
@@ -2238,7 +2395,7 @@ public class RecipientDatabase extends Database {
* <p>
* This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}.
*/
private boolean update(@NonNull SqlUtil.UpdateQuery updateQuery, @NonNull ContentValues contentValues) {
private boolean update(@NonNull SqlUtil.Query updateQuery, @NonNull ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
return database.update(TABLE_NAME, contentValues, updateQuery.getWhere(), updateQuery.getWhereArgs()) > 0;
@@ -2333,7 +2490,7 @@ public class RecipientDatabase extends Database {
uuidValues.put(SYSTEM_PHONE_LABEL, e164Settings.getSystemPhoneLabel());
uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri());
uuidValues.put(PROFILE_SHARING, uuidSettings.isProfileSharing() || e164Settings.isProfileSharing());
uuidValues.put(GROUPS_V2_CAPABILITY, uuidSettings.getGroupsV2Capability() != Recipient.Capability.UNKNOWN ? uuidSettings.getGroupsV2Capability().serialize() : e164Settings.getGroupsV2Capability().serialize());
uuidValues.put(CAPABILITIES, Math.max(uuidSettings.getCapabilities(), e164Settings.getCapabilities()));
uuidValues.put(MENTION_SETTING, uuidSettings.getMentionSetting() != MentionSetting.ALWAYS_NOTIFY ? uuidSettings.getMentionSetting().getId() : e164Settings.getMentionSetting().getId());
if (uuidSettings.getProfileKey() != null) {
updateProfileValuesForMerge(uuidValues, uuidSettings);
@@ -2525,7 +2682,6 @@ public class RecipientDatabase extends Database {
private final String e164;
private final String email;
private final GroupId groupId;
private final GroupMasterKey groupMasterKey;
private final GroupType groupType;
private final boolean blocked;
private final long muteUntil;
@@ -2551,14 +2707,13 @@ public class RecipientDatabase extends Database {
private final String notificationChannel;
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final boolean forceSmsSelection;
private final Recipient.Capability uuidCapability;
private final long capabilities;
private final Recipient.Capability groupsV2Capability;
private final Recipient.Capability groupsV1MigrationCapability;
private final InsightsBannerTier insightsBannerTier;
private final byte[] storageId;
private final byte[] identityKey;
private final IdentityDatabase.VerifiedStatus identityStatus;
private final MentionSetting mentionSetting;
private final byte[] storageProto;
private final SyncExtras syncExtras;
RecipientSettings(@NonNull RecipientId id,
@Nullable UUID uuid,
@@ -2566,7 +2721,6 @@ public class RecipientDatabase extends Database {
@Nullable String e164,
@Nullable String email,
@Nullable GroupId groupId,
@Nullable GroupMasterKey groupMasterKey,
@NonNull GroupType groupType,
boolean blocked,
long muteUntil,
@@ -2592,55 +2746,50 @@ public class RecipientDatabase extends Database {
@Nullable String notificationChannel,
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
boolean forceSmsSelection,
Recipient.Capability uuidCapability,
Recipient.Capability groupsV2Capability,
long capabilities,
@NonNull InsightsBannerTier insightsBannerTier,
@Nullable byte[] storageId,
@Nullable byte[] identityKey,
@NonNull IdentityDatabase.VerifiedStatus identityStatus,
@NonNull MentionSetting mentionSetting,
@Nullable byte[] storageProto)
@NonNull SyncExtras syncExtras)
{
this.id = id;
this.uuid = uuid;
this.username = username;
this.e164 = e164;
this.email = email;
this.groupId = groupId;
this.groupMasterKey = groupMasterKey;
this.groupType = groupType;
this.blocked = blocked;
this.muteUntil = muteUntil;
this.messageVibrateState = messageVibrateState;
this.callVibrateState = callVibrateState;
this.messageRingtone = messageRingtone;
this.callRingtone = callRingtone;
this.color = color;
this.defaultSubscriptionId = defaultSubscriptionId;
this.expireMessages = expireMessages;
this.registered = registered;
this.profileKey = profileKey;
this.profileKeyCredential = profileKeyCredential;
this.systemDisplayName = systemDisplayName;
this.systemContactPhoto = systemContactPhoto;
this.systemPhoneLabel = systemPhoneLabel;
this.systemContactUri = systemContactUri;
this.signalProfileName = signalProfileName;
this.signalProfileAvatar = signalProfileAvatar;
this.hasProfileImage = hasProfileImage;
this.profileSharing = profileSharing;
this.lastProfileFetch = lastProfileFetch;
this.notificationChannel = notificationChannel;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.forceSmsSelection = forceSmsSelection;
this.uuidCapability = uuidCapability;
this.groupsV2Capability = groupsV2Capability;
this.insightsBannerTier = insightsBannerTier;
this.storageId = storageId;
this.identityKey = identityKey;
this.identityStatus = identityStatus;
this.mentionSetting = mentionSetting;
this.storageProto = storageProto;
this.id = id;
this.uuid = uuid;
this.username = username;
this.e164 = e164;
this.email = email;
this.groupId = groupId;
this.groupType = groupType;
this.blocked = blocked;
this.muteUntil = muteUntil;
this.messageVibrateState = messageVibrateState;
this.callVibrateState = callVibrateState;
this.messageRingtone = messageRingtone;
this.callRingtone = callRingtone;
this.color = color;
this.defaultSubscriptionId = defaultSubscriptionId;
this.expireMessages = expireMessages;
this.registered = registered;
this.profileKey = profileKey;
this.profileKeyCredential = profileKeyCredential;
this.systemDisplayName = systemDisplayName;
this.systemContactPhoto = systemContactPhoto;
this.systemPhoneLabel = systemPhoneLabel;
this.systemContactUri = systemContactUri;
this.signalProfileName = signalProfileName;
this.signalProfileAvatar = signalProfileAvatar;
this.hasProfileImage = hasProfileImage;
this.profileSharing = profileSharing;
this.lastProfileFetch = lastProfileFetch;
this.notificationChannel = notificationChannel;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.forceSmsSelection = forceSmsSelection;
this.capabilities = capabilities;
this.groupsV2Capability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH));
this.groupsV1MigrationCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH));
this.insightsBannerTier = insightsBannerTier;
this.storageId = storageId;
this.mentionSetting = mentionSetting;
this.syncExtras = syncExtras;
}
public RecipientId getId() {
@@ -2667,13 +2816,6 @@ public class RecipientDatabase extends Database {
return groupId;
}
/**
* Only read populated for sync.
*/
public @Nullable GroupMasterKey getGroupMasterKey() {
return groupMasterKey;
}
public @NonNull GroupType getGroupType() {
return groupType;
}
@@ -2778,32 +2920,80 @@ public class RecipientDatabase extends Database {
return forceSmsSelection;
}
public Recipient.Capability getUuidCapability() {
return uuidCapability;
public @NonNull Recipient.Capability getGroupsV2Capability() {
return groupsV2Capability;
}
public Recipient.Capability getGroupsV2Capability() {
return groupsV2Capability;
public @NonNull Recipient.Capability getGroupsV1MigrationCapability() {
return groupsV1MigrationCapability;
}
public @Nullable byte[] getStorageId() {
return storageId;
}
public @Nullable byte[] getIdentityKey() {
return identityKey;
}
public @NonNull IdentityDatabase.VerifiedStatus getIdentityStatus() {
return identityStatus;
}
public @NonNull MentionSetting getMentionSetting() {
return mentionSetting;
}
public @Nullable byte[] getStorageProto() {
return storageProto;
public @NonNull SyncExtras getSyncExtras() {
return syncExtras;
}
long getCapabilities() {
return capabilities;
}
/**
* A bundle of data that's only necessary when syncing to storage service, not for a
* {@link Recipient}.
*/
public static class SyncExtras {
private final byte[] storageProto;
private final GroupMasterKey groupMasterKey;
private final byte[] identityKey;
private final VerifiedStatus identityStatus;
private final boolean archived;
private final boolean forcedUnread;
public SyncExtras(@Nullable byte[] storageProto,
@Nullable GroupMasterKey groupMasterKey,
@Nullable byte[] identityKey,
@NonNull VerifiedStatus identityStatus,
boolean archived,
boolean forcedUnread)
{
this.storageProto = storageProto;
this.groupMasterKey = groupMasterKey;
this.identityKey = identityKey;
this.identityStatus = identityStatus;
this.archived = archived;
this.forcedUnread = forcedUnread;
}
public @Nullable byte[] getStorageProto() {
return storageProto;
}
public @Nullable GroupMasterKey getGroupMasterKey() {
return groupMasterKey;
}
public boolean isArchived() {
return archived;
}
public @Nullable byte[] getIdentityKey() {
return identityKey;
}
public @NonNull VerifiedStatus getIdentityStatus() {
return identityStatus;
}
public boolean isForcedUnread() {
return forcedUnread;
}
}
}

View File

@@ -58,8 +58,10 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.Closeable;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
@@ -362,8 +364,8 @@ public class SmsDatabase extends MessageDatabase {
}
@Override
public void markAsMissedCall(long id) {
updateTypeBitmask(id, Types.TOTAL_MASK, Types.MISSED_CALL_TYPE);
public void markAsMissedCall(long id, boolean isVideoOffer) {
updateTypeBitmask(id, Types.TOTAL_MASK, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE);
}
@Override
@@ -373,6 +375,7 @@ public class SmsDatabase extends MessageDatabase {
ContentValues values = new ContentValues();
values.put(REMOTE_DELETED, 1);
values.putNull(BODY);
values.putNull(REACTIONS);
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) });
long threadId = getThreadIdForMessage(id);
@@ -630,12 +633,14 @@ public class SmsDatabase extends MessageDatabase {
@Override
public boolean hasReceivedAnyCallsSince(long threadId, long timestamp) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String[] projection = new String[]{SmsDatabase.TYPE};
String selection = THREAD_ID + " = ? AND " + DATE_RECEIVED + " > ? AND (" + TYPE + " = ? OR " + TYPE + " = ?)";
String[] selectionArgs = new String[]{String.valueOf(threadId),
String.valueOf(timestamp),
String.valueOf(Types.INCOMING_CALL_TYPE),
String.valueOf(Types.MISSED_CALL_TYPE)};
String[] projection = SqlUtil.buildArgs(SmsDatabase.TYPE);
String selection = THREAD_ID + " = ? AND " + DATE_RECEIVED + " > ? AND (" + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " =?)";
String[] selectionArgs = SqlUtil.buildArgs(threadId,
timestamp,
Types.INCOMING_AUDIO_CALL_TYPE,
Types.INCOMING_VIDEO_CALL_TYPE,
Types.MISSED_AUDIO_CALL_TYPE,
Types.MISSED_VIDEO_CALL_TYPE);
try (Cursor cursor = db.query(TABLE_NAME, projection, selection, selectionArgs, null, null, null)) {
return cursor != null && cursor.moveToFirst();
@@ -643,18 +648,18 @@ public class SmsDatabase extends MessageDatabase {
}
@Override
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address) {
return insertCallLog(address, Types.INCOMING_CALL_TYPE, false, System.currentTimeMillis());
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) {
return insertCallLog(address, isVideoOffer ? Types.INCOMING_VIDEO_CALL_TYPE : Types.INCOMING_AUDIO_CALL_TYPE, false, System.currentTimeMillis());
}
@Override
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address) {
return insertCallLog(address, Types.OUTGOING_CALL_TYPE, false, System.currentTimeMillis());
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer) {
return insertCallLog(address, isVideoOffer ? Types.OUTGOING_VIDEO_CALL_TYPE : Types.OUTGOING_AUDIO_CALL_TYPE, false, System.currentTimeMillis());
}
@Override
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp) {
return insertCallLog(address, Types.MISSED_CALL_TYPE, true, timestamp);
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer) {
return insertCallLog(address, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE, true, timestamp);
}
private @NonNull Pair<Long, Long> insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) {
@@ -684,6 +689,21 @@ public class SmsDatabase extends MessageDatabase {
return new Pair<>(messageId, threadId);
}
@Override
public List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp) {
String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " >= ? AND " + TYPE + " = ?";
String[] args = SqlUtil.buildArgs(threadId, afterTimestamp, Types.PROFILE_CHANGE_TYPE);
try (Reader reader = readerFor(queryMessages(where, args, true, -1))) {
List<MessageRecord> results = new ArrayList<>(reader.getCount());
while (reader.getNext() != null) {
results.add(reader.getCurrent());
}
return results;
}
}
@Override
public void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
@@ -735,6 +755,39 @@ public class SmsDatabase extends MessageDatabase {
}
}
@Override
public void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, @NonNull List<RecipientId> pendingRecipients) {
insertGroupV1MigrationNotification(recipientId, threadId);
if (pendingRecipients.size() > 0) {
insertGroupV1MigrationEvent(recipientId, threadId, pendingRecipients);
}
notifyConversationListeners(threadId);
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
}
private void insertGroupV1MigrationNotification(@NonNull RecipientId recipientId, long threadId) {
insertGroupV1MigrationEvent(recipientId, threadId, Collections.emptyList());
}
private void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, @NonNull List<RecipientId> pendingRecipients) {
ContentValues values = new ContentValues();
values.put(RECIPIENT_ID, recipientId.serialize());
values.put(ADDRESS_DEVICE_ID, 1);
values.put(DATE_RECEIVED, System.currentTimeMillis());
values.put(DATE_SENT, System.currentTimeMillis());
values.put(READ, 1);
values.put(TYPE, Types.GV1_MIGRATION_TYPE);
values.put(THREAD_ID, threadId);
if (pendingRecipients.size() > 0) {
values.put(BODY, RecipientId.toSerializedList(pendingRecipients));
}
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
}
@Override
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
if (message.isJoined()) {
@@ -771,7 +824,7 @@ public class SmsDatabase extends MessageDatabase {
if (message.getGroupId() == null) {
groupRecipient = null;
} else {
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(message.getGroupId());
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(message.getGroupId());
groupRecipient = Recipient.resolved(id);
}
@@ -926,6 +979,8 @@ public class SmsDatabase extends MessageDatabase {
@Override
public boolean deleteMessage(long messageId) {
Log.d(TAG, "deleteMessage(" + messageId + ")");
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long threadId = getThreadIdForMessage(messageId);
@@ -962,6 +1017,7 @@ public class SmsDatabase extends MessageDatabase {
@Override
void deleteThread(long threadId) {
Log.d(TAG, "deleteThread(" + threadId + ")");
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
}
@@ -982,8 +1038,40 @@ public class SmsDatabase extends MessageDatabase {
db.delete(TABLE_NAME, where, null);
}
@Override
public List<MessageRecord> getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit) {
String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " +
TABLE_NAME + "." + getDateReceivedColumnName() + " >= ?";
String[] args = SqlUtil.buildArgs(threadId, timestamp);
try (Reader reader = readerFor(queryMessages(where, args, false, limit))) {
List<MessageRecord> results = new ArrayList<>(reader.cursor.getCount());
while (reader.getNext() != null) {
results.add(reader.getCurrent());
}
return results;
}
}
private Cursor queryMessages(@NonNull String where, @NonNull String[] args, boolean reverse, long limit) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME,
MESSAGE_PROJECTION,
where,
args,
null,
null,
reverse ? ID + " DESC" : null,
limit > 0 ? String.valueOf(limit) : null);
}
@Override
void deleteThreads(@NonNull Set<Long> threadIds) {
Log.d(TAG, "deleteThreads(count: " + threadIds.size() + ")");
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = "";
@@ -998,6 +1086,7 @@ public class SmsDatabase extends MessageDatabase {
@Override
void deleteAllThreads() {
Log.d(TAG, "deleteAllThreads()");
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
@@ -1184,7 +1273,7 @@ public class SmsDatabase extends MessageDatabase {
}
}
public static class Reader {
public static class Reader implements Closeable {
private final Cursor cursor;
private final Context context;
@@ -1255,6 +1344,7 @@ public class SmsDatabase extends MessageDatabase {
return new LinkedList<>();
}
@Override
public void close() {
cursor.close();
}

View File

@@ -32,8 +32,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import net.sqlcipher.database.SQLiteDatabase;
import org.jsoup.helper.StringUtil;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@@ -41,6 +41,8 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.groups.BadGroupIdException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -56,11 +58,14 @@ import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -70,7 +75,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
public class ThreadDatabase extends Database {
@@ -102,7 +106,7 @@ public class ThreadDatabase extends Database {
public static final String LAST_SEEN = "last_seen";
public static final String HAS_SENT = "has_sent";
private static final String LAST_SCROLLED = "last_scrolled";
private static final String PINNED = "pinned";
static final String PINNED = "pinned";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
DATE + " INTEGER DEFAULT 0, " +
@@ -144,7 +148,7 @@ public class ThreadDatabase extends Database {
.toList();
private static final List<String> COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION = Stream.concat(Stream.concat(Stream.of(TYPED_THREAD_PROJECTION),
Stream.of(RecipientDatabase.TYPED_RECIPIENT_PROJECTION)),
Stream.of(RecipientDatabase.TYPED_RECIPIENT_PROJECTION_NO_ID)),
Stream.of(GroupDatabase.TYPED_GROUP_PROJECTION))
.toList();
@@ -270,6 +274,7 @@ public class ThreadDatabase extends Database {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] { ID }, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
@@ -283,6 +288,7 @@ public class ThreadDatabase extends Database {
mmsSmsDatabase.deleteAbandonedMessages();
attachmentDatabase.trimAllAbandonedAttachments();
groupReceiptDatabase.deleteAbandonedRows();
mentionDatabase.deleteAbandonedMentions();
attachmentDatabase.deleteAbandonedAttachmentFiles();
db.setTransactionSuccessful();
} finally {
@@ -304,6 +310,7 @@ public class ThreadDatabase extends Database {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
db.beginTransaction();
@@ -312,6 +319,7 @@ public class ThreadDatabase extends Database {
mmsSmsDatabase.deleteAbandonedMessages();
attachmentDatabase.trimAllAbandonedAttachments();
groupReceiptDatabase.deleteAbandonedRows();
mentionDatabase.deleteAbandonedMentions();
attachmentDatabase.deleteAbandonedAttachmentFiles();
db.setTransactionSuccessful();
} finally {
@@ -401,6 +409,7 @@ public class ThreadDatabase extends Database {
List<MarkedMessageInfo> smsRecords = new LinkedList<>();
List<MarkedMessageInfo> mmsRecords = new LinkedList<>();
boolean needsSync = false;
db.beginTransaction();
@@ -413,6 +422,8 @@ public class ThreadDatabase extends Database {
}
for (long threadId : threadIds) {
ThreadRecord previous = getThreadRecord(threadId);
smsRecords.addAll(DatabaseFactory.getSmsDatabase(context).setMessagesReadSince(threadId, sinceTimestamp));
mmsRecords.addAll(DatabaseFactory.getMmsDatabase(context).setMessagesReadSince(threadId, sinceTimestamp));
@@ -423,7 +434,12 @@ public class ThreadDatabase extends Database {
contentValues.put(UNREAD_COUNT, unreadCount);
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{threadId + ""});
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId));
if (previous != null && previous.isForcedUnread()) {
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(previous.getRecipient().getId());
needsSync = true;
}
}
db.setTransactionSuccessful();
@@ -433,6 +449,11 @@ public class ThreadDatabase extends Database {
notifyConversationListeners(new HashSet<>(threadIds));
notifyConversationListListeners();
if (needsSync) {
StorageSyncHelper.scheduleSyncForDataChange();
}
return Util.concatenatedList(smsRecords, mmsRecords);
}
@@ -441,19 +462,22 @@ public class ThreadDatabase extends Database {
db.beginTransaction();
try {
ContentValues contentValues = new ContentValues();
List<RecipientId> recipientIds = getRecipientIdsForThreadIds(threadIds);
SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, threadIds);
ContentValues contentValues = new ContentValues();
contentValues.put(READ, ReadStatus.FORCED_UNREAD.serialize());
for (long threadId : threadIds) {
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] { String.valueOf(threadId) });
}
db.update(TABLE_NAME, contentValues, query.getWhere(), query.getWhereArgs());
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientIds);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
notifyConversationListListeners();
StorageSyncHelper.scheduleSyncForDataChange();
notifyConversationListListeners();
}
}
@@ -552,6 +576,28 @@ public class ThreadDatabase extends Database {
return db.rawQuery(query, null);
}
public @NonNull List<ThreadRecord> getRecentV1Groups(int limit) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String where = MESSAGE_COUNT + " != 0 AND " +
"(" +
GroupDatabase.TABLE_NAME + "." + GroupDatabase.ACTIVE + " = 1 AND " +
GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY + " IS NULL AND " +
GroupDatabase.TABLE_NAME + "." + GroupDatabase.MMS + " = 0" +
")";
String query = createQuery(where, 0, limit, true);
List<ThreadRecord> threadRecords = new ArrayList<>();
try (Reader reader = readerFor(db.rawQuery(query, null))) {
ThreadRecord record;
while ((record = reader.getNext()) != null) {
threadRecords.add(record);
}
}
return threadRecords;
}
public Cursor getConversationList() {
return getConversationList("0");
}
@@ -672,7 +718,7 @@ public class ThreadDatabase extends Database {
final String query;
if (pinned) {
query = createQuery(where, PINNED + " ASC", offset, limit, false);
query = createQuery(where, PINNED + " ASC", offset, limit);
} else {
query = createQuery(where, offset, limit, false);
}
@@ -737,6 +783,25 @@ public class ThreadDatabase extends Database {
return 0;
}
/**
* @return Pinned recipients, in order from top to bottom.
*/
public @NonNull List<RecipientId> getPinnedRecipientIds() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String[] projection = new String[]{RECIPIENT_ID};
String query = PINNED + " > ?";
String[] args = SqlUtil.buildArgs(0);
List<RecipientId> pinned = new LinkedList<>();
try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, PINNED + " ASC")) {
while (cursor.moveToNext()) {
pinned.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)));
}
}
return pinned;
}
public void pinConversations(@NonNull Set<Long> threadIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
@@ -750,6 +815,7 @@ public class ThreadDatabase extends Database {
contentValues.put(PINNED, ++pinnedCount);
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId));
}
db.setTransactionSuccessful();
@@ -759,6 +825,9 @@ public class ThreadDatabase extends Database {
}
notifyConversationListListeners();
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().getId());
StorageSyncHelper.scheduleSyncForDataChange();
}
public void unpinConversations(@NonNull Set<Long> threadIds) {
@@ -771,6 +840,9 @@ public class ThreadDatabase extends Database {
db.update(TABLE_NAME, contentValues, selection, SqlUtil.buildArgs(Stream.of(threadIds).toArray()));
notifyConversationListListeners();
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().getId());
StorageSyncHelper.scheduleSyncForDataChange();
}
public void archiveConversation(long threadId) {
@@ -945,6 +1017,20 @@ public class ThreadDatabase extends Database {
return Recipient.resolved(id);
}
public @NonNull List<RecipientId> getRecipientIdsForThreadIds(Collection<Long> threadIds) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, threadIds);
List<RecipientId> ids = new ArrayList<>(threadIds.size());
try (Cursor cursor = db.query(TABLE_NAME, new String[] { RECIPIENT_ID }, query.getWhere(), query.getWhereArgs(), null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)));
}
}
return ids;
}
public boolean hasThread(@NonNull RecipientId recipientId) {
return getThreadIdIfExistsFor(recipientId) > -1;
}
@@ -960,16 +1046,106 @@ public class ThreadDatabase extends Database {
}
void updateReadState(long threadId) {
int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId);
ThreadRecord previous = getThreadRecord(threadId);
int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId);
ContentValues contentValues = new ContentValues();
contentValues.put(READ, unreadCount == 0);
contentValues.put(READ, unreadCount == 0 ? ReadStatus.READ.serialize() : ReadStatus.UNREAD.serialize());
contentValues.put(UNREAD_COUNT, unreadCount);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,ID_WHERE,
new String[] {String.valueOf(threadId)});
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId));
notifyConversationListListeners();
if (previous != null && previous.isForcedUnread()) {
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(previous.getRecipient().getId());
StorageSyncHelper.scheduleSyncForDataChange();
}
}
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalContactRecord record) {
applyStorageSyncUpdate(recipientId, record.isArchived(), record.isForcedUnread());
}
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalGroupV1Record record) {
applyStorageSyncUpdate(recipientId, record.isArchived(), record.isForcedUnread());
}
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalGroupV2Record record) {
applyStorageSyncUpdate(recipientId, record.isArchived(), record.isForcedUnread());
}
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalAccountRecord record) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived(), record.isNoteToSelfForcedUnread());
ContentValues clearPinnedValues = new ContentValues();
clearPinnedValues.put(PINNED, 0);
db.update(TABLE_NAME, clearPinnedValues, null, null);
int pinnedPosition = 1;
for (SignalAccountRecord.PinnedConversation pinned : record.getPinnedConversations()) {
ContentValues pinnedValues = new ContentValues();
pinnedValues.put(PINNED, pinnedPosition);
Recipient pinnedRecipient;
if (pinned.getContact().isPresent()) {
pinnedRecipient = Recipient.externalPush(context, pinned.getContact().get());
} else if (pinned.getGroupV1Id().isPresent()) {
try {
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v1Exact(pinned.getGroupV1Id().get()));
} catch (BadGroupIdException e) {
Log.w(TAG, "Failed to parse pinned groupV1 ID!", e);
pinnedRecipient = null;
}
} else if (pinned.getGroupV2MasterKey().isPresent()) {
try {
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
} catch (InvalidInputException e) {
Log.w(TAG, "Failed to parse pinned groupV2 master key!", e);
pinnedRecipient = null;
}
} else {
Log.w(TAG, "Empty pinned conversation on the AccountRecord?");
pinnedRecipient = null;
}
if (pinnedRecipient != null) {
db.update(TABLE_NAME, pinnedValues, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(pinnedRecipient.getId()));
}
pinnedPosition++;
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
notifyConversationListListeners();
}
private void applyStorageSyncUpdate(@NonNull RecipientId recipientId, boolean archived, boolean forcedUnread) {
ContentValues values = new ContentValues();
values.put(ARCHIVED, archived);
if (forcedUnread) {
values.put(READ, ReadStatus.FORCED_UNREAD.serialize());
} else {
Long threadId = getThreadIdFor(recipientId);
if (threadId != null) {
int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId);
values.put(READ, unreadCount == 0 ? ReadStatus.READ.serialize() : ReadStatus.UNREAD.serialize());
values.put(UNREAD_COUNT, unreadCount);
}
}
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(recipientId));
}
public boolean update(long threadId, boolean unarchive) {
@@ -1145,10 +1321,10 @@ public class ThreadDatabase extends Database {
return Extra.forMessageRequest();
}
if (record.isViewOnce()) {
return Extra.forViewOnce();
} else if (record.isRemoteDelete()) {
if (record.isRemoteDelete()) {
return Extra.forRemoteDelete();
} else if (record.isViewOnce()) {
return Extra.forViewOnce();
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
StickerSlide slide = Objects.requireNonNull(((MmsMessageRecord) record).getSlideDeck().getStickerSlide());
return Extra.forSticker(slide.getEmoji());
@@ -1166,10 +1342,10 @@ public class ThreadDatabase extends Database {
private @NonNull String createQuery(@NonNull String where, long offset, long limit, boolean preferPinned) {
String orderBy = (preferPinned ? TABLE_NAME + "." + PINNED + " DESC, " : "") + TABLE_NAME + "." + DATE + " DESC";
return createQuery(where, orderBy, offset, limit, preferPinned);
return createQuery(where, orderBy, offset, limit);
}
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit, boolean preferPinned) {
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit) {
String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ",");
String query =
@@ -1400,7 +1576,7 @@ public class ThreadDatabase extends Database {
}
}
private enum ReadStatus {
enum ReadStatus {
READ(1), UNREAD(0), FORCED_UNREAD(2);
private final int value;

View File

@@ -62,13 +62,18 @@ import org.thoughtcrime.securesms.util.FileUtils;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Triple;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
@@ -149,8 +154,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int STICKER_EMOJI_IN_NOTIFICATIONS = 73;
private static final int THUMBNAIL_CLEANUP = 74;
private static final int STICKER_CONTENT_TYPE_CLEANUP = 75;
private static final int MENTION_CLEANUP = 76;
private static final int MENTION_CLEANUP_V2 = 77;
private static final int REACTION_CLEANUP = 78;
private static final int CAPABILITIES_REFACTOR = 79;
private static final int GV1_MIGRATION = 80;
private static final int DATABASE_VERSION = 75;
private static final int DATABASE_VERSION = 80;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -1060,6 +1070,96 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
Log.i(TAG, "Updated " + rows + " sticker attachment content types.");
}
if (oldVersion < MENTION_CLEANUP) {
String selectMentionIdsNotInGroupsV2 = "select mention._id from mention left join thread on mention.thread_id = thread._id left join recipient on thread.recipient_ids = recipient._id where recipient.group_type != 3";
db.delete("mention", "_id in (" + selectMentionIdsNotInGroupsV2 + ")", null);
db.delete("mention", "message_id NOT IN (SELECT _id FROM mms) OR thread_id NOT IN (SELECT _id from thread)", null);
List<Long> idsToDelete = new LinkedList<>();
try (Cursor cursor = db.rawQuery("select mention.*, mms.body from mention inner join mms on mention.message_id = mms._id", null)) {
while (cursor != null && cursor.moveToNext()) {
int rangeStart = CursorUtil.requireInt(cursor, "range_start");
int rangeLength = CursorUtil.requireInt(cursor, "range_length");
String body = CursorUtil.requireString(cursor, "body");
if (body == null || body.isEmpty() || rangeStart < 0 || rangeLength < 0 || (rangeStart + rangeLength) > body.length()) {
idsToDelete.add(CursorUtil.requireLong(cursor, "_id"));
}
}
}
if (Util.hasItems(idsToDelete)) {
String ids = TextUtils.join(",", idsToDelete);
db.delete("mention", "_id in (" + ids + ")", null);
}
}
if (oldVersion < MENTION_CLEANUP_V2) {
String selectMentionIdsWithMismatchingThreadIds = "select mention._id from mention left join mms on mention.message_id = mms._id where mention.thread_id != mms.thread_id";
db.delete("mention", "_id in (" + selectMentionIdsWithMismatchingThreadIds + ")", null);
List<Long> idsToDelete = new LinkedList<>();
Set<Triple<Long, Integer, Integer>> mentionTuples = new HashSet<>();
try (Cursor cursor = db.rawQuery("select mention.*, mms.body from mention inner join mms on mention.message_id = mms._id order by mention._id desc", null)) {
while (cursor != null && cursor.moveToNext()) {
long mentionId = CursorUtil.requireLong(cursor, "_id");
long messageId = CursorUtil.requireLong(cursor, "message_id");
int rangeStart = CursorUtil.requireInt(cursor, "range_start");
int rangeLength = CursorUtil.requireInt(cursor, "range_length");
String body = CursorUtil.requireString(cursor, "body");
if (body != null && rangeStart < body.length() && body.charAt(rangeStart) != '\uFFFC') {
idsToDelete.add(mentionId);
} else {
Triple<Long, Integer, Integer> tuple = new Triple<>(messageId, rangeStart, rangeLength);
if (mentionTuples.contains(tuple)) {
idsToDelete.add(mentionId);
} else {
mentionTuples.add(tuple);
}
}
}
if (Util.hasItems(idsToDelete)) {
String ids = TextUtils.join(",", idsToDelete);
db.delete("mention", "_id in (" + ids + ")", null);
}
}
}
if (oldVersion < REACTION_CLEANUP) {
ContentValues values = new ContentValues();
values.putNull("reactions");
db.update("sms", values, "remote_deleted = ?", new String[] { "1" });
}
if (oldVersion < CAPABILITIES_REFACTOR) {
db.execSQL("ALTER TABLE recipient ADD COLUMN capabilities INTEGER DEFAULT 0");
db.execSQL("UPDATE recipient SET capabilities = 1 WHERE gv2_capability = 1");
db.execSQL("UPDATE recipient SET capabilities = 2 WHERE gv2_capability = -1");
}
if (oldVersion < GV1_MIGRATION) {
db.execSQL("ALTER TABLE groups ADD COLUMN expected_v2_id TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE groups ADD COLUMN former_v1_members TEXT DEFAULT NULL");
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON groups (expected_v2_id)");
int count = 0;
try (Cursor cursor = db.rawQuery("SELECT * FROM groups WHERE group_id LIKE '__textsecure_group__!%' AND LENGTH(group_id) = 53", null)) {
while (cursor.moveToNext()) {
String gv1 = CursorUtil.requireString(cursor, "group_id");
String gv2 = GroupId.parseOrThrow(gv1).requireV1().deriveV2MigrationGroupId().toString();
ContentValues values = new ContentValues();
values.put("expected_v2_id", gv2);
count += db.update("groups", values, "group_id = ?", SqlUtil.buildArgs(gv1));
}
}
Log.i(TAG, "Updated " + count + " GV1 groups with expected GV2 IDs.");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -5,6 +5,7 @@ import android.Manifest;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import androidx.loader.content.CursorLoader;
@@ -15,7 +16,7 @@ public class RecentPhotosLoader extends CursorLoader {
public static Uri BASE_URL = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
private static final String[] PROJECTION = new String[] {
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATE_MODIFIED,
MediaStore.Images.ImageColumns.ORIENTATION,
@@ -26,7 +27,8 @@ public class RecentPhotosLoader extends CursorLoader {
MediaStore.Images.ImageColumns.HEIGHT
};
private static final String SELECTION = MediaStore.Images.Media.DATA + " NOT NULL";
private static final String SELECTION = Build.VERSION.SDK_INT > 28 ? MediaStore.Images.Media.IS_PENDING + " != 1"
: MediaStore.Images.Media.DATA + " IS NULL";
private final Context context;

View File

@@ -140,16 +140,28 @@ public abstract class DisplayRecord {
return SmsDatabase.Types.isJoinedType(type);
}
public boolean isIncomingCall() {
return SmsDatabase.Types.isIncomingCall(type);
public boolean isIncomingAudioCall() {
return SmsDatabase.Types.isIncomingAudioCall(type);
}
public boolean isOutgoingCall() {
return SmsDatabase.Types.isOutgoingCall(type);
public boolean isIncomingVideoCall() {
return SmsDatabase.Types.isIncomingVideoCall(type);
}
public boolean isMissedCall() {
return SmsDatabase.Types.isMissedCall(type);
public boolean isOutgoingAudioCall() {
return SmsDatabase.Types.isOutgoingAudioCall(type);
}
public boolean isOutgoingVideoCall() {
return SmsDatabase.Types.isOutgoingVideoCall(type);
}
public final boolean isMissedAudioCall() {
return SmsDatabase.Types.isMissedAudioCall(type);
}
public final boolean isMissedVideoCall() {
return SmsDatabase.Types.isMissedVideoCall(type);
}
public boolean isVerificationStatusChange() {

View File

@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.google.protobuf.ByteString;
@@ -62,26 +64,30 @@ final class GroupsV2UpdateMessageProducer {
UpdateDescription describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange decryptedGroupChange) {
Optional<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid);
if (selfPending.isPresent()) {
return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy));
return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
}
ByteString foundingMemberUuid = decryptedGroupChange.getEditor();
if (!foundingMemberUuid.isEmpty()) {
if (selfUuidBytes.equals(foundingMemberUuid)) {
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group));
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
} else {
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator));
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
}
}
if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfUuid).isPresent()) {
return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group));
return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
} else {
return updateDescription(context.getString(R.string.MessageRecord_group_updated));
return updateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
}
}
List<UpdateDescription> describeChanges(@NonNull DecryptedGroupChange change) {
List<UpdateDescription> describeChanges(@Nullable DecryptedGroup previousGroupState, @NonNull DecryptedGroupChange change) {
if (DecryptedGroup.getDefaultInstance().equals(previousGroupState)) {
previousGroupState = null;
}
List<UpdateDescription> updates = new LinkedList<>();
if (change.getEditor().isEmpty() || UuidUtil.UNKNOWN_UUID.equals(UuidUtil.fromByteString(change.getEditor()))) {
@@ -96,7 +102,7 @@ final class GroupsV2UpdateMessageProducer {
describeUnknownEditorNewTimer(change, updates);
describeUnknownEditorNewAttributeAccess(change, updates);
describeUnknownEditorNewMembershipAccess(change, updates);
describeUnknownEditorNewGroupInviteLinkAccess(change, updates);
describeUnknownEditorNewGroupInviteLinkAccess(previousGroupState, change, updates);
describeRequestingMembers(change, updates);
describeUnknownEditorRequestingMembersApprovals(change, updates);
describeUnknownEditorRequestingMembersDeletes(change, updates);
@@ -119,7 +125,7 @@ final class GroupsV2UpdateMessageProducer {
describeNewTimer(change, updates);
describeNewAttributeAccess(change, updates);
describeNewMembershipAccess(change, updates);
describeNewGroupInviteLinkAccess(change, updates);
describeNewGroupInviteLinkAccess(previousGroupState, change, updates);
describeRequestingMembers(change, updates);
describeRequestingMembersApprovals(change, updates);
describeRequestingMembersDeletes(change, updates);
@@ -141,14 +147,14 @@ final class GroupsV2UpdateMessageProducer {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), (editor) -> context.getString(R.string.MessageRecord_s_updated_group, editor)));
updates.add(updateDescription(change.getEditor(), (editor) -> context.getString(R.string.MessageRecord_s_updated_group, editor), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
}
}
private void describeUnknownEditorUnknownChange(@NonNull List<UpdateDescription> updates) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_was_updated)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_was_updated), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
}
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
@@ -159,18 +165,18 @@ final class GroupsV2UpdateMessageProducer {
if (editorIsYou) {
if (newMemberIsYou) {
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_group_link)));
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_group_link), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
} else {
updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added)));
updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
}
} else {
if (newMemberIsYou) {
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
} else {
if (member.getUuid().equals(change.getEditor())) {
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_group_link, newMember)));
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_group_link, newMember), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember)));
updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
}
}
}
@@ -182,9 +188,9 @@ final class GroupsV2UpdateMessageProducer {
boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes);
if (newMemberIsYou) {
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
} else {
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember)));
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
}
}
}
@@ -197,18 +203,18 @@ final class GroupsV2UpdateMessageProducer {
if (editorIsYou) {
if (removedMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16));
} else {
updates.add(updateDescription(member, removedMember -> context.getString(R.string.MessageRecord_you_removed_s, removedMember)));
updates.add(updateDescription(member, removedMember -> context.getString(R.string.MessageRecord_you_removed_s, removedMember), R.drawable.ic_update_group_remove_light_16, R.drawable.ic_update_group_remove_dark_16));
}
} else {
if (removedMemberIsYou) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_removed_you_from_the_group, editor)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_removed_you_from_the_group, editor), R.drawable.ic_update_group_remove_light_16, R.drawable.ic_update_group_remove_dark_16));
} else {
if (member.equals(change.getEditor())) {
updates.add(updateDescription(member, leavingMember -> context.getString(R.string.MessageRecord_s_left_the_group, leavingMember)));
updates.add(updateDescription(member, leavingMember -> context.getString(R.string.MessageRecord_s_left_the_group, leavingMember), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), member, (editor, removedMember) -> context.getString(R.string.MessageRecord_s_removed_s, editor, removedMember)));
updates.add(updateDescription(change.getEditor(), member, (editor, removedMember) -> context.getString(R.string.MessageRecord_s_removed_s, editor, removedMember), R.drawable.ic_update_group_remove_light_16, R.drawable.ic_update_group_remove_dark_16));
}
}
}
@@ -220,9 +226,9 @@ final class GroupsV2UpdateMessageProducer {
boolean removedMemberIsYou = member.equals(selfUuidBytes);
if (removedMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16));
} else {
updates.add(updateDescription(member, oldMember -> context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, oldMember)));
updates.add(updateDescription(member, oldMember -> context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, oldMember), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16));
}
}
}
@@ -234,23 +240,23 @@ final class GroupsV2UpdateMessageProducer {
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
if (editorIsYou) {
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_you_made_s_an_admin, newAdmin)));
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_you_made_s_an_admin, newAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
if (changedMemberIsYou) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_made_you_an_admin, editor)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_made_you_an_admin, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, newAdmin) -> context.getString(R.string.MessageRecord_s_made_s_an_admin, editor, newAdmin)));
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, newAdmin) -> context.getString(R.string.MessageRecord_s_made_s_an_admin, editor, newAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
} else {
if (editorIsYou) {
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, oldAdmin)));
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, oldAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
if (changedMemberIsYou) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, editor)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, oldAdmin) -> context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, editor, oldAdmin)));
updates.add(updateDescription(change.getEditor(), roleChange.getUuid(), (editor, oldAdmin) -> context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, editor, oldAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
}
@@ -263,15 +269,15 @@ final class GroupsV2UpdateMessageProducer {
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
if (changedMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_now_an_admin)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_now_an_admin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_s_is_now_an_admin, newAdmin)));
updates.add(updateDescription(roleChange.getUuid(), newAdmin -> context.getString(R.string.MessageRecord_s_is_now_an_admin, newAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
} else {
if (changedMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, oldAdmin)));
updates.add(updateDescription(roleChange.getUuid(), oldAdmin -> context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, oldAdmin), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
}
@@ -285,10 +291,10 @@ final class GroupsV2UpdateMessageProducer {
boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
if (newMemberIsYou) {
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor)));
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
} else {
if (editorIsYou) {
updates.add(updateDescription(invitee.getUuid(), newInvitee -> context.getString(R.string.MessageRecord_you_invited_s_to_the_group, newInvitee)));
updates.add(updateDescription(invitee.getUuid(), newInvitee -> context.getString(R.string.MessageRecord_you_invited_s_to_the_group, newInvitee), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
} else {
notYouInviteCount++;
}
@@ -297,7 +303,7 @@ final class GroupsV2UpdateMessageProducer {
if (notYouInviteCount > 0) {
final int notYouInviteCountFinalCopy = notYouInviteCount;
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, editor, notYouInviteCountFinalCopy)));
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, editor, notYouInviteCountFinalCopy), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
}
}
@@ -311,9 +317,9 @@ final class GroupsV2UpdateMessageProducer {
UUID uuid = UuidUtil.fromByteStringOrUnknown(invitee.getAddedByUuid());
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_were_invited_to_the_group)));
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_were_invited_to_the_group), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
} else {
updates.add(0, updateDescription(invitee.getAddedByUuid(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor)));
updates.add(0, updateDescription(invitee.getAddedByUuid(), editor -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, editor), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
}
} else {
notYouInviteCount++;
@@ -321,7 +327,7 @@ final class GroupsV2UpdateMessageProducer {
}
if (notYouInviteCount > 0) {
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount)));
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
}
}
@@ -333,12 +339,12 @@ final class GroupsV2UpdateMessageProducer {
boolean decline = invitee.getUuid().equals(change.getEditor());
if (decline) {
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
}
} else if (invitee.getUuid().equals(selfUuidBytes)) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, editor)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, editor), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
} else {
notDeclineCount++;
}
@@ -346,10 +352,10 @@ final class GroupsV2UpdateMessageProducer {
if (notDeclineCount > 0) {
if (editorIsYou) {
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount)));
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
} else {
final int notDeclineCountFinalCopy = notDeclineCount;
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, editor, notDeclineCountFinalCopy)));
updates.add(updateDescription(change.getEditor(), editor -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, editor, notDeclineCountFinalCopy), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
}
}
}
@@ -361,14 +367,14 @@ final class GroupsV2UpdateMessageProducer {
boolean inviteeWasYou = invitee.getUuid().equals(selfUuidBytes);
if (inviteeWasYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_an_admin_revoked_your_invitation_to_the_group)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_an_admin_revoked_your_invitation_to_the_group), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
} else {
notDeclineCount++;
}
}
if (notDeclineCount > 0) {
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount)));
updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
}
}
@@ -381,18 +387,18 @@ final class GroupsV2UpdateMessageProducer {
if (editorIsYou) {
if (newMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
} else {
updates.add(updateDescription(uuid, newPromotedMember -> context.getString(R.string.MessageRecord_you_added_invited_member_s, newPromotedMember)));
updates.add(updateDescription(uuid, newPromotedMember -> context.getString(R.string.MessageRecord_you_added_invited_member_s, newPromotedMember), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
}
} else {
if (newMemberIsYou) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_light_16));
} else {
if (uuid.equals(change.getEditor())) {
updates.add(updateDescription(uuid, newAcceptedMember -> context.getString(R.string.MessageRecord_s_accepted_invite, newAcceptedMember)));
updates.add(updateDescription(uuid, newAcceptedMember -> context.getString(R.string.MessageRecord_s_accepted_invite, newAcceptedMember), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), uuid, (editor, newAcceptedMember) -> context.getString(R.string.MessageRecord_s_added_invited_member_s, editor, newAcceptedMember)));
updates.add(updateDescription(change.getEditor(), uuid, (editor, newAcceptedMember) -> context.getString(R.string.MessageRecord_s_added_invited_member_s, editor, newAcceptedMember), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
}
}
}
@@ -405,9 +411,9 @@ final class GroupsV2UpdateMessageProducer {
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
if (newMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
} else {
updates.add(updateDescription(uuid, newMemberName -> context.getString(R.string.MessageRecord_s_joined_the_group, newMemberName)));
updates.add(updateDescription(uuid, newMemberName -> context.getString(R.string.MessageRecord_s_joined_the_group, newMemberName), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16));
}
}
}
@@ -418,16 +424,16 @@ final class GroupsV2UpdateMessageProducer {
if (change.hasNewTitle()) {
String newTitle = StringUtil.isolateBidi(change.getNewTitle().getValue());
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle), R.drawable.ic_update_group_name_light_16, R.drawable.ic_update_group_name_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, editor, newTitle)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, editor, newTitle), R.drawable.ic_update_group_name_light_16, R.drawable.ic_update_group_name_dark_16));
}
}
}
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
if (change.hasNewTitle()) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(change.getNewTitle().getValue()))));
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(change.getNewTitle().getValue())), R.drawable.ic_update_group_name_light_16, R.drawable.ic_update_group_name_dark_16));
}
}
@@ -436,16 +442,16 @@ final class GroupsV2UpdateMessageProducer {
if (change.hasNewAvatar()) {
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_avatar)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_avatar), R.drawable.ic_update_group_avatar_light_16, R.drawable.ic_update_group_avatar_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_avatar, editor)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_avatar, editor), R.drawable.ic_update_group_avatar_light_16, R.drawable.ic_update_group_avatar_dark_16));
}
}
}
private void describeUnknownEditorNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
if (change.hasNewAvatar()) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed), R.drawable.ic_update_group_avatar_light_16, R.drawable.ic_update_group_avatar_dark_16));
}
}
@@ -455,9 +461,9 @@ final class GroupsV2UpdateMessageProducer {
if (change.hasNewTimer()) {
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, editor, time)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, editor, time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16));
}
}
}
@@ -465,7 +471,7 @@ final class GroupsV2UpdateMessageProducer {
private void describeUnknownEditorNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
if (change.hasNewTimer()) {
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16));
}
}
@@ -475,9 +481,9 @@ final class GroupsV2UpdateMessageProducer {
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, editor, accessLevel)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, editor, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
}
@@ -485,7 +491,7 @@ final class GroupsV2UpdateMessageProducer {
private void describeUnknownEditorNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
@@ -495,9 +501,9 @@ final class GroupsV2UpdateMessageProducer {
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, editor, accessLevel)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, editor, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
}
@@ -505,11 +511,20 @@ final class GroupsV2UpdateMessageProducer {
private void describeUnknownEditorNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
private void describeNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
private void describeNewGroupInviteLinkAccess(@Nullable DecryptedGroup previousGroupState,
@NonNull DecryptedGroupChange change,
@NonNull List<UpdateDescription> updates)
{
AccessControl.AccessRequired previousAccessControl = null;
if (previousGroupState != null) {
previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink();
}
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean groupLinkEnabled = false;
@@ -517,52 +532,85 @@ final class GroupsV2UpdateMessageProducer {
case ANY:
groupLinkEnabled = true;
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off)));
if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_admin_approval_for_the_group_link), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor)));
if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_admin_approval_for_the_group_link, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
break;
case ADMINISTRATOR:
groupLinkEnabled = true;
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on)));
if (previousAccessControl == AccessControl.AccessRequired.ANY) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_admin_approval_for_the_group_link), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor)));
if (previousAccessControl == AccessControl.AccessRequired.ANY) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_admin_approval_for_the_group_link, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
break;
case UNSATISFIABLE:
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_group_link)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_group_link), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_group_link, editor)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_group_link, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
break;
}
if (!groupLinkEnabled && change.getNewInviteLinkPassword().size() > 0) {
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_group_link)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_group_link), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_group_link, editor)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_group_link, editor), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
}
private void describeUnknownEditorNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
private void describeUnknownEditorNewGroupInviteLinkAccess(@Nullable DecryptedGroup previousGroupState,
@NonNull DecryptedGroupChange change,
@NonNull List<UpdateDescription> updates)
{
AccessControl.AccessRequired previousAccessControl = null;
if (previousGroupState != null) {
previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink();
}
switch (change.getNewInviteLinkAccess()) {
case ANY:
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off)));
if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_off), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
break;
case ADMINISTRATOR:
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on)));
if (previousAccessControl == AccessControl.AccessRequired.ANY) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_on), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
break;
case UNSATISFIABLE:
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
break;
}
if (change.getNewInviteLinkPassword().size() > 0) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_reset)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_reset), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16));
}
}
@@ -571,9 +619,9 @@ final class GroupsV2UpdateMessageProducer {
boolean requestingMemberIsYou = member.getUuid().equals(selfUuidBytes);
if (requestingMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
} else {
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_group_link, requesting)));
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_group_link, requesting), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16));
}
}
}
@@ -583,14 +631,14 @@ final class GroupsV2UpdateMessageProducer {
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
if (requestingMemberIsYou) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_approved_your_request_to_join_the_group, editor)));
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_approved_your_request_to_join_the_group, editor), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
} else {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (editorIsYou) {
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requesting)));
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requesting), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), requestingMember.getUuid(), (editor, requesting) -> context.getString(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, editor, requesting)));
updates.add(updateDescription(change.getEditor(), requestingMember.getUuid(), (editor, requesting) -> context.getString(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, editor, requesting), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
}
}
}
@@ -601,9 +649,9 @@ final class GroupsV2UpdateMessageProducer {
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
if (requestingMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
} else {
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, requesting)));
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, requesting), R.drawable.ic_update_group_accept_light_16, R.drawable.ic_update_group_accept_dark_16));
}
}
}
@@ -616,17 +664,17 @@ final class GroupsV2UpdateMessageProducer {
if (requestingMemberIsYou) {
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_canceled_your_request_to_join_the_group)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_canceled_your_request_to_join_the_group), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
}
} else {
boolean editorIsCanceledMember = change.getEditor().equals(requestingMember);
if (editorIsCanceledMember) {
updates.add(updateDescription(requestingMember, editorRequesting -> context.getString(R.string.MessageRecord_s_canceled_their_request_to_join_the_group, editorRequesting)));
updates.add(updateDescription(requestingMember, editorRequesting -> context.getString(R.string.MessageRecord_s_canceled_their_request_to_join_the_group, editorRequesting), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
} else {
updates.add(updateDescription(change.getEditor(), requestingMember, (editor, requesting) -> context.getString(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, editor, requesting)));
updates.add(updateDescription(change.getEditor(), requestingMember, (editor, requesting) -> context.getString(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, editor, requesting), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
}
}
}
@@ -637,9 +685,9 @@ final class GroupsV2UpdateMessageProducer {
boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes);
if (requestingMemberIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin)));
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
} else {
updates.add(updateDescription(requestingMember, requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, requesting)));
updates.add(updateDescription(requestingMember, requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, requesting), R.drawable.ic_update_group_decline_light_16, R.drawable.ic_update_group_decline_dark_16));
}
}
}
@@ -662,20 +710,32 @@ final class GroupsV2UpdateMessageProducer {
String create(String arg1, String arg2);
}
private static UpdateDescription updateDescription(@NonNull String string) {
return UpdateDescription.staticDescription(string);
private static UpdateDescription updateDescription(@NonNull String string,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource)
{
return UpdateDescription.staticDescription(string, lightIconResource, darkIconResource);
}
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull StringFactory1Arg stringFactory) {
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes,
@NonNull StringFactory1Arg stringFactory,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource)
{
UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes);
return UpdateDescription.mentioning(Collections.singletonList(uuid1), () -> stringFactory.create(descriptionStrategy.describe(uuid1)));
return UpdateDescription.mentioning(Collections.singletonList(uuid1), () -> stringFactory.create(descriptionStrategy.describe(uuid1)), lightIconResource, darkIconResource);
}
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes, @NonNull ByteString uuid2Bytes, @NonNull StringFactory2Args stringFactory) {
private UpdateDescription updateDescription(@NonNull ByteString uuid1Bytes,
@NonNull ByteString uuid2Bytes,
@NonNull StringFactory2Args stringFactory,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource)
{
UUID uuid1 = UuidUtil.fromByteStringOrUnknown(uuid1Bytes);
UUID uuid2 = UuidUtil.fromByteStringOrUnknown(uuid2Bytes);
return UpdateDescription.mentioning(Arrays.asList(uuid1, uuid2), () -> stringFactory.create(descriptionStrategy.describe(uuid1), descriptionStrategy.describe(uuid2)));
return UpdateDescription.mentioning(Arrays.asList(uuid1, uuid2), () -> stringFactory.create(descriptionStrategy.describe(uuid1), descriptionStrategy.describe(uuid2)), lightIconResource, darkIconResource);
}
}

View File

@@ -1,13 +1,24 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.whispersystems.libsignal.util.guava.Function;
@@ -20,9 +31,9 @@ public final class LiveUpdateMessage {
* recreates the string asynchronously when they change.
*/
@AnyThread
public static LiveData<String> fromMessageDescription(@NonNull UpdateDescription updateDescription) {
public static LiveData<Spannable> fromMessageDescription(@NonNull Context context, @NonNull UpdateDescription updateDescription) {
if (updateDescription.isStringStatic()) {
return LiveDataUtil.just(updateDescription.getStaticString());
return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticString()));
}
List<LiveData<Recipient>> allMentionedRecipients = Stream.of(updateDescription.getMentioned())
@@ -32,16 +43,37 @@ public final class LiveUpdateMessage {
LiveData<?> mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object())
: LiveDataUtil.merge(allMentionedRecipients);
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> updateDescription.getString());
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getString()));
}
/**
* Observes a single recipient and recreates the string asynchronously when they change.
*/
public static LiveData<String> recipientToStringAsync(@NonNull RecipientId recipientId,
@NonNull Function<Recipient, String> createStringInBackground)
public static LiveData<Spannable> recipientToStringAsync(@NonNull RecipientId recipientId,
@NonNull Function<Recipient, Spannable> createStringInBackground)
{
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveData(), createStringInBackground);
}
private static @NonNull Spannable toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string) {
boolean isDarkTheme = ThemeUtil.isDarkTheme(context);
int drawableResource = isDarkTheme ? updateDescription.getDarkIconResource() : updateDescription.getLightIconResource();
int tint = isDarkTheme ? updateDescription.getDarkTint() : updateDescription.getLightTint();
if (tint == 0) {
tint = ThemeUtil.getThemedColor(context, R.attr.conversation_item_update_text_color);
}
if (drawableResource == 0) {
return new SpannableString(string);
} else {
Drawable drawable = ContextCompat.getDrawable(context, drawableResource);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
drawable.setColorFilter(tint, PorterDuff.Mode.SRC_ATOP);
Spannable stringWithImage = new SpannableStringBuilder().append(SpanUtil.buildImageSpan(drawable)).append(" ").append(string);
return new SpannableString(SpanUtil.color(tint, stringWithImage));
}
}
}

View File

@@ -22,8 +22,11 @@ import android.text.SpannableString;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.R;
@@ -38,9 +41,11 @@ import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Function;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -48,6 +53,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
/**
@@ -127,43 +133,56 @@ public abstract class MessageRecord extends DisplayRecord {
if (isGroupUpdate() && isGroupV2()) {
return getGv2ChangeDescription(context, getBody());
} else if (isGroupUpdate() && isOutgoing()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_updated_group));
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
} else if (isGroupUpdate()) {
return fromRecipient(getIndividualRecipient(), r -> GroupUtil.getNonV2GroupDescription(context, getBody()).toString(r));
return fromRecipient(getIndividualRecipient(), r -> GroupUtil.getNonV2GroupDescription(context, getBody()).toString(r), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
} else if (isGroupQuit() && isOutgoing()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_left_group));
return staticUpdateDescription(context.getString(R.string.MessageRecord_left_group), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16);
} else if (isGroupQuit()) {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.ConversationItem_group_action_left, r.getDisplayName(context)));
} else if (isIncomingCall()) {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_called_you, r.getDisplayName(context)));
} else if (isOutgoingCall()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_called));
} else if (isMissedCall()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_call));
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.ConversationItem_group_action_left, r.getDisplayName(context)), R.drawable.ic_update_group_leave_light_16, R.drawable.ic_update_group_leave_dark_16);
} else if (isIncomingAudioCall()) {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_called_you_date, r.getDisplayName(context), getCallDateString(context)), R.drawable.ic_update_audio_call_incoming_light_16, R.drawable.ic_update_audio_call_incoming_dark_16);
} else if (isIncomingVideoCall()) {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_called_you_date, r.getDisplayName(context), getCallDateString(context)), R.drawable.ic_update_video_call_incomg_light_16, R.drawable.ic_update_video_call_incoming_dark_16);
} else if (isOutgoingAudioCall()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_called_date, getCallDateString(context)), R.drawable.ic_update_audio_call_outgoing_light_16, R.drawable.ic_update_audio_call_outgoing_dark_16);
} else if (isOutgoingVideoCall()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_called_date, getCallDateString(context)), R.drawable.ic_update_video_call_outgoing_light_16, R.drawable.ic_update_video_call_outgoing_dark_16);
} else if (isMissedAudioCall()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_audio_call_date, getCallDateString(context)), R.drawable.ic_update_audio_call_missed_light_16, R.drawable.ic_update_audio_call_missed_dark_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
} else if (isMissedVideoCall()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_video_call_date, getCallDateString(context)), R.drawable.ic_update_video_call_missed_light_16, R.drawable.ic_update_video_call_missed_dark_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
} else if (isJoined()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)));
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
} else if (isExpirationTimerUpdate()) {
int seconds = (int)(getExpiresIn() / 1000);
if (seconds <= 0) {
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, r.getDisplayName(context)));
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages), R.drawable.ic_update_timer_disabled_light_16, R.drawable.ic_update_timer_disabled_dark_16)
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, r.getDisplayName(context)), R.drawable.ic_update_timer_disabled_light_16, R.drawable.ic_update_timer_disabled_dark_16);
}
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, r.getDisplayName(context), time));
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16)
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, r.getDisplayName(context), time), R.drawable.ic_update_timer_light_16, R.drawable.ic_update_timer_dark_16);
} else if (isIdentityUpdate()) {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)));
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)), R.drawable.ic_update_safety_number_light_16, R.drawable.ic_update_safety_number_dark_16);
} else if (isIdentityVerified()) {
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, r.getDisplayName(context)));
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, r.getDisplayName(context)));
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, r.getDisplayName(context)), R.drawable.ic_update_verified_light_16, R.drawable.ic_update_verified_dark_16);
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, r.getDisplayName(context)), R.drawable.ic_update_verified_light_16, R.drawable.ic_update_verified_dark_16);
} else if (isIdentityDefault()) {
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, r.getDisplayName(context)));
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, r.getDisplayName(context)));
if (isOutgoing()) return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, r.getDisplayName(context)), R.drawable.ic_update_info_light_16, R.drawable.ic_update_info_dark_16);
else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, r.getDisplayName(context)), R.drawable.ic_update_info_light_16, R.drawable.ic_update_info_dark_16);
} else if (isProfileChange()) {
return staticUpdateDescription(getProfileChangeDescription(context));
return staticUpdateDescription(getProfileChangeDescription(context), R.drawable.ic_update_profile_light_16, R.drawable.ic_update_profile_dark_16);
} else if (isEndSession()) {
if (isOutgoing()) return staticUpdateDescription(context.getString(R.string.SmsMessageRecord_secure_session_reset));
else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)));
if (isOutgoing()) return staticUpdateDescription(context.getString(R.string.SmsMessageRecord_secure_session_reset), R.drawable.ic_update_info_light_16, R.drawable.ic_update_info_dark_16);
else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)), R.drawable.ic_update_info_light_16, R.drawable.ic_update_info_dark_16);
} else if (isGroupV1MigrationEvent()) {
if (Util.isEmpty(getBody())) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_this_group_was_updated_to_a_new_group), R.drawable.ic_update_group_role_light_16, R.drawable.ic_update_group_role_dark_16);
} else {
int count = getGroupV1MigrationEventInvites().size();
return staticUpdateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_members_couldnt_be_added_to_the_new_group_and_have_been_invited, count, count), R.drawable.ic_update_group_add_light_16, R.drawable.ic_update_group_add_dark_16);
}
}
return null;
@@ -177,13 +196,13 @@ public abstract class MessageRecord extends DisplayRecord {
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get());
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() != 0) {
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getChange()));
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange()));
} else {
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange());
}
} catch (IOException e) {
Log.w(TAG, "GV2 Message update detail could not be read", e);
return staticUpdateDescription(context.getString(R.string.MessageRecord_group_updated));
return staticUpdateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_light_16, R.drawable.ic_update_group_dark_16);
}
}
@@ -210,12 +229,35 @@ public abstract class MessageRecord extends DisplayRecord {
}
}
private static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient, @NonNull Function<Recipient, String> stringFunction) {
return UpdateDescription.mentioning(Collections.singletonList(recipient.getUuid().or(UuidUtil.UNKNOWN_UUID)), () -> stringFunction.apply(recipient.resolve()));
private @NonNull String getCallDateString(@NonNull Context context) {
return DateUtils.getExtendedRelativeTimeSpanString(context, Locale.getDefault(), getDateSent());
}
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string) {
return UpdateDescription.staticDescription(string);
private static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient,
@NonNull Function<Recipient, String> stringGenerator,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource)
{
return UpdateDescription.mentioning(Collections.singletonList(recipient.getUuid().or(UuidUtil.UNKNOWN_UUID)),
() -> stringGenerator.apply(recipient.resolve()),
lightIconResource,
darkIconResource);
}
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource)
{
return UpdateDescription.staticDescription(string, lightIconResource, darkIconResource);
}
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource,
@ColorInt int lightTint,
@ColorInt int darkTint)
{
return UpdateDescription.staticDescription(string, lightIconResource, darkIconResource, lightTint, darkTint);
}
private @NonNull String getProfileChangeDescription(@NonNull Context context) {
@@ -316,9 +358,22 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isInvalidVersionKeyExchange(type);
}
public boolean isGroupV1MigrationEvent() {
return SmsDatabase.Types.isGroupV1MigrationEvent(type);
}
public @NonNull List<RecipientId> getGroupV1MigrationEventInvites() {
if (isGroupV1MigrationEvent()) {
return RecipientId.fromSerializedList(getBody());
} else {
return Collections.emptyList();
}
}
public boolean isUpdate() {
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isProfileChange();
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
isProfileChange() || isGroupV1MigrationEvent();
}
public boolean isMediaPending() {

View File

@@ -147,8 +147,12 @@ public final class ThreadRecord {
return MmsSmsColumns.Types.isOutgoingMessageType(type);
}
public boolean isOutgoingCall() {
return SmsDatabase.Types.isOutgoingCall(type);
public boolean isOutgoingAudioCall() {
return SmsDatabase.Types.isOutgoingAudioCall(type);
}
public boolean isOutgoingVideoCall() {
return SmsDatabase.Types.isOutgoingVideoCall(type);
}
public boolean isVerificationStatusChange() {

View File

@@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.database.model;
import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
@@ -28,17 +30,29 @@ public final class UpdateDescription {
private final Collection<UUID> mentioned;
private final StringFactory stringFactory;
private final String staticString;
private final int lightIconResource;
private final int darkIconResource;
private final int lightTint;
private final int darkTint;
private UpdateDescription(@NonNull Collection<UUID> mentioned,
@Nullable StringFactory stringFactory,
@Nullable String staticString)
@Nullable String staticString,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource,
@ColorInt int lightTint,
@ColorInt int darkTint)
{
if (staticString == null && stringFactory == null) {
throw new AssertionError();
}
this.mentioned = mentioned;
this.stringFactory = stringFactory;
this.staticString = staticString;
this.mentioned = mentioned;
this.stringFactory = stringFactory;
this.staticString = staticString;
this.lightIconResource = lightIconResource;
this.darkIconResource = darkIconResource;
this.lightTint = lightTint;
this.darkTint = darkTint;
}
/**
@@ -49,18 +63,39 @@ public final class UpdateDescription {
* @param stringFactory The background method for generating the string.
*/
public static UpdateDescription mentioning(@NonNull Collection<UUID> mentioned,
@NonNull StringFactory stringFactory)
@NonNull StringFactory stringFactory,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource)
{
return new UpdateDescription(UuidUtil.filterKnown(mentioned),
stringFactory,
null);
null,
lightIconResource,
darkIconResource,
0,
0);
}
/**
* Create an update description that's string value is fixed.
*/
public static UpdateDescription staticDescription(@NonNull String staticString) {
return new UpdateDescription(Collections.emptyList(), null, staticString);
public static UpdateDescription staticDescription(@NonNull String staticString,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource)
{
return new UpdateDescription(Collections.emptyList(), null, staticString, lightIconResource, darkIconResource, 0, 0);
}
/**
* Create an update description that's string value is fixed with a specific tint color.
*/
public static UpdateDescription staticDescription(@NonNull String staticString,
@DrawableRes int lightIconResource,
@DrawableRes int darkIconResource,
@ColorInt int lightTint,
@ColorInt int darkTint)
{
return new UpdateDescription(Collections.emptyList(), null, staticString, lightIconResource, darkIconResource, lightTint, darkTint);
}
public boolean isStringStatic() {
@@ -93,6 +128,22 @@ public final class UpdateDescription {
return mentioned;
}
public @DrawableRes int getLightIconResource() {
return lightIconResource;
}
public @DrawableRes int getDarkIconResource() {
return darkIconResource;
}
public @ColorInt int getLightTint() {
return lightTint;
}
public @ColorInt int getDarkTint() {
return darkTint;
}
public static UpdateDescription concatWithNewLines(@NonNull List<UpdateDescription> updateDescriptions) {
if (updateDescriptions.size() == 0) {
throw new AssertionError();
@@ -103,7 +154,9 @@ public final class UpdateDescription {
}
if (allAreStatic(updateDescriptions)) {
return UpdateDescription.staticDescription(concatStaticLines(updateDescriptions));
return UpdateDescription.staticDescription(concatStaticLines(updateDescriptions),
updateDescriptions.get(0).getLightIconResource(),
updateDescriptions.get(0).getDarkIconResource());
}
Set<UUID> allMentioned = new HashSet<>();
@@ -112,7 +165,10 @@ public final class UpdateDescription {
allMentioned.addAll(updateDescription.getMentioned());
}
return UpdateDescription.mentioning(allMentioned, () -> concatLines(updateDescriptions));
return UpdateDescription.mentioning(allMentioned,
() -> concatLines(updateDescriptions),
updateDescriptions.get(0).getLightIconResource(),
updateDescriptions.get(0).getDarkIconResource());
}
private static boolean allAreStatic(@NonNull Collection<UpdateDescription> updateDescriptions) {

View File

@@ -6,6 +6,7 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.KbsEnclave;
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
import org.thoughtcrime.securesms.groups.GroupsV2AuthorizationMemoryValueCache;
@@ -15,6 +16,7 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.pin.KbsEnclaves;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
@@ -111,11 +113,11 @@ public class ApplicationDependencies {
return groupsV2Operations;
}
public static synchronized @NonNull KeyBackupService getKeyBackupService() {
public static synchronized @NonNull KeyBackupService getKeyBackupService(@NonNull KbsEnclave enclave) {
return getSignalServiceAccountManager().getKeyBackupService(IasKeyStore.getIasKeyStore(application),
BuildConfig.KBS_ENCLAVE_NAME,
Hex.fromStringOrThrow(BuildConfig.KBS_SERVICE_ID),
BuildConfig.KBS_MRENCLAVE,
enclave.getEnclaveName(),
Hex.fromStringOrThrow(enclave.getServiceId()),
enclave.getMrEnclave(),
10);
}
@@ -214,16 +216,6 @@ public class ApplicationDependencies {
return frameRateTracker;
}
public static synchronized @NonNull KeyValueStore getKeyValueStore() {
assertInitialization();
if (keyValueStore == null) {
keyValueStore = provider.provideKeyValueStore();
}
return keyValueStore;
}
public static synchronized @NonNull MegaphoneRepository getMegaphoneRepository() {
assertInitialization();
@@ -281,7 +273,6 @@ public class ApplicationDependencies {
@NonNull LiveRecipientCache provideRecipientCache();
@NonNull JobManager provideJobManager();
@NonNull FrameRateTracker provideFrameRateTracker();
@NonNull KeyValueStore provideKeyValueStore();
@NonNull MegaphoneRepository provideMegaphoneRepository();
@NonNull EarlyMessageCache provideEarlyMessageCache();
@NonNull MessageNotifier provideMessageNotifier();

View File

@@ -99,7 +99,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
Optional.of(new SecurityEventListener(context)),
provideClientZkOperations().getProfileOperations(),
SignalExecutors.newCachedBoundedExecutor("signal-messages", 1, 16));
SignalExecutors.newCachedBoundedExecutor("signal-messages", 1, 16),
FeatureFlags.maxEnvelopeSize());
}
@Override
@@ -153,12 +154,6 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
return new FrameRateTracker(context);
}
@Override
public @NonNull KeyValueStore provideKeyValueStore() {
return new KeyValueStore(context);
}
@Override
public @NonNull MegaphoneRepository provideMegaphoneRepository() {
return new MegaphoneRepository(context);
}

View File

@@ -116,4 +116,16 @@ public class CallParticipant {
public int hashCode() {
return Objects.hash(cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled);
}
@Override
public @NonNull String toString() {
return "CallParticipant{" +
"cameraState=" + cameraState +
", recipient=" + recipient.getId() +
", identityKey=" + (identityKey == null ? "absent" : "present") +
", videoSink=" + (videoSink.getEglBase() == null ? "not initialized" : "initialized") +
", videoEnabled=" + videoEnabled +
", microphoneEnabled=" + microphoneEnabled +
'}';
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.events;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
@@ -13,6 +14,8 @@ import java.util.List;
public class WebRtcViewModel {
public enum State {
IDLE,
// Normal states
CALL_PRE_JOIN,
CALL_INCOMING,
@@ -32,7 +35,14 @@ public class WebRtcViewModel {
// Multiring Hangup States
CALL_ACCEPTED_ELSEWHERE,
CALL_DECLINED_ELSEWHERE,
CALL_ONGOING_ELSEWHERE
CALL_ONGOING_ELSEWHERE;
public boolean isErrorState() {
return this == NETWORK_FAILURE ||
this == RECIPIENT_UNAVAILABLE ||
this == NO_SUCH_USER ||
this == UNTRUSTED_IDENTITY;
}
}
private final @NonNull State state;
@@ -48,7 +58,7 @@ public class WebRtcViewModel {
public WebRtcViewModel(@NonNull State state,
@NonNull Recipient recipient,
@NonNull CameraState localCameraState,
@NonNull BroadcastVideoSink localSink,
@Nullable BroadcastVideoSink localSink,
boolean isBluetoothAvailable,
boolean isMicrophoneEnabled,
boolean isRemoteVideoOffer,
@@ -62,7 +72,7 @@ public class WebRtcViewModel {
this.callConnectedTime = callConnectedTime;
this.remoteParticipants = remoteParticipants;
localParticipant = CallParticipant.createLocal(localCameraState, localSink, isMicrophoneEnabled);
localParticipant = CallParticipant.createLocal(localCameraState, localSink != null ? localSink : new BroadcastVideoSink(null), isMicrophoneEnabled);
}
public @NonNull State getState() {
@@ -97,4 +107,15 @@ public class WebRtcViewModel {
return remoteParticipants;
}
@Override public @NonNull String toString() {
return "WebRtcViewModel{" +
"state=" + state +
", recipient=" + recipient.getId() +
", isBluetoothAvailable=" + isBluetoothAvailable +
", isRemoteVideoOffer=" + isRemoteVideoOffer +
", callConnectedTime=" + callConnectedTime +
", localParticipant=" + localParticipant +
", remoteParticipants=" + remoteParticipants +
'}';
}
}

View File

@@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.groups;
public final class GroupAlreadyExistsException extends GroupChangeException {
public GroupAlreadyExistsException(Throwable throwable) {
super(throwable);
}
}

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