Compare commits

..

549 Commits

Author SHA1 Message Date
Alex Hart 65e88d2d1c Bump version to 4.78.4 2020-11-23 10:48:48 -04:00
Alex Hart cef8aa67dd Updated language translations. 2020-11-23 10:43:51 -04:00
Alex Hart 5941b22eb6 Revert "Move to Signal Protocol written in Rust."
This reverts commit 907e8d93a3.
2020-11-23 10:22:53 -04:00
Alex Hart 9e7c55847e Bump version to 4.78.3 2020-11-20 16:54:53 -04:00
Alex Hart 5209b74605 Updated language translations. 2020-11-20 16:51:57 -04:00
Cody Henthorne b90a74d26a Add additional Group Calling features. 2020-11-20 15:42:46 -05:00
Alan Evans 8c1737e597 Increase uncompressed video attachment size to 300 Mb. 2020-11-20 15:15:42 -04:00
Greyson Parrelli 2ea5bd2d44 Convert GV1->GV2 migration flags to booleans. 2020-11-20 13:50:14 -05:00
Greyson Parrelli 4166e7931e Fix membership diffs that occur during a GV1->GV2 migration.
Co-authored-by: Alan Evans <alan@signal.org>
2020-11-20 13:26:17 -05:00
Alan Evans 89f2c25d73 Display video file output size and duration during clipping.
Prevent video upscale, i.e. use input bit rate if lower than our normal target rates.
Do not time limit videos that are under the send file size.
Increase time limit to 10 minutes to match our lowest acceptable bitrate.
2020-11-20 13:27:58 -04:00
Alan Evans abb1ca2afe Increase in-app recording duration to 60 seconds. 2020-11-20 13:11:48 -04:00
Greyson Parrelli f7befd1593 Block GV1 creation if forced migrations are enabled. 2020-11-20 11:49:18 -05:00
Greyson Parrelli 28511de23c Ensure we properly detect update messages for migrations. 2020-11-20 11:39:55 -05:00
Greyson Parrelli 2ff3d1b7c5 Update phrasing on donate megaphone dismiss button. 2020-11-19 13:46:35 -05:00
Greyson Parrelli fe6ae7e142 Don't show donate or research megaphones on new app installs. 2020-11-19 08:42:35 -05:00
Greyson Parrelli 0da6c83ce4 Bump version to 4.78.2 2020-11-18 19:52:48 -05:00
Greyson Parrelli 184b7db43c Updated language translations. 2020-11-18 19:51:15 -05:00
Greyson Parrelli e442e34c1b More reliably setup initial preferences. 2020-11-18 19:47:27 -05:00
Greyson Parrelli 011efb0ce7 Request READ_PHONE_NUMBERS permission when necessary. 2020-11-18 19:47:27 -05:00
Alex Hart 63d00f87d8 Bump version to 4.78.1 2020-11-18 19:26:12 -04:00
Alex Hart 0323858145 Updated language translations. 2020-11-18 19:22:19 -04:00
Greyson Parrelli a70e8ec7a7 Update reproducible build instructions. 2020-11-18 19:11:48 -04:00
Alex Hart b306a3ef41 Update Dockerfile to utilize new commandline tools distributable. 2020-11-18 19:11:48 -04:00
Greyson Parrelli ccd3467a61 Fix crash with MediaSendActivity progress dialog.
Co-authored-by: Alan Evans <alan@signal.org>
2020-11-18 17:31:43 -05:00
Alex Hart 40338afe7a Bump version to 4.78.0 2020-11-18 16:30:43 -04:00
Alex Hart ff97f6af56 Updated language translations. 2020-11-18 16:30:43 -04:00
Alan Evans 6e7858e00f Restrict video send duration. 2020-11-18 16:30:43 -04:00
Greyson Parrelli 95468c85a8 Break large read receipt messages into chunks. 2020-11-18 14:19:28 -05:00
Cody Henthorne f59e10d82c Fix read/unread conversation list colors. 2020-11-18 14:00:14 -05:00
Alex Hart 930370783e Implement ShortcutInfo for API 30. 2020-11-18 14:25:01 -04:00
Alex Hart 75062ada8a Upgrade SDK to 30. 2020-11-18 13:38:27 -04:00
Cody Henthorne 23618923d8 Attempt to recover from reoccurring exceptions when showing notifications. 2020-11-18 12:28:05 -05:00
Greyson Parrelli f1d3a2f322 Fix Android 11 issue where keyboard wasn't auto-showing for PIN reminders. 2020-11-18 11:57:33 -05:00
Cody Henthorne 3b7fbbaf6e Fix crash when call concluded on non-existent remote peer. 2020-11-18 11:34:06 -05:00
Greyson Parrelli 725d793b20 Fix issue with link preview UI sizing. 2020-11-18 11:33:44 -05:00
Greyson Parrelli 5c3baca055 Add support for a donation megaphone. 2020-11-18 10:33:46 -05:00
Alan Evans 6e5abc92a0 Fix situation where group thread does not yet exist. 2020-11-17 15:52:38 -04:00
Alex Hart 8df6e95781 Stop proximity sensor on pause. 2020-11-17 15:15:13 -04:00
Alex Hart 2290a6c0df Synchronize voice note queue reads and writes. 2020-11-17 14:42:01 -04:00
Jack Lloyd 907e8d93a3 Move to Signal Protocol written in Rust.
Co-authored-by: Alex Hart <alex@signal.org>
2020-11-16 12:28:11 -05:00
Cody Henthorne 918497fb94 Bump version to 4.77.3 2020-11-16 11:49:35 -05:00
Cody Henthorne 3ccd6304c7 Updated language translations. 2020-11-16 11:47:55 -05:00
Greyson Parrelli 51d47adf57 Fix issue where FeatureFlags were triggering listeners for non-changes. 2020-11-16 11:27:58 -05:00
Cody Henthorne f1e5206f56 Fix Invite Friend theming bug. 2020-11-16 10:17:42 -05:00
Cody Henthorne f410635e2c Bump version to 4.77.2 2020-11-13 15:01:12 -05:00
Cody Henthorne 302d57bf19 Updated language translations. 2020-11-13 14:54:53 -05:00
Fumiaki Yoshimatsu 4c301a49b4 Fix appearance of small audio view to show correct background color and the progress circle. 2020-11-13 14:39:46 -05:00
Cody Henthorne 4ecfee292e Fix incorrect restarting and theming when system changes night mode. 2020-11-13 14:39:00 -05:00
Alex Hart 2a193ef455 Refactor with WindowUtil and correct some colors. 2020-11-13 14:43:58 -04:00
Cody Henthorne 96e241ef9c Fix RTL constraints for Help screen. 2020-11-13 12:32:55 -05:00
Cody Henthorne e294a895e8 Bump version to 4.77.1 2020-11-12 12:54:33 -05:00
Cody Henthorne 003b9b1551 Updated language translations. 2020-11-12 12:49:45 -05:00
Alex Hart a4e4af502e Retheme action modes. 2020-11-12 13:42:07 -04:00
Alex Hart 06aada20c1 Open keyboard when we open contact selection from blocked preference. 2020-11-12 13:39:38 -04:00
Greyson Parrelli 2dace38d43 Add support for GV1->GV2 forced migration. 2020-11-12 12:32:10 -05:00
Greyson Parrelli 554aa1ddf0 Trim message bodies at display time. 2020-11-12 12:18:20 -05:00
Greyson Parrelli 3b2a5f1ce3 Remove old profile sharing UI. 2020-11-12 12:01:43 -05:00
Cody Henthorne 3fc4b098e8 Show correct emojis for recipient names. 2020-11-12 11:22:00 -05:00
Fumiaki Yoshimatsu a7d672f6b4 Apply locale updates correctly for appcompat-v1.2.0.
Fixes #9736

See https://developer.android.com/jetpack/androidx/releases/appcompat#1.2.0
for how the code is "correctly" applying a new configuration.

Co-authored-by: Cody Henthorne <cody@signal.org>
2020-11-12 09:56:07 -05:00
Greyson Parrelli 7e347f5cce Add support for manual initiation of GV1->GV2 migrations. 2020-11-12 09:52:21 -05:00
Cody Henthorne 4eaa6ebb47 Bump version to 4.77.0 2020-11-11 15:39:00 -05:00
Cody Henthorne e85ef6881d Updated language translations. 2020-11-11 15:34:15 -05:00
Cody Henthorne b1f6786392 Add initial support for Group Calling. 2020-11-11 15:29:02 -05:00
Cody Henthorne 696fffb603 Improve mention notifications by only showing alerting notifications once. 2020-11-11 15:29:02 -05:00
Alan Evans 3bb366ee04 Do not send typing indicator when deleting from the end & send stopped typing indicator when compose completely empty. 2020-11-11 15:29:02 -05:00
Alex Hart 6a59974f89 Add group settings UI polish. 2020-11-11 15:29:02 -05:00
Alan Evans 8e39267c42 Center number display vertically for non-signal contacts. 2020-11-11 15:29:02 -05:00
Jim Gustafson b937534ce5 Update to RingRTC v2.8.0. 2020-11-11 15:29:01 -05:00
Alex Hart f5b46f7356 Consolidate AnimatedDialog themes to single DayNight theme. 2020-11-11 15:29:01 -05:00
Greyson Parrelli cd58c09be3 Proper handling of GV1->GV2 migrations in storage service. 2020-11-11 15:29:01 -05:00
Greyson Parrelli e8f0038c36 Perform bulk receipt processing in a transaction. 2020-11-11 15:29:01 -05:00
Greyson Parrelli 0b77b33902 Add the ability to trace methods in internal builds.
Currently only for internal builds. Use the @Trace annotation to trace
methods for viewing in Perfetto.
2020-11-11 15:29:01 -05:00
Cody Henthorne c3b5323010 Update assets and themes to leverage DayNight system. 2020-11-11 15:29:01 -05:00
Greyson Parrelli 81eaae4070 Update emoji to Unicode 13.0 2020-11-11 15:29:01 -05:00
Cody Henthorne 65461ce86f Fix incorrect reaction notification copy for various attachment types.
Fixes #10141. Thanks to @Sgn-32 for the initial PR.
2020-11-11 15:29:01 -05:00
Cody Henthorne 536e3139a2 Add foundation for using Android's DayNight theming system. 2020-11-11 15:29:01 -05:00
Alex Hart e9c7b120a0 Improve contact blocking UX via settings. 2020-11-11 15:29:01 -05:00
Cody Henthorne d6a230a235 Update AppCompat to 1.2 along with other Android UI libraries. 2020-11-11 15:29:01 -05:00
Alex Hart 6bf300ada8 Do not require write to read from single backup uri. 2020-11-11 15:29:01 -05:00
Greyson Parrelli d307db8a95 Add the ability to add suggested members after a GV1 migration. 2020-11-11 15:29:01 -05:00
Alex Hart c4c32d80b2 Update CameraX to 1.0.0-beta11. 2020-11-11 15:29:01 -05:00
Greyson Parrelli f4c1e34402 Enforce max envelope size in more places. 2020-11-11 15:29:00 -05:00
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
Alan Evans 810365d334 Bump version to 4.72.2 2020-09-25 15:29:36 -03:00
Alan Evans 4b31510589 Updated language translations. 2020-09-25 15:24:47 -03:00
Alan Evans dfce9a34b8 Fix leave group crash. 2020-09-25 15:18:34 -03:00
Alex Hart dc9370c32b Fix false group name and avatar updates. 2020-09-25 15:18:34 -03:00
Cody Henthorne 8dbc721c08 Fix stale call preview state by finishing when leaving. 2020-09-25 15:18:34 -03:00
Cody Henthorne 6448b84430 Fix various mention issues.
Fixes #9960
2020-09-25 15:18:34 -03:00
Alan Evans 93d6ce40c3 GV2 learn more copy update. 2020-09-25 15:18:34 -03:00
Alan Evans ce5be2c1be Share group link via signal to one recipient. 2020-09-25 12:33:14 -03:00
Alan Evans 20fe837022 Enable and disable group link options with first switch. 2020-09-25 12:32:48 -03:00
Greyson Parrelli e3ce18fa3e Fix possible threading issues with attachment cleanup.
The way things were ordered, it was possible for us to create an
attachment file, but have it 'cleaned up' before we were able to link it
to an attachment row.
2020-09-24 16:51:20 -04:00
Greyson Parrelli 864a1d5e93 Prefer remote value for profile sharing during storage sync. 2020-09-24 12:41:31 -04:00
Greyson Parrelli 9cf7eec247 Log sent timestamps when hitting message processing errors. 2020-09-24 12:26:18 -04:00
Greyson Parrelli d9c15621f6 Log more details around conversation fetch times. 2020-09-24 12:26:02 -04:00
Greyson Parrelli fea14218a9 Don't allow borderless images to have quotes attached.
Fixes #9924
2020-09-24 12:11:40 -04:00
Greyson Parrelli dbbded5250 Bump version to 4.72.1 2020-09-24 10:54:07 -04:00
Greyson Parrelli d65cfc7981 Updated language translations. 2020-09-24 10:53:44 -04:00
Greyson Parrelli dc9124f291 Fix crash in RetrieveProfileJob. 2020-09-24 10:46:56 -04:00
Cody Henthorne 4cd433b6bc Retain call start timestamp per peer to prevent race conditions. 2020-09-24 10:43:39 -04:00
Greyson Parrelli f9a9ee6b0c Bump version to 4.72.0 2020-09-23 16:54:38 -04:00
Greyson Parrelli 1741f7ed58 Updated language translations. 2020-09-23 16:54:38 -04:00
Alan Evans d459c751be Show linked device update message if we don't have the capability to join a group by link. 2020-09-23 16:54:38 -04:00
Alan Evans 34ef8b52f6 Display a loading message if group update message is taking a while to load. 2020-09-23 16:54:38 -04:00
Alan Evans 5ae96905bb Do not allow replying on reactions and messages without visible content. 2020-09-23 16:54:38 -04:00
Alan Evans b1fdbc0151 Refresh own GV2 capability on group create. 2020-09-23 16:54:38 -04:00
Alan Evans a5ad27b5f2 Hide "My contacts" phone number privacy option. 2020-09-23 16:54:38 -04:00
Greyson Parrelli efcd5052a2 Remove Pixel 4 from the CameraX blacklist on Android 11. 2020-09-23 16:54:38 -04:00
Greyson Parrelli f2b10c0ba8 Always include ourselves in optimistic profile fetches. 2020-09-23 16:54:38 -04:00
Greyson Parrelli f182be2d79 Inline CDS feature flag. 2020-09-23 16:54:38 -04:00
Greyson Parrelli 41b10630bb Default to WEBP for sticker contentTypes. 2020-09-23 16:54:38 -04:00
Alan Evans 45915bed90 Inline GV2 feature flag. 2020-09-23 16:54:38 -04:00
Greyson Parrelli a2c2ab428a Fallback to profile fetches for unlisted contacts. 2020-09-23 16:54:38 -04:00
Alan Evans a05f74d302 Do not set color before profile name is known. 2020-09-23 16:54:38 -04:00
Alan Evans 74e94f3a97 Separate capability reads from writes and introduce gv2-2 write flag. 2020-09-23 16:54:38 -04:00
Christian Ascheberg 15ee8c6cac Fix timestamp of missed call record.
Fixes #7647
2020-09-23 16:54:38 -04:00
Alex Hart 18957b1f41 Remove members menu item for group message requests. 2020-09-23 16:54:38 -04:00
Cody Henthorne 29930cac41 Use mention-updated body for unread reaction notification text. 2020-09-23 16:54:38 -04:00
Cody Henthorne e3338dc3ff Add MMS info to conversation settings. 2020-09-23 16:54:38 -04:00
Greyson Parrelli 97b7b4a501 Fix crash when receiving SMS before finishing registration.
If someone has set Signal as the default SMS but has cleared data or
otherwise reset the app's storage state, it can get into a weird
situation. Notably, it'll crash because SmsReceiveJob.onRun() expects
Recipient.self() to be available.

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

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

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

While it'd be tricky to limit jobs with constraints, this just does the
simple thing of not enqueueing unnecessary jobs for constraint-less
jobs.
2020-09-23 16:54:38 -04:00
Alex Hart 5943b9d7d6 Fix sending receipts.
Fixes #10016
2020-09-23 16:54:38 -04:00
Alex Hart 0271e4c918 Add lifecycle check in SnackbarAsyncTask. 2020-09-23 16:54:38 -04:00
Greyson Parrelli 9dc33eff3a Remove thumbnails from the AttachmentDatabase.
Glide can do everything for us now, including video thumbnails.
2020-09-23 16:54:38 -04:00
Jim Gustafson 5aef1c8a68 Update to RingRTC v2.7.0 2020-09-23 16:54:38 -04:00
Alan Evans c608a05270 Prevent a resolve call in main. 2020-09-23 16:54:38 -04:00
Cody Henthorne e2cfd247c3 Fix mention parsing for quotes. 2020-09-23 16:54:38 -04:00
Greyson Parrelli 97eb9154b2 Prevent NPE when setting sticker emoji. 2020-09-23 16:54:38 -04:00
Jim Gustafson d7ff635445 RingRTC: Update to v2.6.0
Co-authored-by: Peter Thatcher <peter@signal.org>
2020-09-23 16:54:38 -04:00
Alan Evans aff57fb54e Create the temporary backup file hidden in the final location.
Fixes #10003
2020-09-23 16:54:38 -04:00
Greyson Parrelli e89285a219 Reduce log noise. 2020-09-23 16:54:38 -04:00
Greyson Parrelli 706f43caa8 Remove AttachmentsV3 feature flag. 2020-09-23 16:54:38 -04:00
Cody Henthorne dc4faf57cb Add foundational UX and state support for Group Calling. 2020-09-23 16:54:38 -04:00
Alex Hart 7baf8052a2 Fix savedInstanceState crash. 2020-09-23 16:54:37 -04:00
Alan Evans d3c59585fd Bump version to 4.71.5 2020-09-14 11:16:09 -03:00
Alan Evans 859bb8dc79 Updated language translations. 2020-09-14 11:12:30 -03:00
Alan Evans 58cd2e07ba Add some required face blurring models back.
Fixes #10009
2020-09-14 10:49:33 -03:00
Greyson Parrelli a5a6fb590a Bump version to 4.71.4 2020-09-10 18:03:51 -04:00
Greyson Parrelli 3619993e68 Updated language translations. 2020-09-10 18:03:28 -04:00
Greyson Parrelli 88e12c78fa Disable mentions megaphone. 2020-09-10 17:54:57 -04:00
Alan Evans 5c285b4ac6 Cycle groups v2 feature flag. 2020-09-10 18:47:36 -03:00
Cody Henthorne c6b729c470 Bump version to 4.71.3 2020-09-10 15:38:55 -04:00
Cody Henthorne 890014759e Updated language translations. 2020-09-10 15:38:12 -04:00
Cody Henthorne 68c1c43381 Update radio styling in storage settings. 2020-09-10 14:40:29 -04:00
Greyson Parrelli d0dfcaaad5 Fix issue with storage key intersections.
- When doing the intersection, ignore keys that have type mismatches (same storageId, different types)
- If we detect that scenario, schedule a force push to happen afterwards
- Also schedule a force push afterwards if we detect that there's keys in the manifest that don't have any storage item on the service
2020-09-10 14:01:41 -04:00
Alan Evans 3cffaddc0a Validate incoming Group lengths and remote delete entries if wrong.
Ignore incoming messages with bad V1 group lengths.
2020-09-10 14:39:29 -03:00
Alex Hart bf4cac0c82 Fix unarchive menu action. 2020-09-10 13:49:16 -03:00
Alex Hart f680749a00 Use proper lifecycle for SimpleTask which touches fragment view. 2020-09-10 13:40:09 -03:00
Cody Henthorne 13a67980d9 Fix wrong timestamp being used when trimming by length. 2020-09-10 12:09:28 -04:00
Alan Evans f110d595d2 Fix selection limit for add members GV1.
Fixes #10005
2020-09-10 09:59:57 -03:00
Cody Henthorne 9c8857352b Bump version to 4.71.2 2020-09-09 16:09:49 -04:00
Cody Henthorne c09a1fdba8 Updated language translations. 2020-09-09 16:04:27 -04:00
Greyson Parrelli cdc7033a51 Update CDS enclave. 2020-09-09 15:38:42 -04:00
Alex Hart fa30c759d7 Fix PIP positioning in video calls. 2020-09-09 13:06:38 -03:00
Fumiaki Yoshimatsu d040be2df0 Use the light styles in the action bar style in the light theme, but keep the dark theme version of it in the action mode.
Fixes #9932
2020-09-09 12:24:45 -03:00
Alan Evans 935c831a7f Fix equality comparison causing blank updates and "The group was updated" messages. 2020-09-09 12:16:09 -03:00
Cody Henthorne 867e95eef1 Re-download sticker if backing file data no longer exists. 2020-09-09 11:15:34 -04:00
Alan Evans 2ee04bd1b6 Insert placeholder group on GV2 storage service sync. 2020-09-09 11:59:09 -03:00
Greyson Parrelli 75d567e555 Implement new client deprecation UI. 2020-09-09 10:22:22 -04:00
Alex Hart d8a489971c Fix missing reply arrows. 2020-09-09 10:42:14 -03:00
Greyson Parrelli 19ce5b5c76 Reduce APNGParser logging. 2020-09-08 18:08:40 -04:00
Greyson Parrelli 7c70ea4d3e Change directory refresh interval to every 24 hours. 2020-09-08 18:06:09 -04:00
Greyson Parrelli 2784285d47 Add support for fetching remote deprecation. 2020-09-08 18:03:56 -04:00
Cody Henthorne c946a7a1d5 Bump version to 4.71.1 2020-09-08 14:30:22 -04:00
Greyson Parrelli 3e60b49b8b Updated language translations. 2020-09-08 14:25:34 -04:00
Cody Henthorne 4e7331bbb8 Fix typo in trim message history copy. 2020-09-08 14:15:10 -04:00
Cody Henthorne b8c7e86223 Fix improper deletion of stickers when restored from backup. 2020-09-08 14:07:56 -04:00
Alex Hart 3b925f8674 Add in-app donate button to preferences screen. 2020-09-08 12:48:52 -03:00
Cody Henthorne f1f6d41c73 Bumped version to 4.71.0 2020-09-08 09:47:58 -04:00
Alan Evans 29ef1cb1be Updated language translations. 2020-09-08 09:47:58 -04:00
Alan Evans 4296085d65 Show no notification actions when the message content is hidden.
Fixes #9928
2020-09-08 09:47:57 -04:00
Alan Evans c797b09228 Set profile sharing based on who added you to the group. 2020-09-08 09:47:57 -04:00
Greyson Parrelli a870ef0030 Set isRecipientUpdate based on delivery status, not address count.
We were setting isRecipientUpdate to `true` incorrectly if there were
unregistered people in the group, resulting in the message not being
rendered on linked devices. Instead of using the address count, we can
just look at the current receipt status of the message.

Fixes #9981
2020-09-08 09:47:57 -04:00
Alan Evans 43ed9e7310 Set discoverable account attribute. 2020-09-08 09:47:57 -04:00
Cody Henthorne bcd27355f9 Add trim conversations by time option. 2020-09-08 09:47:57 -04:00
Alan Evans 6a14dc69c0 Make Group V2 creation driven by version flag. 2020-09-03 20:23:26 -04:00
Jim Gustafson ed9acd25f9 Ensure serial handling of calling events and improve busy UX. 2020-09-03 20:23:26 -04:00
Alan Evans 7b24e66ed3 Phone number privacy settings and certificate support behind feature flag. 2020-09-03 20:23:26 -04:00
Alan Evans abd3d4b546 Group link copy changes. 2020-09-03 20:23:26 -04:00
Alan Evans 4040c4240a Lighter weight mentions membership query. 2020-09-03 20:23:26 -04:00
Alan Evans 1ee747f3ef Always share profile as part of unblocking. 2020-09-03 20:23:26 -04:00
Alan Evans f88874bec8 Default values for member level and admin when no UUID. 2020-09-03 20:23:26 -04:00
Greyson Parrelli ed440a2150 Do not clear UUID for unregistered users.
Otherwise, a number could be unregistered and re-registered by a
different person, assigning a new UUID to an existing RecipientId,
which we never want to do.
2020-09-03 20:23:26 -04:00
Greyson Parrelli 2fd46b196b Show sticker emoji in notification. 2020-09-03 20:23:26 -04:00
Greyson Parrelli 12dfcaf7e7 Log sent timestamp with message sends. 2020-09-03 20:23:26 -04:00
Greyson Parrelli f4a199f621 Add support for animated stickers. 2020-09-03 20:23:26 -04:00
Alan Evans bb708e0aa3 Ignore link preview descriptions that match the title. 2020-09-03 20:23:26 -04:00
Alan Evans d625740ca4 Ensure feature flag is string before cast. 2020-09-03 20:23:26 -04:00
Greyson Parrelli 250402e9b9 Add support for rendering APNGs. 2020-09-03 20:23:26 -04:00
Jim Gustafson 1d2ffe56fb Update to RingRTC v2.5.1 2020-09-01 15:43:07 -04:00
Alan Evans d16c0d2887 Prevent autofill for username editor. 2020-09-01 15:43:07 -04:00
Cody Henthorne b3555f2f94 Use updated Safety Number Change dialog for calls.
Fixes [#9940](https://github.com/signalapp/Signal-Android/issues/9940)
2020-09-01 15:43:07 -04:00
Greyson Parrelli 83a638fc6d Bump version to 4.70.5 2020-09-01 14:56:32 -04:00
Greyson Parrelli f1534a710f Updated language translations. 2020-09-01 14:56:08 -04:00
Greyson Parrelli a16845340b Update CDS enclave. 2020-09-01 14:56:08 -04:00
Alan Evans ffa4725f8e Bump version to 4.70.4 2020-08-31 12:54:22 -03:00
Alan Evans 7792c66c64 Updated language translations. 2020-08-31 12:50:08 -03:00
Alan Evans 1a3985d709 Add QR group link share. 2020-08-31 12:35:38 -03:00
Greyson Parrelli 4714895c59 Do not attempt to send to unregistered users when using CDS flag.
CDS is slow, and unregistered users will always trigger a CDS lookup on
send (since we can't get their UUID).

This starts skipping sends to unregistered users and shortens the time
window to do a full CDS lookup from every 12 hours to every 6 hours.
2020-08-31 11:33:57 -04:00
Fumiaki Yoshimatsu 1e37951701 Use onCreateOptionsMenu when to inflate a menu in order for menu items to appear correctly in RTL languages.
The bug was reported in
https://community.signalusers.org/t/beta-feedback-for-the-upcoming-android-4-70-release/16449/20?u=alan-signal, but it was not necessarily a regression caused by the commit suggested in the forum post. It is more like that the bug was finally exposed by the commit. Before the commit the menu items were not properly aligned nor translated upon configuration changes in RTL languages.
2020-08-31 12:12:15 -03:00
Alan Evans e8be1ad752 Handle GV2 sync messages. 2020-08-31 12:07:03 -03:00
Alan Evans e316a70b6c Fix group limit enforcement and display. 2020-08-31 12:02:50 -03:00
Alan Evans 40a8d21c15 Fix to allow send of Signal invitation SMS to a single person.
Fixes #9970
2020-08-31 11:33:50 -03:00
Alan Evans 28d5ca7ed9 Bump version to 4.70.3 2020-08-28 10:49:31 -03:00
Alan Evans 110b18545f Updated language translations. 2020-08-28 10:44:03 -03:00
Alan Evans a478605da4 Remove requesting members if they are directly added to the group. 2020-08-28 10:32:20 -03:00
Alan Evans f5f1589813 Fix class cast exception when member is approved. 2020-08-28 10:32:20 -03:00
Greyson Parrelli 0c332b6adb Fix corner cases with LinkPreviewViewModel enabled state. 2020-08-28 09:30:03 -04:00
Greyson Parrelli ba712ce357 Fix crash with link preview date formatting on Android < 7.
The 'X' wasn't supported until Android 7.
2020-08-28 09:30:03 -04:00
Alan Evans 2d2395accf Hide block options if recipient is not blockable. 2020-08-28 10:13:23 -03:00
Alan Evans 8634289b7a Bump version to 4.70.2 2020-08-27 17:39:17 -03:00
Alan Evans 45043fb9a8 Updated language translations. 2020-08-27 17:38:23 -03:00
Cody Henthorne 0449795725 Make top gradient disappear with call controls.
Fixes [#9951](https://github.com/signalapp/Signal-Android/issues/9951)
2020-08-27 16:26:15 -04:00
Alan Evans a96093f1b7 Exclude unused facial models from APK. 2020-08-27 17:01:10 -03:00
Alex Hart bd4f7691e9 Add proper background color to camera icon.
Fixes #9945
2020-08-27 17:00:05 -03:00
Alex Hart e12acbae70 Add @ to username in preferences. 2020-08-27 16:34:21 -03:00
Alan Evans 48dc4eac10 Bump version to 4.70.1 2020-08-27 12:25:39 -03:00
Alan Evans a869c92eee Updated language translations. 2020-08-27 12:23:00 -03:00
Greyson Parrelli 4fefd14538 Add unit test to prevent shipping forced feature flags. 2020-08-27 11:14:20 -04:00
Greyson Parrelli c09dbfa47c Prevent corner-case where link previews were generated for SMS.
Also added some hardening to make sure that it's impossible for any link
previews to be fetched if the setting is disabled (this was already the
case in practice, we just have some assertions in there now).

Fixes #9956
2020-08-27 12:12:44 -03:00
Alan Evans d3c9f66de6 Prevent simple dialog flicker. 2020-08-27 12:12:44 -03:00
Alan Evans 01d7694108 Add reset confirmation dialog and copy to group link management screen. 2020-08-27 12:12:44 -03:00
Alex Hart 1425b651d4 Update username UX and UI. 2020-08-27 12:12:44 -03:00
Greyson Parrelli b1befbeefc Add additional LinkPreviewUtil unit tests.
Also updated the date format -- funnily enough Android will work with
either Z or X in the format, but the test JVM will fail if it doesn't
use X. X is definitely the correct thing to use based on the Javadoc, I
think Android's implementation is just a little more lenient.
2020-08-27 09:32:33 -04:00
Panagiotis Vasilopoulos 3a9a84a0b1 Do not attempt to create link previews for .i2p links 2020-08-27 10:01:50 -03:00
Alan Evans 368284cccc Enable auto verify for signal.group links. 2020-08-26 20:48:42 -03:00
Alan Evans ef777f4db9 Make group links remote capable. 2020-08-26 18:02:42 -03:00
Alan Evans a8e4e8e882 Bump version to 4.70.0 2020-08-26 17:25:54 -03:00
Alan Evans cf93760d00 Updated language translations. 2020-08-26 17:25:54 -03:00
Greyson Parrelli dd8b9ff8fb Add support for article dates in link previews. 2020-08-26 17:25:54 -03:00
Alan Evans bfed03b7b5 Manage group links behind feature flag. 2020-08-26 17:25:54 -03:00
Alan Evans 860f06ec9e Join group via invite link. 2020-08-26 12:51:25 -03:00
Alex Hart b58376920f Order pinned conversations in "first added" order instead of reordering as messages come in. 2020-08-26 11:13:01 -03:00
Alan Evans 4ace075ddf Display membership count in link preview description field. 2020-08-26 09:26:25 -03:00
Fumiaki Yoshimatsu dda98a474d Listen to the uiMode configuration changes.
Fixes #9736
Fixes #9922
2020-08-25 17:11:29 -03:00
Alan Evans f1c0df7d87 Update KBS Service Id on staging. 2020-08-25 17:10:07 -03:00
Greyson Parrelli c78e098cb4 Add support for link preview descriptions. 2020-08-25 16:05:39 -04:00
Alex Hart a3438c4f8d Change where edit profile screen requests camera permission. 2020-08-25 16:35:16 -03:00
Alan Evans 92ecf2d5de Add group link join version feature flag. 2020-08-25 16:35:06 -03:00
Alex Hart f18b653725 Fix crash when scrolling to the top of a conversation. 2020-08-25 15:17:21 -03:00
Alex Hart 5128438cfb Fix action bar usability in vertical screen split. 2020-08-25 09:33:11 -03:00
Greyson Parrelli f29f25822b Have DatabaseFactory.getMmsDatabase() return MessageDatabase. 2020-08-24 16:40:47 -04:00
Greyson Parrelli ecfe218840 Bump version to 4.69.6 2020-08-24 14:34:53 -04:00
Greyson Parrelli dd33d2b5d0 Updated language translations. 2020-08-24 14:34:28 -04:00
Alex Hart 12a8d4e10b Fix crash on multi-archive. 2020-08-24 14:34:28 -04:00
Alex Hart c5c2fb31b1 Fix CREATE statement for RecipientDatabase. 2020-08-24 14:25:37 -04:00
Alex Hart 343b7faf98 Bumped version to 4.69.5 2020-08-24 11:16:41 -03:00
Alex Hart 18aa8bbf60 Updated language translations. 2020-08-24 11:16:41 -03:00
Greyson Parrelli a358d1630f Rotate the CDS feature flag. 2020-08-24 11:16:41 -03:00
Alan Evans 01375b321c Don't release bitmaps managed by Glide, and don't cache group preview avatars. 2020-08-24 11:16:41 -03:00
Alex Hart d2739d52e0 Remember position in react-with-any-emoji picker. 2020-08-24 11:16:41 -03:00
Alex Hart 4668510106 Fix crash when archiving multiple conversations. 2020-08-24 11:16:41 -03:00
Alex Hart ffcd311c90 Fix strange long press behavior in convo list.
Fixes #9944
2020-08-24 11:16:41 -03:00
Alex Hart b94a636542 Apply Content-Range and Content-Length headers to resumable upload request. 2020-08-24 11:16:41 -03:00
Jim Gustafson a7aec6bfbc Update to RingRTC v2.5.0 2020-08-24 11:16:41 -03:00
Greyson Parrelli 190ca9eddd Have DatabaseFactory.getSmsDatabase() return MessageDatabase.
Slowly moving towards a single interface.
2020-08-24 11:16:41 -03:00
Greyson Parrelli 2cf9eb69eb Add support for handling unknown protobuf fields. 2020-08-24 11:16:41 -03:00
Alan Evans ffcb90da52 Accept any length group link password. 2020-08-24 11:16:41 -03:00
Alan Evans 878b0c9275 Change group invite link host. 2020-08-24 11:16:41 -03:00
Evan Hahn 5505cb0dea Update donation link in contribution instructions. 2020-08-24 11:16:41 -03:00
Alex Hart 7ac14dccda Refresh username in onResume and utilize imeAction. 2020-08-24 11:16:41 -03:00
Greyson Parrelli 6cffd0a723 Update link preview sync settings.
We need to rotate the link preview setting to avoid newer desktops with
older mobile clients from generating proxy-less previews.
2020-08-24 11:16:41 -03:00
Alan Evans 220ebf93c7 During registration, persist time that call me is available.
Fixes #9926
2020-08-19 16:32:01 -04:00
Greyson Parrelli d0681a5592 Make calling status strings consistent.
Fixes #9904
2020-08-19 16:32:01 -04:00
Alan Evans 09d167c16d Group link preview and info display bottom sheet. 2020-08-19 16:32:01 -04:00
Alan Evans 477bb45df7 Group invite link epoch support. 2020-08-19 16:32:01 -04:00
Alex Hart e006306036 Utilize ItemCallback for ReactWithAnyAdapter.
Fixes #9918
2020-08-19 16:32:01 -04:00
Greyson Parrelli 065cbcf0f9 Bump version to 4.69.4 2020-08-19 16:08:07 -04:00
Greyson Parrelli 7a6b958bbe Updated language translations. 2020-08-19 16:07:46 -04:00
Cody Henthorne ef6a5b6599 Fix bug causing call requests to not be handled properly. 2020-08-19 15:49:16 -04:00
Greyson Parrelli cdae919b5e Bump version to 4.69.3 2020-08-19 10:03:26 -04:00
Greyson Parrelli 12889f4549 Updated language translations. 2020-08-19 10:03:04 -04:00
Greyson Parrelli 089d59b691 Properly mark local note-to-self attachments as uploaded. 2020-08-19 09:59:37 -04:00
Alex Hart b3e247e9cc Fix crash when loading vector from typed array.
Fixes #9933
2020-08-19 10:45:01 -03:00
Greyson Parrelli 56392b87f7 Bump version to 4.69.2 2020-08-18 19:22:42 -04:00
Greyson Parrelli 1b1a4aeb38 Updated language translations. 2020-08-18 19:22:18 -04:00
Greyson Parrelli 16147e0c08 Ensure link preview fetches are canceled on message send. 2020-08-18 18:34:18 -04:00
Cody Henthorne 139317cf1b Improve various aspects of mentions. 2020-08-18 18:13:45 -04:00
Cody Henthorne 72b94127fb Stop muted threads from triggering full notification updates. 2020-08-18 14:15:55 -04:00
Alan Evans 1f1fc94d22 Fix flakey robolectric test. 2020-08-18 11:57:35 -03:00
Greyson Parrelli a574fe026c Bump version to 4.69.1 2020-08-17 12:04:41 -04:00
Greyson Parrelli aa82083d30 Updated language translations. 2020-08-17 12:04:41 -04:00
Greyson Parrelli 08d5df70c2 Don't show the link preview megaphone if previously disabled. 2020-08-17 12:04:41 -04:00
Greyson Parrelli 29b8fa5897 Keep pinned chats at the top of the 'recent' chat section. 2020-08-17 11:12:10 -04:00
Alex Hart e96faf31d4 Fix browser opening on long-press of debug log links. 2020-08-17 11:54:41 -03:00
Greyson Parrelli 157a73aa99 Fix title of conversation pin menu item. 2020-08-17 10:37:17 -04:00
Greyson Parrelli bdd298c8a0 Prevent swipe actions on the 'Pinned' header. 2020-08-17 10:31:28 -04:00
Greyson Parrelli 3f7dd21186 Do not attempt to create link previews for .onion links. 2020-08-17 10:27:30 -04:00
Greyson Parrelli 086b708cf7 Fix NPE when double-tapping the conversation pinning icon. 2020-08-17 10:07:58 -04:00
Alan Evans 57e0e57f48 Fix NPE when link preview image cannot be decoded. 2020-08-15 10:10:15 -03:00
Greyson Parrelli 4b7efbfdc0 Bump version to 4.69.0 2020-08-14 15:54:06 -04:00
Greyson Parrelli 7dc2653042 Updated language translations. 2020-08-14 15:54:06 -04:00
Cody Henthorne e428453835 Fix conversation list bug with pinned chats.
Co-authored-by: Alex Hart <alex@signal.org>
2020-08-14 15:54:06 -04:00
Greyson Parrelli f84c8229de Revert "Replace a call to a deprecated method to update context with the new one."
This reverts commit 5f0d384c9e.

Introduced a bug where the system theme wasn't changing until app
restart.
2020-08-14 15:54:06 -04:00
Alex Hart a73427d68d Fix issues with conversation list position. 2020-08-14 15:54:05 -04:00
Alan Evans e4456bb236 Handle GV2 addresses. 2020-08-14 15:54:05 -04:00
Alex Hart 06eadd0c15 Add mentions unread counter. 2020-08-14 15:54:05 -04:00
Alan Evans 3c90dfa660 Ensure a GV2 update message mentioning you as a new member is first in the list. 2020-08-14 15:54:05 -04:00
Greyson Parrelli ace1b8ee71 Update link preview settings and add some UI polish. 2020-08-14 15:54:05 -04:00
Cody Henthorne 676356e800 Add Mentions Megaphone. 2020-08-14 15:54:05 -04:00
Greyson Parrelli f732e54c22 Update group size flag. 2020-08-14 15:54:05 -04:00
Cody Henthorne cdc2e74f68 Stop conversations without meaningful messages from showing in list. 2020-08-14 15:54:05 -04:00
Cody Henthorne 724f3e872b Update Mention UI/UX to match latest designs. 2020-08-14 15:54:05 -04:00
Alex Hart d63e5165eb Add ability to pin up to 4 conversations. 2020-08-14 15:54:05 -04:00
Cody Henthorne 9892c4392e Fix janky avatar preview transition for notched devices. 2020-08-14 15:54:05 -04:00
Cody Henthorne 5ced1a775c Fix bug where SN change dialog appeared unnecessarily. 2020-08-14 15:54:05 -04:00
Cody Henthorne 761de1318e Update mention data during recipient merge. 2020-08-14 15:54:05 -04:00
Cody Henthorne 02508512d5 Fix incorrect snippet generation by ignoring profile name change messages. 2020-08-14 15:54:05 -04:00
Greyson Parrelli 6e6105af05 Open up link previews to work with all sites. 2020-08-14 15:54:05 -04:00
Jared Andrews d569419e13 Fixes conversation overflow menu items not being tappable.
Fixes #9908
2020-08-13 19:47:46 -04:00
Greyson Parrelli 93f1641803 Bump version to 4.68.8 2020-08-10 21:13:19 -04:00
Greyson Parrelli ff52bf93fa Make the CDS flag remote capable. 2020-08-10 13:27:11 -04:00
Greyson Parrelli a039275a0c Bump version to 4.68.7 2020-08-10 11:40:37 -04:00
Greyson Parrelli a98d10104d Updated language translations. 2020-08-10 11:39:30 -04:00
Alan Evans 8924bc59b1 Hide legacy group warning when GV2 create feature flag is off or MMS is forced.
Fixes #9913
2020-08-08 17:43:07 -03:00
Greyson Parrelli eefe60a9c9 Bump version to 4.68.6 2020-08-07 19:37:05 -04:00
Greyson Parrelli fe1cb3d904 Updated language translations. 2020-08-07 19:36:26 -04:00
Greyson Parrelli 0448278a78 Include a recipient in sent transcripts when possible. 2020-08-07 19:20:35 -04:00
Greyson Parrelli 99c0c2ff4c Fix crash when opening debuglogs during registration. 2020-08-07 19:20:35 -04:00
Greyson Parrelli b369b734ca Improve storage service insert recovery. 2020-08-07 19:20:35 -04:00
Greyson Parrelli 57150a20fd Make verificationV2 a separate flag. 2020-08-07 19:20:35 -04:00
Cody Henthorne 1634d7d531 Show mention picker immediately after @ entered. 2020-08-07 15:27:15 -04:00
Cody Henthorne d563de4207 Add mention detection to search flows. 2020-08-07 15:18:40 -04:00
1702 changed files with 75610 additions and 23280 deletions
+2 -1
View File
@@ -6,6 +6,7 @@ on:
branches: branches:
- 'master' - 'master'
- '4.**' - '4.**'
- '5.**'
jobs: jobs:
build: build:
@@ -21,7 +22,7 @@ jobs:
java-version: 1.8 java-version: 1.8
- name: Install NDK - name: Install NDK
run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;20.0.5594570" --sdk_root=${ANDROID_SDK_ROOT} run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;21.0.6113669" --sdk_root=${ANDROID_SDK_ROOT}
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
+18
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 -1
View File
@@ -1,6 +1,8 @@
.classpath .classpath
captures/ captures/
project.properties project.properties
keystore.debug.properties
keystore.staging.properties
.project .project
.settings .settings
bin/ bin/
@@ -23,5 +25,5 @@ ffpr
test/androidTestEspresso/res/values/arrays.xml test/androidTestEspresso/res/values/arrays.xml
obj/ obj/
jni/libspeex/.deps/ jni/libspeex/.deps/
*.sh
pkcs11.password pkcs11.password
dev.keystore
+1 -1
View File
@@ -83,7 +83,7 @@ There are several other ways to get involved:
* Try to reproduce issues and help with troubleshooting. * Try to reproduce issues and help with troubleshooting.
* Discover solutions to open issues and post any relevant findings. * Discover solutions to open issues and post any relevant findings.
* Test other people's pull requests. * Test other people's pull requests.
* Contribute to Signal via the [Freedom of the Press Foundation's donation page](https://freedom.press/crowdfunding/signal/). * [Donate to Signal.](https://signal.org/donate/)
* Share Signal with your friends and family. * Share Signal with your friends and family.
Signal is made for you. Thank you for your feedback and support. Signal is made for you. Thank you for your feedback and support.
-25
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}
+136 -55
View File
@@ -11,13 +11,15 @@ buildscript {
jcenter { jcenter {
content { content {
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824' includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
includeGroupByRegex "com\\.archinamon.*"
} }
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.6.3' classpath 'com.android.tools.build:gradle:4.0.2'
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0' classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10' classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
classpath 'com.archinamon:android-gradle-aspectj:4.2.0'
} }
} }
@@ -25,9 +27,22 @@ apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf' apply plugin: 'com.google.protobuf'
apply plugin: 'androidx.navigation.safeargs' apply plugin: 'androidx.navigation.safeargs'
apply plugin: 'witness' apply plugin: 'witness'
apply plugin: 'com.archinamon.aspectj-ext'
apply from: 'translations.gradle' apply from: 'translations.gradle'
apply from: 'witness-verifications.gradle' apply from: 'witness-verifications.gradle'
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Internal")) {
aspectj {
includeJar 'sqlcipher'
includeAspectsFromJar 'Signal-Android'
java = JavaVersion.VERSION_1_8
}
} else if (getGradle().getStartParameter().getTaskRequests().toString().contains("Test")) {
aspectj {
compileTests = false
}
}
repositories { repositories {
maven { maven {
url "https://raw.github.com/signalapp/maven/master/photoview/releases/" url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
@@ -80,8 +95,8 @@ protobuf {
} }
} }
def canonicalVersionCode = 686 def canonicalVersionCode = 745
def canonicalVersionName = "4.68.5" def canonicalVersionName = "4.78.4"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['universal' : 0, def abiPostFix = ['universal' : 0,
@@ -90,22 +105,35 @@ def abiPostFix = ['universal' : 0,
'x86' : 3, 'x86' : 3,
'x86_64' : 4] 'x86_64' : 4]
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
android { android {
flavorDimensions "none" flavorDimensions 'distribution', 'environment'
compileSdkVersion 28 compileSdkVersion 30
buildToolsVersion '28.0.3' buildToolsVersion '30.0.2'
useLibrary 'org.apache.http.legacy' useLibrary 'org.apache.http.legacy'
dexOptions { dexOptions {
javaMaxHeapSize "4g" javaMaxHeapSize "4g"
} }
signingConfigs {
if (keystores.debug != null) {
debug {
storeFile file("${project.rootDir}/${keystores.debug.storeFile}")
storePassword keystores.debug.storePassword
keyAlias keystores.debug.keyAlias
keyPassword keystores.debug.keyPassword
}
}
}
defaultConfig { defaultConfig {
versionCode canonicalVersionCode * postFixSize versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName versionName canonicalVersionName
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 28 targetSdkVersion 30
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -119,16 +147,20 @@ android {
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\"" buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"" buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443" buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\"" buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"" buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\"" "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\"" 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('", "') + '"}' buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "int", "TRACE_EVENT_MAX", "2000"
ndk { ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
@@ -163,8 +195,16 @@ android {
exclude 'META-INF/proguard/androidx-annotations.pro' exclude 'META-INF/proguard/androidx-annotations.pro'
} }
aaptOptions {
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl'
}
buildTypes { buildTypes {
debug { debug {
if (keystores['debug'] != null) {
signingConfig signingConfigs.debug
}
isDefault true
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard/proguard-firebase-messaging.pro', 'proguard/proguard-firebase-messaging.pro',
@@ -188,22 +228,9 @@ android {
testProguardFiles 'proguard/proguard-automation.pro', testProguardFiles 'proguard/proguard-automation.pro',
'proguard/proguard.cfg' 'proguard/proguard.cfg'
} }
staging {
initWith debug
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
}
flipper { flipper {
initWith debug initWith debug
isDefault false
minifyEnabled false minifyEnabled false
} }
release { release {
@@ -214,18 +241,53 @@ android {
productFlavors { productFlavors {
play { play {
dimension "none" dimension 'distribution'
isDefault true
ext.websiteUpdateUrl = "null" ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
} }
website { website {
dimension "none" dimension 'distribution'
ext.websiteUpdateUrl = "https://updates.signal.org/android" ext.websiteUpdateUrl = "https://updates.signal.org/android"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\"" 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"
buildConfigField "int", "TRACE_EVENT_MAX", "30_000"
}
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 -> android.applicationVariants.all { variant ->
@@ -256,27 +318,28 @@ android {
dependencies { dependencies {
lintChecks project(':lintchecks') lintChecks project(':lintchecks')
implementation('androidx.appcompat:appcompat:1.1.0-beta01') { implementation ('androidx.appcompat:appcompat:1.2.0') {
force = true force = true
} }
implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0' implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.0.0' implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.navigation:navigation-fragment:2.1.0' implementation 'androidx.navigation:navigation-fragment:2.1.0'
implementation 'androidx.navigation:navigation-ui:2.1.0' implementation 'androidx.navigation:navigation-ui:2.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05' implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0' implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
implementation "androidx.camera:camera-core:1.0.0-beta01" implementation "androidx.camera:camera-core:1.0.0-beta11"
implementation "androidx.camera:camera-camera2:1.0.0-beta01" implementation "androidx.camera:camera-camera2:1.0.0-beta11"
implementation "androidx.camera:camera-lifecycle:1.0.0-beta01" implementation "androidx.camera:camera-lifecycle:1.0.0-beta11"
implementation "androidx.camera:camera-view:1.0.0-alpha18"
implementation "androidx.concurrent:concurrent-futures:1.0.0" implementation "androidx.concurrent:concurrent-futures:1.0.0"
implementation "androidx.autofill:autofill:1.0.0" implementation "androidx.autofill:autofill:1.0.0"
implementation "androidx.paging:paging-common:2.1.2" implementation "androidx.paging:paging-common:2.1.2"
@@ -295,6 +358,7 @@ dependencies {
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1' 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: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.conscrypt:conscrypt-android:2.0.0'
implementation 'org.signal:aesgcmprovider:0.0.3' implementation 'org.signal:aesgcmprovider:0.0.3'
@@ -304,7 +368,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar' implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.4.1' implementation 'org.signal:ringrtc-android:2.8.3'
implementation "me.leolin:ShortcutBadger:1.1.16" implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0' implementation 'se.emilsjolander:stickylistheaders:2.7.0'
@@ -358,27 +422,27 @@ dependencies {
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.11.1' testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'org.mockito:mockito-core:1.9.5' testImplementation 'org.mockito:mockito-core:2.8.9'
testImplementation 'org.powermock:powermock-api-mockito:1.6.5' testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
testImplementation 'org.powermock:powermock-module-junit4:1.6.5' testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.5' testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.4'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.5' testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
testImplementation 'androidx.test:core:1.2.0' 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' 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.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
} }
dependencyVerification { dependencyVerification {
configuration = '(play|website)(Debug|Release)RuntimeClasspath' configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
} }
def assembleWebsiteDescriptor = { variant, file -> def assembleWebsiteDescriptor = { variant, file ->
if (file.exists()) { if (file.exists()) {
MessageDigest md = MessageDigest.getInstance("SHA-256"); MessageDigest md = MessageDigest.getInstance("SHA-256");
@@ -422,28 +486,24 @@ def signProductionRelease = { variant ->
task signProductionPlayRelease { task signProductionPlayRelease {
doLast { 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 { task signProductionWebsiteRelease {
doLast { 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') } File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
assembleWebsiteDescriptor(variant, signedRelease) assembleWebsiteDescriptor(variant, signedRelease)
} }
} }
tasks.whenTaskAdded { task ->
if (task.name.equals("assemblePlayRelease")) {
task.finalizedBy signProductionPlayRelease
}
if (task.name.equals("assembleWebsiteRelease")) {
task.finalizedBy signProductionWebsiteRelease
}
}
def getLastCommitTimestamp() { def getLastCommitTimestamp() {
new ByteArrayOutputStream().withStream { os -> new ByteArrayOutputStream().withStream { os ->
def result = exec { def result = exec {
@@ -455,3 +515,24 @@ def getLastCommitTimestamp() {
return os.toString() + "000" return os.toString() + "000"
} }
} }
tasks.withType(Test) {
testLogging {
events "failed"
exceptionFormat "full"
showCauses true
showExceptions true
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;
}
}
+8 -1
View File
@@ -5,5 +5,12 @@
<application <application
android:name=".FlipperApplicationContext" android:name=".FlipperApplicationContext"
tools:replace="android:name"/> tools:replace="android:name">
<activity
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" />
</application>
</manifest> </manifest>
@@ -15,7 +15,9 @@ import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteStatement; import net.sqlcipher.database.SQLiteStatement;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.logging.Log;
import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -29,13 +31,23 @@ import java.util.Map;
*/ */
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> { public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
public FlipperSqlCipherAdapter(Context context) { public FlipperSqlCipherAdapter(Context context) {
super(context); super(context);
} }
@Override @Override
public List<Descriptor> getDatabases() { public List<Descriptor> getDatabases() {
return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext()))); try {
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
databaseHelperField.setAccessible(true);
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
} catch (Exception e) {
Log.i(TAG, "Unable to use reflection to access raw database.", e);
}
return Collections.emptyList();
} }
@Override @Override
-4
View File
@@ -1,4 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="app_name">Signal (Flipper)</string>
</resources>
@@ -0,0 +1,103 @@
package org.thoughtcrime.securesms.tracing;
import androidx.annotation.NonNull;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.thoughtcrime.securesms.logging.Log;
/**
* Uses AspectJ to augment relevant methods to be traced with the {@link TracerImpl}.
*/
@Aspect
public class TraceAspect {
@Pointcut("within(@org.thoughtcrime.securesms.tracing.Trace *)")
public void withinAnnotatedClass() {}
@Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
public void methodInsideAnnotatedType() {}
@Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")
public void constructorInsideAnnotatedType() {}
@Pointcut("execution(@org.thoughtcrime.securesms.tracing.Trace * *(..)) || methodInsideAnnotatedType()")
public void annotatedMethod() {}
@Pointcut("execution(@org.thoughtcrime.securesms.tracing.Trace *.new(..)) || constructorInsideAnnotatedType()")
public void annotatedConstructor() {}
@Pointcut("execution(* *(..)) && within(net.sqlcipher.database.*)")
public void sqlcipher() {}
@Pointcut("execution(* net.sqlcipher.database.SQLiteDatabase.rawQuery(..)) || " +
"execution(* net.sqlcipher.database.SQLiteDatabase.query(..)) || " +
"execution(* net.sqlcipher.database.SQLiteDatabase.insert(..)) || " +
"execution(* net.sqlcipher.database.SQLiteDatabase.insertOrThrow(..)) || " +
"execution(* net.sqlcipher.database.SQLiteDatabase.insertWithOnConflict(..)) || " +
"execution(* net.sqlcipher.database.SQLiteDatabase.delete(..)) || " +
"execution(* net.sqlcipher.database.SQLiteDatabase.update(..))")
public void sqlcipherQuery() {}
@Around("annotatedMethod() || annotatedConstructor() || (sqlcipher() && !sqlcipherQuery())")
public @NonNull Object profile(@NonNull ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
Tracer.getInstance().start(methodName);
Object result = joinPoint.proceed();
Tracer.getInstance().end(methodName);
return result;
}
@Around("sqlcipherQuery()")
public @NonNull Object profileQuery(@NonNull ProceedingJoinPoint joinPoint) throws Throwable {
String table;
String query;
if (joinPoint.getSignature().getName().equals("query")) {
if (joinPoint.getArgs().length == 9) {
table = (String) joinPoint.getArgs()[1];
query = (String) joinPoint.getArgs()[3];
} else if (joinPoint.getArgs().length == 7 || joinPoint.getArgs().length == 8) {
table = (String) joinPoint.getArgs()[0];
query = (String) joinPoint.getArgs()[2];
} else {
table = "N/A";
query = "N/A";
}
} else if (joinPoint.getSignature().getName().equals("rawQuery")) {
table = "";
query = (String) joinPoint.getArgs()[0];
} else if (joinPoint.getSignature().getName().equals("insert")) {
table = (String) joinPoint.getArgs()[0];
query = "";
} else if (joinPoint.getSignature().getName().equals("insertOrThrow")) {
table = (String) joinPoint.getArgs()[0];
query = "";
} else if (joinPoint.getSignature().getName().equals("insertWithOnConflict")) {
table = (String) joinPoint.getArgs()[0];
query = "";
} else if (joinPoint.getSignature().getName().equals("delete")) {
table = (String) joinPoint.getArgs()[0];
query = (String) joinPoint.getArgs()[1];
} else if (joinPoint.getSignature().getName().equals("update")) {
table = (String) joinPoint.getArgs()[0];
query = (String) joinPoint.getArgs()[2];
} else {
table = "N/A";
query = "N/A";
}
query = query == null ? "null" : query;
query = "[" + table + "] " + query;
String methodName = joinPoint.getSignature().toShortString();
Tracer.getInstance().start(methodName, "query", query);
Object result = joinPoint.proceed();
Tracer.getInstance().end(methodName);
return result;
}
}
@@ -0,0 +1,193 @@
package org.thoughtcrime.securesms.tracing;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.trace.TraceProtos;
import org.thoughtcrime.securesms.trace.TraceProtos.Trace;
import org.thoughtcrime.securesms.trace.TraceProtos.TracePacket;
import org.thoughtcrime.securesms.trace.TraceProtos.TrackDescriptor;
import org.thoughtcrime.securesms.trace.TraceProtos.TrackEvent;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A class to create Perfetto-compatible traces. Currently keeps the entire trace in memory to
* avoid weirdness with synchronizing to disk.
*
* Some general info on how the Perfetto format works:
* - The file format is just a Trace proto (see Trace.proto)
* - The Trace proto is just a series of TracePackets
* - TracePackets can describe:
* - Threads
* - Start of a method
* - End of a method
* - (And a bunch of other stuff that's not relevant to use at this point)
*
* We keep a circular buffer of TracePackets for method calls, and we keep a separate list of
* TracePackets for threads so we don't lose any of those.
*
* Serializing is just a matter of throwing all the TracePackets we have into a proto.
*
* Note: This class aims to be largely-thread-safe, but prioritizes speed and memory efficiency
* above all else. These methods are going to be called very quickly from every thread imaginable,
* and we want to create as little overhead as possible. The idea being that it's ok if we don't,
* for example, keep a perfect circular buffer size if it allows us to reduce overhead. The only
* cost of screwing up would be dropping a trace packet or something, which, while sad, won't affect
* how the app functions.
*/
public final class TracerImpl implements Tracer {
private static final int TRUSTED_SEQUENCE_ID = 1;
private static final byte[] SYNCHRONIZATION_MARKER = UuidUtil.toByteArray(UUID.fromString("82477a76-b28d-42ba-81dc-33326d57a079"));
private final Clock clock;
private final Map<Long, TracePacket> threadPackets;
private final Queue<TracePacket> eventPackets;
private final AtomicInteger eventCount;
TracerImpl() {
this.clock = SystemClock::elapsedRealtimeNanos;
this.threadPackets = new ConcurrentHashMap<>();
this.eventPackets = new ConcurrentLinkedQueue<>();
this.eventCount = new AtomicInteger(0);
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public void start(@NonNull String methodName) {
long time = clock.getTimeNanos();
Thread currentThread = Thread.currentThread();
if (!threadPackets.containsKey(currentThread.getId())) {
threadPackets.put(currentThread.getId(), forThread(currentThread));
}
addPacket(forMethodStart(methodName, time, currentThread.getId()));
}
@Override
public void start(@NonNull String methodName, @NonNull String key, @NonNull String value) {
long time = clock.getTimeNanos();
Thread currentThread = Thread.currentThread();
if (!threadPackets.containsKey(currentThread.getId())) {
threadPackets.put(currentThread.getId(), forThread(currentThread));
}
addPacket(forMethodStart(methodName, time, currentThread.getId(), key, value));
}
@Override
public void end(@NonNull String methodName) {
addPacket(forMethodEnd(methodName, clock.getTimeNanos(), Thread.currentThread().getId()));
}
public @NonNull byte[] serialize() {
Trace.Builder trace = Trace.newBuilder();
for (TracePacket thread : threadPackets.values()) {
trace.addPacket(thread);
}
for (TracePacket event : eventPackets) {
trace.addPacket(event);
}
trace.addPacket(forSynchronization(clock.getTimeNanos()));
return trace.build().toByteArray();
}
/**
* Attempts to add a packet to our list while keeping the size of our circular buffer in-check.
* The tracking of the event count is not perfectly thread-safe, but doing it in a thread-safe
* way would likely involve adding a lock, which we really don't want to do, since it'll add
* unnecessary overhead.
*
* Note that we keep track of the event count separately because
* {@link ConcurrentLinkedQueue#size()} is NOT a constant-time operation.
*/
private void addPacket(@NonNull TracePacket packet) {
eventPackets.add(packet);
int size = eventCount.incrementAndGet();
for (int i = size; i > BuildConfig.TRACE_EVENT_MAX; i--) {
eventPackets.poll();
eventCount.decrementAndGet();
}
}
private static TracePacket forThread(@NonNull Thread thread) {
return TracePacket.newBuilder()
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
.setTrackDescriptor(TrackDescriptor.newBuilder()
.setUuid(thread.getId())
.setName(thread.getName()))
.build();
}
private static TracePacket forMethodStart(@NonNull String name, long time, long threadId) {
return TracePacket.newBuilder()
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
.setTimestamp(time)
.setTrackEvent(TrackEvent.newBuilder()
.setTrackUuid(threadId)
.setName(name)
.setType(TrackEvent.Type.TYPE_SLICE_BEGIN))
.build();
}
private static TracePacket forMethodStart(@NonNull String name, long time, long threadId, @NonNull String key, @NonNull String value) {
return TracePacket.newBuilder()
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
.setTimestamp(time)
.setTrackEvent(TrackEvent.newBuilder()
.setTrackUuid(threadId)
.setName(name)
.setType(TrackEvent.Type.TYPE_SLICE_BEGIN)
.addDebugAnnotations(TraceProtos.DebugAnnotation.newBuilder()
.setName(key)
.setStringValue(value)
.build()))
.build();
}
private static TracePacket forMethodEnd(@NonNull String name, long time, long threadId) {
return TracePacket.newBuilder()
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
.setTimestamp(time)
.setTrackEvent(TrackEvent.newBuilder()
.setTrackUuid(threadId)
.setName(name)
.setType(TrackEvent.Type.TYPE_SLICE_END))
.build();
}
private static TracePacket forSynchronization(long time) {
return TracePacket.newBuilder()
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
.setTimestamp(time)
.setSynchronizationMarker(ByteString.copyFrom(SYNCHRONIZATION_MARKER))
.build();
}
private interface Clock {
long getTimeNanos();
}
}
+151
View File
@@ -0,0 +1,151 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
syntax = "proto2";
package signal;
option java_package = "org.thoughtcrime.securesms.trace";
option java_outer_classname = "TraceProtos";
/*
* Minimal interface needed to work with Perfetto.
*
* https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/trace/trace.proto
*/
message Trace {
repeated TracePacket packet = 1;
}
message TracePacket {
optional uint64 timestamp = 8;
optional uint32 timestamp_clock_id = 58;
oneof data {
TrackEvent track_event = 11;
TrackDescriptor track_descriptor = 60;
bytes synchronization_marker = 36;
}
oneof optional_trusted_packet_sequence_id {
uint32 trusted_packet_sequence_id = 10;
}
}
message TrackEvent {
repeated uint64 category_iids = 3;
repeated string categories = 22;
repeated DebugAnnotation debug_annotations = 4;
oneof name_field {
uint64 name_iid = 10;
string name = 23;
}
enum Type {
TYPE_UNSPECIFIED = 0;
TYPE_SLICE_BEGIN = 1;
TYPE_SLICE_END = 2;
TYPE_INSTANT = 3;
TYPE_COUNTER = 4;
}
optional Type type = 9;
optional uint64 track_uuid = 11;
optional int64 counter_value = 30;
oneof timestamp {
int64 timestamp_delta_us = 1;
int64 timestamp_absolute_us = 16;
}
oneof thread_time {
int64 thread_time_delta_us = 2;
int64 thread_time_absolute_us = 17;
}
}
message TrackDescriptor {
optional uint64 uuid = 1;
optional uint64 parent_uuid = 5;
optional string name = 2;
optional ThreadDescriptor thread = 4;
optional CounterDescriptor counter = 8;
}
message ThreadDescriptor {
optional int32 pid = 1;
optional int32 tid = 2;
optional string thread_name = 5;
}
message CounterDescriptor {
enum BuiltinCounterType {
COUNTER_UNSPECIFIED = 0;
COUNTER_THREAD_TIME_NS = 1;
COUNTER_THREAD_INSTRUCTION_COUNT = 2;
}
enum Unit {
UNIT_UNSPECIFIED = 0;
UNIT_TIME_NS = 1;
UNIT_COUNT = 2;
UNIT_SIZE_BYTES = 3;
}
optional BuiltinCounterType type = 1;
repeated string categories = 2;
optional Unit unit = 3;
optional int64 unit_multiplier = 4;
optional bool is_incremental = 5;
}
message DebugAnnotation {
message NestedValue {
enum NestedType {
UNSPECIFIED = 0;
DICT = 1;
ARRAY = 2;
}
optional NestedType nested_type = 1;
repeated string dict_keys = 2;
repeated NestedValue dict_values = 3;
repeated NestedValue array_values = 4;
optional int64 int_value = 5;
optional double double_value = 6;
optional bool bool_value = 7;
optional string string_value = 8;
}
oneof name_field {
uint64 name_iid = 1;
string name = 10;
}
oneof value {
bool bool_value = 2;
uint64 uint_value = 3;
int64 int_value = 4;
double double_value = 5;
string string_value = 6;
uint64 pointer_value = 7;
NestedValue nested_value = 8;
}
}
@@ -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>
+62 -15
View File
@@ -3,9 +3,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms"> package="org.thoughtcrime.securesms">
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle" /> <uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle,androidx.camera.view" />
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS" <permission android:name="${applicationId}.ACCESS_SECRETS"
android:label="Access to TextSecure Secrets" android:label="Access to TextSecure Secrets"
android:protectionLevel="signature" /> android:protectionLevel="signature" />
@@ -35,8 +35,10 @@
<uses-permission android:name="android.permission.SEND_SMS"/> <uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.WRITE_SMS"/> <uses-permission android:name="android.permission.WRITE_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<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" /> <uses-permission android:name="android.permission.CAMERA" />
@@ -113,7 +115,7 @@
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" /> <meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" /> <meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity" <activity android:name=".WebRtcCallActivity"
android:theme="@style/TextSecure.LightTheme.WebRTCCall" android:theme="@style/TextSecure.LightTheme.WebRTCCall"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
@@ -136,7 +138,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.MainActivity" /> android:value=".MainActivity" />
</activity> </activity>
<activity android:name=".PromptMmsActivity" <activity android:name=".PromptMmsActivity"
@@ -223,6 +225,23 @@
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" /> <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter> </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" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="signal.group"/>
</intent-filter>
<meta-data android:name="com.sec.minimode.icon.portrait.normal" <meta-data android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher" /> android:resource="@mipmap/ic_launcher" />
<meta-data android:name="com.sec.minimode.icon.landscape.normal" <meta-data android:name="com.sec.minimode.icon.landscape.normal"
@@ -260,6 +279,10 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" /> android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.managegroup.ManageGroupActivity" <activity android:name=".groups.ui.managegroup.ManageGroupActivity"
android:windowSoftInputMode="stateAlwaysHidden" android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -421,7 +444,7 @@
android:theme="@style/TextSecure.FullScreenMedia" android:theme="@style/TextSecure.FullScreenMedia"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".BlockedContactsActivity" <activity android:name=".blocked.BlockedUsersActivity"
android:theme="@style/TextSecure.LightTheme" android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -510,11 +533,28 @@
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity" <activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" /> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/> <activity android:name=".megaphone.ClientDeprecatedActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" />
<service android:enabled="true" android:name=".service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/> <service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/> <service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/> <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" <service android:name=".service.QuickResponseService"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE" android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
android:exported="true" > android:exported="true" >
@@ -627,18 +667,25 @@
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" /> <receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
<provider android:name=".providers.PartProvider" <provider android:name=".providers.PartProvider"
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:exported="false" android:exported="false"
android:authorities="org.thoughtcrime.provider.securesms" /> android:authorities="${applicationId}.part" />
<provider android:name=".providers.BlobContentProvider"
android:authorities="${applicationId}.blob"
android:exported="false"
android:grantUriPermissions="true" />
<provider android:name=".providers.MmsBodyProvider" <provider android:name=".providers.MmsBodyProvider"
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:exported="false" android:exported="false"
android:authorities="org.thoughtcrime.provider.securesms.mms" /> android:authorities="${applicationId}.mms" />
<provider android:name="androidx.core.content.FileProvider" <provider android:name="androidx.core.content.FileProvider"
android:authorities="org.thoughtcrime.securesms.fileprovider" android:authorities="${applicationId}.fileprovider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
@@ -647,23 +694,23 @@
</provider> </provider>
<provider android:name=".database.DatabaseContentProviders$Conversation" <provider android:name=".database.DatabaseContentProviders$Conversation"
android:authorities="org.thoughtcrime.securesms.database.conversation" android:authorities="${applicationId}.database.conversation"
android:exported="false" /> android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$ConversationList" <provider android:name=".database.DatabaseContentProviders$ConversationList"
android:authorities="org.thoughtcrime.securesms.database.conversationlist" android:authorities="${applicationId}.database.conversationlist"
android:exported="false" /> android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Attachment" <provider android:name=".database.DatabaseContentProviders$Attachment"
android:authorities="org.thoughtcrime.securesms.database.attachment" android:authorities="${applicationId}.database.attachment"
android:exported="false" /> android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Sticker" <provider android:name=".database.DatabaseContentProviders$Sticker"
android:authorities="org.thoughtcrime.securesms.database.sticker" android:authorities="${applicationId}.database.sticker"
android:exported="false" /> android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$StickerPack" <provider android:name=".database.DatabaseContentProviders$StickerPack"
android:authorities="org.thoughtcrime.securesms.database.stickerpack" android:authorities="${applicationId}.database.stickerpack"
android:exported="false" /> android:exported="false" />
<receiver android:name=".service.BootReceiver"> <receiver android:name=".service.BootReceiver">
Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 KiB

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 KiB

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 KiB

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 KiB

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 KiB

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 KiB

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 KiB

After

Width:  |  Height:  |  Size: 685 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 KiB

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.
Binary file not shown.
@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.thoughtcrime.securesms.mediasend.camerax; package androidx.camera.view;
import android.Manifest.permission; import android.Manifest.permission;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@@ -45,43 +45,44 @@ import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope; import androidx.annotation.RestrictTo.Scope;
import androidx.camera.core.Camera; import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector; import androidx.camera.core.CameraSelector;
import androidx.camera.core.DisplayOrientedMeteringPointFactory;
import androidx.camera.core.FocusMeteringAction; import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult; import androidx.camera.core.FocusMeteringResult;
import androidx.camera.core.ImageCapture; import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.OnImageCapturedCallback; import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
import androidx.camera.core.ImageCapture.OnImageSavedCallback;
import androidx.camera.core.ImageProxy; import androidx.camera.core.ImageProxy;
import androidx.camera.core.Logger;
import androidx.camera.core.MeteringPoint; import androidx.camera.core.MeteringPoint;
import androidx.camera.core.MeteringPointFactory;
import androidx.camera.core.VideoCapture;
import androidx.camera.core.VideoCapture.OnVideoSavedCallback;
import androidx.camera.core.impl.LensFacingConverter; import androidx.camera.core.impl.LensFacingConverter;
import androidx.camera.core.impl.utils.executor.CameraXExecutors; import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback; import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures; import androidx.camera.core.impl.utils.futures.Futures;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.R; import java.io.File;
import org.thoughtcrime.securesms.logging.Log;
import java.io.FileDescriptor;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
/** /**
* A {@link View} that displays a preview of the camera with methods {@link * A {@link View} that displays a preview of the camera with methods {@link
* #takePicture(Executor, OnImageCapturedCallback)}, * #takePicture(Executor, OnImageCapturedCallback)},
* {@link #startRecording(FileDescriptor, Executor, VideoCapture.OnVideoSavedCallback)} and {@link #stopRecording()}. * {@link #takePicture(ImageCapture.OutputFileOptions, Executor, OnImageSavedCallback)},
* {@link #startRecording(File , Executor , OnVideoSavedCallback callback)}
* and {@link #stopRecording()}.
* *
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must * <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link * be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
* LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera. * LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
*/ */
// Begin Signal Custom Code Block
@RequiresApi(21) @RequiresApi(21)
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
// End Signal Custom Code Block public final class SignalCameraView extends FrameLayout {
public final class CameraXView extends FrameLayout { static final String TAG = SignalCameraView.class.getSimpleName();
static final String TAG = CameraXView.class.getSimpleName();
static final boolean DEBUG = false;
static final int INDEFINITE_VIDEO_DURATION = -1; static final int INDEFINITE_VIDEO_DURATION = -1;
static final int INDEFINITE_VIDEO_SIZE = -1; static final int INDEFINITE_VIDEO_SIZE = -1;
@@ -107,7 +108,7 @@ public final class CameraXView extends FrameLayout {
// For pinch-to-zoom // For pinch-to-zoom
private PinchToZoomGestureDetector mPinchToZoomGestureDetector; private PinchToZoomGestureDetector mPinchToZoomGestureDetector;
private boolean mIsPinchToZoomEnabled = true; private boolean mIsPinchToZoomEnabled = true;
CameraXModule mCameraModule; SignalCameraXModule mCameraModule;
private final DisplayManager.DisplayListener mDisplayListener = private final DisplayManager.DisplayListener mDisplayListener =
new DisplayListener() { new DisplayListener() {
@Override @Override
@@ -124,26 +125,25 @@ public final class CameraXView extends FrameLayout {
} }
}; };
private PreviewView mPreviewView; private PreviewView mPreviewView;
private ScaleType mScaleType = ScaleType.CENTER_CROP;
// For accessibility event // For accessibility event
private MotionEvent mUpEvent; private MotionEvent mUpEvent;
public CameraXView(@NonNull Context context) { public SignalCameraView(@NonNull Context context) {
this(context, null); this(context, null);
} }
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs) { public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0); this(context, attrs, 0);
} }
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
init(context, attrs); init(context, attrs);
} }
@RequiresApi(21) @RequiresApi(21)
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) { int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes); super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs); init(context, attrs);
} }
@@ -172,23 +172,23 @@ public final class CameraXView extends FrameLayout {
private void init(Context context, @Nullable AttributeSet attrs) { private void init(Context context, @Nullable AttributeSet attrs) {
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */); addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
mCameraModule = new CameraXModule(this); mCameraModule = new SignalCameraXModule(this);
if (attrs != null) { if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraXView); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
setScaleType( setScaleType(
ScaleType.fromId( PreviewView.ScaleType.fromId(
a.getInteger(R.styleable.CameraXView_scaleType, a.getInteger(R.styleable.CameraView_scaleType,
getScaleType().getId()))); getScaleType().getId())));
setPinchToZoomEnabled( setPinchToZoomEnabled(
a.getBoolean( a.getBoolean(
R.styleable.CameraXView_pinchToZoomEnabled, isPinchToZoomEnabled())); R.styleable.CameraView_pinchToZoomEnabled, isPinchToZoomEnabled()));
setCaptureMode( setCaptureMode(
CaptureMode.fromId( CaptureMode.fromId(
a.getInteger(R.styleable.CameraXView_captureMode, a.getInteger(R.styleable.CameraView_captureMode,
getCaptureMode().getId()))); getCaptureMode().getId())));
int lensFacing = a.getInt(R.styleable.CameraXView_lensFacing, LENS_FACING_BACK); int lensFacing = a.getInt(R.styleable.CameraView_lensFacing, LENS_FACING_BACK);
switch (lensFacing) { switch (lensFacing) {
case LENS_FACING_NONE: case LENS_FACING_NONE:
setCameraLensFacing(null); setCameraLensFacing(null);
@@ -203,7 +203,7 @@ public final class CameraXView extends FrameLayout {
// Unhandled event. // Unhandled event.
} }
int flashMode = a.getInt(R.styleable.CameraXView_flash, 0); int flashMode = a.getInt(R.styleable.CameraView_flash, 0);
switch (flashMode) { switch (flashMode) {
case FLASH_MODE_AUTO: case FLASH_MODE_AUTO:
setFlash(ImageCapture.FLASH_MODE_AUTO); setFlash(ImageCapture.FLASH_MODE_AUTO);
@@ -265,7 +265,7 @@ public final class CameraXView extends FrameLayout {
if (savedState instanceof Bundle) { if (savedState instanceof Bundle) {
Bundle state = (Bundle) savedState; Bundle state = (Bundle) savedState;
super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER)); super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER));
setScaleType(ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE))); setScaleType(PreviewView.ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
setZoomRatio(state.getFloat(EXTRA_ZOOM_RATIO)); setZoomRatio(state.getFloat(EXTRA_ZOOM_RATIO));
setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED)); setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED));
setFlash(FlashModeConverter.valueOf(state.getString(EXTRA_FLASH))); setFlash(FlashModeConverter.valueOf(state.getString(EXTRA_FLASH)));
@@ -298,6 +298,21 @@ public final class CameraXView extends FrameLayout {
dpyMgr.unregisterDisplayListener(mDisplayListener); dpyMgr.unregisterDisplayListener(mDisplayListener);
} }
/**
* Gets the {@link LiveData} of the underlying {@link PreviewView}'s
* {@link PreviewView.StreamState}.
*
* @return A {@link LiveData} containing the {@link PreviewView.StreamState}. Apps can either
* get current value by {@link LiveData#getValue()} or register a observer by
* {@link LiveData#observe}.
* @see PreviewView#getPreviewStreamState()
*/
@NonNull
public LiveData<PreviewView.StreamState> getPreviewStreamState() {
return mPreviewView.getPreviewStreamState();
}
@NonNull
PreviewView getPreviewView() { PreviewView getPreviewView() {
return mPreviewView; return mPreviewView;
} }
@@ -347,11 +362,11 @@ public final class CameraXView extends FrameLayout {
/** /**
* Returns the scale type used to scale the preview. * Returns the scale type used to scale the preview.
* *
* @return The current {@link ScaleType}. * @return The current {@link PreviewView.ScaleType}.
*/ */
@NonNull @NonNull
public ScaleType getScaleType() { public PreviewView.ScaleType getScaleType() {
return mScaleType; return mPreviewView.getScaleType();
} }
/** /**
@@ -359,13 +374,10 @@ public final class CameraXView extends FrameLayout {
* *
* <p>This controls how the view finder should be scaled and positioned within the view. * <p>This controls how the view finder should be scaled and positioned within the view.
* *
* @param scaleType The desired {@link ScaleType}. * @param scaleType The desired {@link PreviewView.ScaleType}.
*/ */
public void setScaleType(@NonNull ScaleType scaleType) { public void setScaleType(@NonNull PreviewView.ScaleType scaleType) {
if (scaleType != mScaleType) { mPreviewView.setScaleType(scaleType);
mScaleType = scaleType;
requestLayout();
}
} }
/** /**
@@ -401,8 +413,10 @@ public final class CameraXView extends FrameLayout {
} }
/** /**
* Sets the maximum video duration before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)} is * Sets the maximum video duration before
* called automatically. Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout. * {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)} is called
* automatically.
* Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
*/ */
private void setMaxVideoDuration(long duration) { private void setMaxVideoDuration(long duration) {
mCameraModule.setMaxVideoDuration(duration); mCameraModule.setMaxVideoDuration(duration);
@@ -417,7 +431,8 @@ public final class CameraXView extends FrameLayout {
} }
/** /**
* Sets the maximum video size in bytes before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)} * Sets the maximum video size in bytes before
* {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)}
* is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction. * is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
*/ */
private void setMaxVideoSize(long size) { private void setMaxVideoSize(long size) {
@@ -435,28 +450,38 @@ public final class CameraXView extends FrameLayout {
mCameraModule.takePicture(executor, callback); mCameraModule.takePicture(executor, callback);
} }
/**
* Takes a picture and calls
* {@link OnImageSavedCallback#onImageSaved(ImageCapture.OutputFileResults)} when done.
*
* <p> The value of {@link ImageCapture.Metadata#isReversedHorizontal()} in the
* {@link ImageCapture.OutputFileOptions} will be overwritten based on camera direction. For
* front camera, it will be set to true; for back camera, it will be set to false.
*
* @param outputFileOptions Options to store the newly captured image.
* @param executor The executor in which the callback methods will be run.
* @param callback Callback which will receive success or failure.
*/
public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
@NonNull Executor executor,
@NonNull OnImageSavedCallback callback) {
mCameraModule.takePicture(outputFileOptions, executor, callback);
}
/** /**
* Takes a video and calls the OnVideoSavedCallback when done. * Takes a video and calls the OnVideoSavedCallback when done.
* *
* @param file The destination. * @param outputFileOptions Options to store the newly captured video.
* @param executor The executor in which the callback methods will be run. * @param executor The executor in which the callback methods will be run.
* @param callback Callback which will receive success or failure. * @param callback Callback which will receive success or failure.
*/ */
// Begin Signal Custom Code Block public void startRecording(@NonNull VideoCapture.OutputFileOptions outputFileOptions,
@RequiresApi(26)
// End Signal Custom Code Block
public void startRecording(// Begin Signal Custom Code Block
@NonNull FileDescriptor file,
// End Signal Custom Code Block
@NonNull Executor executor, @NonNull Executor executor,
@NonNull VideoCapture.OnVideoSavedCallback callback) { @NonNull OnVideoSavedCallback callback) {
mCameraModule.startRecording(file, executor, callback); mCameraModule.startRecording(outputFileOptions, executor, callback);
} }
/** Stops an in progress video. */ /** Stops an in progress video. */
// Begin Signal Custom Code Block
@RequiresApi(26)
// End Signal Custom Code Block
public void stopRecording() { public void stopRecording() {
mCameraModule.stopRecording(); mCameraModule.stopRecording();
} }
@@ -554,7 +579,8 @@ public final class CameraXView extends FrameLayout {
mDownEventTimestamp = System.currentTimeMillis(); mDownEventTimestamp = System.currentTimeMillis();
break; break;
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
if (delta() < ViewConfiguration.getLongPressTimeout()) { if (delta() < ViewConfiguration.getLongPressTimeout()
&& mCameraModule.isBoundToLifecycle()) {
mUpEvent = event; mUpEvent = event;
performClick(); performClick();
} }
@@ -578,19 +604,14 @@ public final class CameraXView extends FrameLayout {
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f; final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
mUpEvent = null; mUpEvent = null;
CameraSelector cameraSelector =
new CameraSelector.Builder().requireLensFacing(
mCameraModule.getLensFacing()).build();
DisplayOrientedMeteringPointFactory pointFactory = new DisplayOrientedMeteringPointFactory(
getDisplay(), cameraSelector, mPreviewView.getWidth(), mPreviewView.getHeight());
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
float aePointWidth = afPointWidth * 1.5f;
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
Camera camera = mCameraModule.getCamera(); Camera camera = mCameraModule.getCamera();
if (camera != null) { if (camera != null) {
MeteringPointFactory pointFactory = mPreviewView.getMeteringPointFactory();
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
float aePointWidth = afPointWidth * 1.5f;
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
ListenableFuture<FocusMeteringResult> future = ListenableFuture<FocusMeteringResult> future =
camera.getCameraControl().startFocusAndMetering( camera.getCameraControl().startFocusAndMetering(
new FocusMeteringAction.Builder(afPoint, new FocusMeteringAction.Builder(afPoint,
@@ -609,7 +630,7 @@ public final class CameraXView extends FrameLayout {
}, CameraXExecutors.directExecutor()); }, CameraXExecutors.directExecutor());
} else { } else {
Log.d(TAG, "cannot access camera"); Logger.d(TAG, "cannot access camera");
} }
return true; return true;
@@ -711,45 +732,11 @@ public final class CameraXView extends FrameLayout {
return mCameraModule.isTorchOn(); return mCameraModule.isTorchOn();
} }
/** Options for scaling the bounds of the view finder to the bounds of this view. */
public enum ScaleType {
/**
* Scale the view finder, maintaining the source aspect ratio, so the view finder fills the
* entire view. This will cause the view finder to crop the source image if the camera
* aspect ratio does not match the view aspect ratio.
*/
CENTER_CROP(0),
/**
* Scale the view finder, maintaining the source aspect ratio, so the view finder is
* entirely contained within the view.
*/
CENTER_INSIDE(1);
private final int mId;
int getId() {
return mId;
}
ScaleType(int id) {
mId = id;
}
static ScaleType fromId(int id) {
for (ScaleType st : values()) {
if (st.mId == id) {
return st;
}
}
throw new IllegalArgumentException();
}
}
/** /**
* The capture mode used by CameraView. * The capture mode used by CameraView.
* *
* <p>This enum can be used to determine which capture mode will be enabled for {@link * <p>This enum can be used to determine which capture mode will be enabled for {@link
* CameraXView}. * SignalCameraView}.
*/ */
public enum CaptureMode { public enum CaptureMode {
/** A mode where image capture is enabled. */ /** A mode where image capture is enabled. */
@@ -832,4 +819,4 @@ public final class CameraXView extends FrameLayout {
public void onScaleEnd(ScaleGestureDetector detector) { public void onScaleEnd(ScaleGestureDetector detector) {
} }
} }
} }
@@ -14,7 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package org.thoughtcrime.securesms.mediasend.camerax; package androidx.camera.view;
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
import android.Manifest.permission; import android.Manifest.permission;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@@ -27,17 +29,21 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission; import androidx.annotation.RequiresPermission;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.Camera; import androidx.camera.core.Camera;
import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraSelector; import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraX;
import androidx.camera.core.ImageCapture; import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.OnImageCapturedCallback; import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
import androidx.camera.core.ImageCapture.OnImageSavedCallback;
import androidx.camera.core.Logger;
import androidx.camera.core.Preview; import androidx.camera.core.Preview;
import androidx.camera.core.TorchState; import androidx.camera.core.TorchState;
import androidx.camera.core.UseCase; import androidx.camera.core.UseCase;
import androidx.camera.core.VideoCapture;
import androidx.camera.core.VideoCapture.OnVideoSavedCallback;
import androidx.camera.core.impl.CameraInternal; import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.LensFacingConverter; import androidx.camera.core.impl.LensFacingConverter;
import androidx.camera.core.impl.VideoCaptureConfig;
import androidx.camera.core.impl.utils.CameraOrientationUtil; import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.camera.core.impl.utils.executor.CameraXExecutors; import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback; import androidx.camera.core.impl.utils.futures.FutureCallback;
@@ -51,11 +57,10 @@ import androidx.lifecycle.OnLifecycleEvent;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.video.VideoUtil; import org.thoughtcrime.securesms.video.VideoUtil;
import java.io.FileDescriptor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@@ -65,13 +70,10 @@ import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
/** CameraX use case operation built on @{link androidx.camera.core}. */ /** CameraX use case operation built on @{link androidx.camera.core}. */
// Begin Signal Custom Code Block
@RequiresApi(21) @RequiresApi(21)
// End Signal Custom Code Block @SuppressLint("RestrictedApi")
final class CameraXModule { final class SignalCameraXModule {
public static final String TAG = "CameraXModule"; public static final String TAG = "CameraXModule";
private static final float UNITY_ZOOM_SCALE = 1f; private static final float UNITY_ZOOM_SCALE = 1f;
@@ -82,13 +84,13 @@ final class CameraXModule {
private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4); private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
private final Preview.Builder mPreviewBuilder; private final Preview.Builder mPreviewBuilder;
private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder; private final VideoCapture.Builder mVideoCaptureBuilder;
private final ImageCapture.Builder mImageCaptureBuilder; private final ImageCapture.Builder mImageCaptureBuilder;
private final CameraXView mCameraXView; private final SignalCameraView mCameraView;
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false); final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
private CameraXView.CaptureMode mCaptureMode = CameraXView.CaptureMode.IMAGE; private SignalCameraView.CaptureMode mCaptureMode = SignalCameraView.CaptureMode.IMAGE;
private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION; private long mMaxVideoDuration = SignalCameraView.INDEFINITE_VIDEO_DURATION;
private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE; private long mMaxVideoSize = SignalCameraView.INDEFINITE_VIDEO_SIZE;
@ImageCapture.FlashMode @ImageCapture.FlashMode
private int mFlash = FLASH_MODE_OFF; private int mFlash = FLASH_MODE_OFF;
@Nullable @Nullable
@@ -110,7 +112,6 @@ final class CameraXModule {
public void onDestroy(LifecycleOwner owner) { public void onDestroy(LifecycleOwner owner) {
if (owner == mCurrentLifecycle) { if (owner == mCurrentLifecycle) {
clearCurrentLifecycle(); clearCurrentLifecycle();
mPreview.setSurfaceProvider(null);
} }
} }
}; };
@@ -123,8 +124,8 @@ final class CameraXModule {
@Nullable @Nullable
ProcessCameraProvider mCameraProvider; ProcessCameraProvider mCameraProvider;
CameraXModule(CameraXView view) { SignalCameraXModule(SignalCameraView view) {
mCameraXView = view; mCameraView = view;
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()), Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
new FutureCallback<ProcessCameraProvider>() { new FutureCallback<ProcessCameraProvider>() {
@@ -149,14 +150,12 @@ final class CameraXModule {
mImageCaptureBuilder = new ImageCapture.Builder().setTargetName("ImageCapture"); mImageCaptureBuilder = new ImageCapture.Builder().setTargetName("ImageCapture");
// Begin Signal Custom Code Block mVideoCaptureBuilder = new VideoCapture.Builder().setTargetName("VideoCapture")
mVideoCaptureConfigBuilder = .setAudioBitRate(VideoUtil.AUDIO_BIT_RATE)
new VideoCaptureConfig.Builder().setTargetName("VideoCapture") .setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
.setAudioBitRate(VideoUtil.AUDIO_BIT_RATE) .setBitRate(VideoUtil.VIDEO_BIT_RATE);
.setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
.setBitRate(VideoUtil.VIDEO_BIT_RATE);
// End Signal Custom Code Block
} }
@RequiresPermission(permission.CAMERA) @RequiresPermission(permission.CAMERA)
void bindToLifecycle(LifecycleOwner lifecycleOwner) { void bindToLifecycle(LifecycleOwner lifecycleOwner) {
mNewLifecycle = lifecycleOwner; mNewLifecycle = lifecycleOwner;
@@ -173,12 +172,15 @@ final class CameraXModule {
} }
clearCurrentLifecycle(); clearCurrentLifecycle();
if (mNewLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
// Lifecycle is already in a destroyed state. Since it may have been a valid
// lifecycle when bound, but became destroyed while waiting for layout, treat this as
// a no-op now that we have cleared the previous lifecycle.
mNewLifecycle = null;
return;
}
mCurrentLifecycle = mNewLifecycle; mCurrentLifecycle = mNewLifecycle;
mNewLifecycle = null; mNewLifecycle = null;
if (mCurrentLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
mCurrentLifecycle = null;
throw new IllegalArgumentException("Cannot bind to lifecycle in a destroyed state.");
}
if (mCameraProvider == null) { if (mCameraProvider == null) {
// try again once the camera provider is no longer null // try again once the camera provider is no longer null
@@ -188,18 +190,18 @@ final class CameraXModule {
Set<Integer> available = getAvailableCameraLensFacing(); Set<Integer> available = getAvailableCameraLensFacing();
if (available.isEmpty()) { if (available.isEmpty()) {
Log.w(TAG, "Unable to bindToLifeCycle since no cameras available"); Logger.w(TAG, "Unable to bindToLifeCycle since no cameras available");
mCameraLensFacing = null; mCameraLensFacing = null;
} }
// Ensure the current camera exists, or default to another camera // Ensure the current camera exists, or default to another camera
if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) { if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) {
Log.w(TAG, "Camera does not exist with direction " + mCameraLensFacing); Logger.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
// Default to the first available camera direction // Default to the first available camera direction
mCameraLensFacing = available.iterator().next(); mCameraLensFacing = available.iterator().next();
Log.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing); Logger.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
} }
// Do not attempt to create use cases for a null cameraLensFacing. This could occur if // Do not attempt to create use cases for a null cameraLensFacing. This could occur if
@@ -216,14 +218,12 @@ final class CameraXModule {
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0 boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|| getDisplayRotationDegrees() == 180; || getDisplayRotationDegrees() == 180;
Rational targetAspectRatio;
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels); int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
// End Signal Custom Code Block // End Signal Custom Code Block
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) { Rational targetAspectRatio;
// mImageCaptureBuilder.setTargetAspectRatio(AspectRatio.RATIO_4_3); if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait)); mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
// End Signal Custom Code Block // End Signal Custom Code Block
@@ -232,7 +232,6 @@ final class CameraXModule {
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait)); mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
// End Signal Custom Code Block // End Signal Custom Code Block
// mImageCaptureBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9);
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9; targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
} }
@@ -245,15 +244,14 @@ final class CameraXModule {
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
Size size = VideoUtil.getVideoRecordingSize(); Size size = VideoUtil.getVideoRecordingSize();
mVideoCaptureConfigBuilder.setTargetResolution(size); mVideoCaptureBuilder.setTargetResolution(size);
mVideoCaptureConfigBuilder.setMaxResolution(size); mVideoCaptureBuilder.setMaxResolution(size);
// End Signal Custom Code Block // End Signal Custom Code Block
mVideoCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation()); mVideoCaptureBuilder.setTargetRotation(getDisplaySurfaceRotation());
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
if (MediaConstraints.isVideoTranscodeAvailable()) { if (MediaConstraints.isVideoTranscodeAvailable()) {
mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.getUseCaseConfig()); mVideoCapture = mVideoCaptureBuilder.build();
} }
// End Signal Custom Code Block // End Signal Custom Code Block
@@ -262,15 +260,15 @@ final class CameraXModule {
mPreviewBuilder.setTargetResolution(new Size(getMeasuredWidth(), height)); mPreviewBuilder.setTargetResolution(new Size(getMeasuredWidth(), height));
mPreview = mPreviewBuilder.build(); mPreview = mPreviewBuilder.build();
mPreview.setSurfaceProvider(mCameraXView.getPreviewView().getPreviewSurfaceProvider()); mPreview.setSurfaceProvider(mCameraView.getPreviewView().getSurfaceProvider());
CameraSelector cameraSelector = CameraSelector cameraSelector =
new CameraSelector.Builder().requireLensFacing(mCameraLensFacing).build(); new CameraSelector.Builder().requireLensFacing(mCameraLensFacing).build();
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) { if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector, mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
mImageCapture, mImageCapture,
mPreview); mPreview);
} else if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) { } else if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector, mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
mVideoCapture, mVideoCapture,
mPreview); mPreview);
@@ -301,7 +299,7 @@ final class CameraXModule {
return; return;
} }
if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) { if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
throw new IllegalStateException("Can not take picture under VIDEO capture mode."); throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
} }
@@ -312,17 +310,32 @@ final class CameraXModule {
mImageCapture.takePicture(executor, callback); mImageCapture.takePicture(executor, callback);
} }
// Begin Signal Custom Code Block public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
@RequiresApi(26) @NonNull Executor executor, OnImageSavedCallback callback) {
public void startRecording(FileDescriptor file, if (mImageCapture == null) {
// End Signal Custom Code Block return;
Executor executor, }
final VideoCapture.OnVideoSavedCallback callback) {
if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
}
if (callback == null) {
throw new IllegalArgumentException("OnImageSavedCallback should not be empty");
}
outputFileOptions.getMetadata().setReversedHorizontal(mCameraLensFacing != null
&& mCameraLensFacing == CameraSelector.LENS_FACING_FRONT);
mImageCapture.takePicture(outputFileOptions, executor, callback);
}
public void startRecording(VideoCapture.OutputFileOptions outputFileOptions,
Executor executor, final OnVideoSavedCallback callback) {
if (mVideoCapture == null) { if (mVideoCapture == null) {
return; return;
} }
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) { if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
throw new IllegalStateException("Can not record video under IMAGE capture mode."); throw new IllegalStateException("Can not record video under IMAGE capture mode.");
} }
@@ -332,15 +345,14 @@ final class CameraXModule {
mVideoIsRecording.set(true); mVideoIsRecording.set(true);
mVideoCapture.startRecording( mVideoCapture.startRecording(
file, outputFileOptions,
executor, executor,
new VideoCapture.OnVideoSavedCallback() { new VideoCapture.OnVideoSavedCallback() {
@Override @Override
// Begin Signal Custom Code Block public void onVideoSaved(
public void onVideoSaved(@NonNull FileDescriptor savedFile) { @NonNull VideoCapture.OutputFileResults outputFileResults) {
// End Signal Custom Code Block
mVideoIsRecording.set(false); mVideoIsRecording.set(false);
callback.onVideoSaved(savedFile); callback.onVideoSaved(outputFileResults);
} }
@Override @Override
@@ -349,15 +361,12 @@ final class CameraXModule {
@NonNull String message, @NonNull String message,
@Nullable Throwable cause) { @Nullable Throwable cause) {
mVideoIsRecording.set(false); mVideoIsRecording.set(false);
Log.e(TAG, message, cause); Logger.e(TAG, message, cause);
callback.onError(videoCaptureError, message, cause); callback.onError(videoCaptureError, message, cause);
} }
}); });
} }
// Begin Signal Custom Code Block
@RequiresApi(26)
// End Signal Custom Code Block
public void stopRecording() { public void stopRecording() {
if (mVideoCapture == null) { if (mVideoCapture == null) {
return; return;
@@ -388,14 +397,15 @@ final class CameraXModule {
@RequiresPermission(permission.CAMERA) @RequiresPermission(permission.CAMERA)
public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) { public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) {
String cameraId; if (mCameraProvider == null) {
try { return false;
cameraId = CameraX.getCameraWithLensFacing(lensFacing); }
} catch (Exception e) { try {
throw new IllegalStateException("Unable to query lens facing.", e); return mCameraProvider.hasCamera(
new CameraSelector.Builder().requireLensFacing(lensFacing).build());
} catch (CameraInfoUnavailableException e) {
return false;
} }
return cameraId != null;
} }
@Nullable @Nullable
@@ -454,7 +464,7 @@ final class CameraXModule {
} }
}, CameraXExecutors.directExecutor()); }, CameraXExecutors.directExecutor());
} else { } else {
Log.e(TAG, "Failed to set zoom ratio"); Logger.e(TAG, "Failed to set zoom ratio");
} }
} }
@@ -486,6 +496,10 @@ final class CameraXModule {
} }
} }
boolean isBoundToLifecycle() {
return mCamera != null;
}
int getRelativeCameraOrientation(boolean compensateForMirroring) { int getRelativeCameraOrientation(boolean compensateForMirroring) {
int rotationDegrees = 0; int rotationDegrees = 0;
if (mCamera != null) { if (mCamera != null) {
@@ -520,6 +534,11 @@ final class CameraXModule {
if (!toUnbind.isEmpty()) { if (!toUnbind.isEmpty()) {
mCameraProvider.unbind(toUnbind.toArray((new UseCase[0]))); mCameraProvider.unbind(toUnbind.toArray((new UseCase[0])));
} }
// Remove surface provider once unbound.
if (mPreview != null) {
mPreview.setSurfaceProvider(null);
}
} }
mCamera = null; mCamera = null;
mCurrentLifecycle = null; mCurrentLifecycle = null;
@@ -532,7 +551,7 @@ final class CameraXModule {
mImageCapture.setTargetRotation(getDisplaySurfaceRotation()); mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
} }
if (mVideoCapture != null && MediaConstraints.isVideoTranscodeAvailable()) { if (mVideoCapture != null) {
mVideoCapture.setTargetRotation(getDisplaySurfaceRotation()); mVideoCapture.setTargetRotation(getDisplaySurfaceRotation());
} }
} }
@@ -567,7 +586,7 @@ final class CameraXModule {
return false; return false;
} }
CameraInternal camera = mImageCapture.getBoundCamera(); CameraInternal camera = mImageCapture.getCamera();
if (camera == null) { if (camera == null) {
return false; return false;
@@ -614,15 +633,15 @@ final class CameraXModule {
} }
public Context getContext() { public Context getContext() {
return mCameraXView.getContext(); return mCameraView.getContext();
} }
public int getWidth() { public int getWidth() {
return mCameraXView.getWidth(); return mCameraView.getWidth();
} }
public int getHeight() { public int getHeight() {
return mCameraXView.getHeight(); return mCameraView.getHeight();
} }
public int getDisplayRotationDegrees() { public int getDisplayRotationDegrees() {
@@ -630,15 +649,15 @@ final class CameraXModule {
} }
protected int getDisplaySurfaceRotation() { protected int getDisplaySurfaceRotation() {
return mCameraXView.getDisplaySurfaceRotation(); return mCameraView.getDisplaySurfaceRotation();
} }
private int getMeasuredWidth() { private int getMeasuredWidth() {
return mCameraXView.getMeasuredWidth(); return mCameraView.getMeasuredWidth();
} }
private int getMeasuredHeight() { private int getMeasuredHeight() {
return mCameraXView.getMeasuredHeight(); return mCameraView.getMeasuredHeight();
} }
@Nullable @Nullable
@@ -647,11 +666,11 @@ final class CameraXModule {
} }
@NonNull @NonNull
public CameraXView.CaptureMode getCaptureMode() { public SignalCameraView.CaptureMode getCaptureMode() {
return mCaptureMode; return mCaptureMode;
} }
public void setCaptureMode(@NonNull CameraXView.CaptureMode captureMode) { public void setCaptureMode(@NonNull SignalCameraView.CaptureMode captureMode) {
this.mCaptureMode = captureMode; this.mCaptureMode = captureMode;
rebindToLifecycle(); rebindToLifecycle();
} }
@@ -0,0 +1,58 @@
package org.signal.glide;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class Log {
private Log() {}
public static void v(@NonNull String tag, @NonNull String message) {
SignalGlideCodecs.getLogProvider().v(tag, message);
}
public static void d(@NonNull String tag, @NonNull String message) {
SignalGlideCodecs.getLogProvider().d(tag, message);
}
public static void i(@NonNull String tag, @NonNull String message) {
SignalGlideCodecs.getLogProvider().i(tag, message);
}
public static void w(@NonNull String tag, @NonNull String message) {
SignalGlideCodecs.getLogProvider().w(tag, message);
}
public static void e(@NonNull String tag, @NonNull String message) {
e(tag, message, null);
}
public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
SignalGlideCodecs.getLogProvider().e(tag, message, throwable);
}
public interface Provider {
void v(@NonNull String tag, @NonNull String message);
void d(@NonNull String tag, @NonNull String message);
void i(@NonNull String tag, @NonNull String message);
void w(@NonNull String tag, @NonNull String message);
void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable);
Provider EMPTY = new Provider() {
@Override
public void v(@NonNull String tag, @NonNull String message) { }
@Override
public void d(@NonNull String tag, @NonNull String message) { }
@Override
public void i(@NonNull String tag, @NonNull String message) { }
@Override
public void w(@NonNull String tag, @NonNull String message) { }
@Override
public void e(@NonNull String tag, @NonNull String message, @NonNull Throwable throwable) { }
};
}
}
@@ -0,0 +1,18 @@
package org.signal.glide;
import androidx.annotation.NonNull;
public final class SignalGlideCodecs {
private static Log.Provider logProvider = Log.Provider.EMPTY;
private SignalGlideCodecs() {}
public static void setLogProvider(@NonNull Log.Provider provider) {
logProvider = provider;
}
public static @NonNull Log.Provider getLogProvider() {
return logProvider;
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng;
import android.content.Context;
import org.signal.glide.common.FrameAnimationDrawable;
import org.signal.glide.apng.decode.APNGDecoder;
import org.signal.glide.common.decode.FrameSeqDecoder;
import org.signal.glide.common.loader.AssetStreamLoader;
import org.signal.glide.common.loader.FileLoader;
import org.signal.glide.common.loader.Loader;
import org.signal.glide.common.loader.ResourceStreamLoader;
/**
* @Description: APNGDrawable
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
public class APNGDrawable extends FrameAnimationDrawable<APNGDecoder> {
public APNGDrawable(Loader provider) {
super(provider);
}
public APNGDrawable(APNGDecoder decoder) {
super(decoder);
}
@Override
protected APNGDecoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener) {
return new APNGDecoder(streamLoader, listener);
}
public static APNGDrawable fromAsset(Context context, String assetPath) {
AssetStreamLoader assetStreamLoader = new AssetStreamLoader(context, assetPath);
return new APNGDrawable(assetStreamLoader);
}
public static APNGDrawable fromFile(String filePath) {
FileLoader fileLoader = new FileLoader(filePath);
return new APNGDrawable(fileLoader);
}
public static APNGDrawable fromResource(Context context, int resId) {
ResourceStreamLoader resourceStreamLoader = new ResourceStreamLoader(context, resId);
return new APNGDrawable(resourceStreamLoader);
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
import org.signal.glide.apng.io.APNGReader;
import java.io.IOException;
/**
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27acTL.27:_The_Animation_Control_Chunk
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
class ACTLChunk extends Chunk {
static final int ID = fourCCToInt("acTL");
int num_frames;
int num_plays;
@Override
void innerParse(APNGReader apngReader) throws IOException {
num_frames = apngReader.readInt();
num_plays = apngReader.readInt();
}
}
@@ -0,0 +1,211 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import org.signal.glide.Log;
import org.signal.glide.apng.io.APNGReader;
import org.signal.glide.apng.io.APNGWriter;
import org.signal.glide.common.decode.Frame;
import org.signal.glide.common.decode.FrameSeqDecoder;
import org.signal.glide.common.io.Reader;
import org.signal.glide.common.loader.Loader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* @Description: APNG4Android
* @Author: pengfei.zhou
* @CreateDate: 2019-05-13
*/
public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
private static final String TAG = APNGDecoder.class.getSimpleName();
private APNGWriter apngWriter;
private int mLoopCount;
private final Paint paint = new Paint();
private class SnapShot {
byte dispose_op;
Rect dstRect = new Rect();
ByteBuffer byteBuffer;
}
private SnapShot snapShot = new SnapShot();
/**
* @param loader webp的reader
* @param renderListener 渲染的回调
*/
public APNGDecoder(Loader loader, FrameSeqDecoder.RenderListener renderListener) {
super(loader, renderListener);
paint.setAntiAlias(true);
}
@Override
protected APNGWriter getWriter() {
if (apngWriter == null) {
apngWriter = new APNGWriter();
}
return apngWriter;
}
@Override
protected APNGReader getReader(Reader reader) {
return new APNGReader(reader);
}
@Override
protected int getLoopCount() {
return mLoopCount;
}
@Override
protected void release() {
snapShot.byteBuffer = null;
apngWriter = null;
}
@Override
protected Rect read(APNGReader reader) throws IOException {
List<Chunk> chunks = APNGParser.parse(reader);
List<Chunk> otherChunks = new ArrayList<>();
boolean actl = false;
APNGFrame lastFrame = null;
byte[] ihdrData = new byte[0];
int canvasWidth = 0, canvasHeight = 0;
for (Chunk chunk : chunks) {
if (chunk instanceof ACTLChunk) {
mLoopCount = ((ACTLChunk) chunk).num_plays;
actl = true;
} else if (chunk instanceof FCTLChunk) {
APNGFrame frame = new APNGFrame(reader, (FCTLChunk) chunk);
frame.prefixChunks = otherChunks;
frame.ihdrData = ihdrData;
frames.add(frame);
lastFrame = frame;
} else if (chunk instanceof FDATChunk) {
if (lastFrame != null) {
lastFrame.imageChunks.add(chunk);
}
} else if (chunk instanceof IDATChunk) {
if (!actl) {
//如果为非APNG图片,则只解码PNG
Frame frame = new StillFrame(reader);
frame.frameWidth = canvasWidth;
frame.frameHeight = canvasHeight;
frames.add(frame);
mLoopCount = 1;
break;
}
if (lastFrame != null) {
lastFrame.imageChunks.add(chunk);
}
} else if (chunk instanceof IHDRChunk) {
canvasWidth = ((IHDRChunk) chunk).width;
canvasHeight = ((IHDRChunk) chunk).height;
ihdrData = ((IHDRChunk) chunk).data;
} else if (!(chunk instanceof IENDChunk)) {
otherChunks.add(chunk);
}
}
frameBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
snapShot.byteBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
return new Rect(0, 0, canvasWidth, canvasHeight);
}
@Override
protected void renderFrame(Frame frame) {
if (frame == null || fullRect == null) {
return;
}
try {
Bitmap bitmap = obtainBitmap(fullRect.width() / sampleSize, fullRect.height() / sampleSize);
Canvas canvas = cachedCanvas.get(bitmap);
if (canvas == null) {
canvas = new Canvas(bitmap);
cachedCanvas.put(bitmap, canvas);
}
if (frame instanceof APNGFrame) {
// 从缓存中恢复当前帧
frameBuffer.rewind();
bitmap.copyPixelsFromBuffer(frameBuffer);
// 开始绘制前,处理快照中的设定
if (this.frameIndex == 0) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
} else {
canvas.save();
canvas.clipRect(snapShot.dstRect);
switch (snapShot.dispose_op) {
// 从快照中恢复上一帧之前的显示内容
case FCTLChunk.APNG_DISPOSE_OP_PREVIOUS:
snapShot.byteBuffer.rewind();
bitmap.copyPixelsFromBuffer(snapShot.byteBuffer);
break;
// 清空上一帧所画区域
case FCTLChunk.APNG_DISPOSE_OP_BACKGROUND:
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
break;
// 什么都不做
case FCTLChunk.APNG_DISPOSE_OP_NON:
default:
break;
}
canvas.restore();
}
// 然后根据dispose设定传递到快照信息中
if (((APNGFrame) frame).dispose_op == FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
if (snapShot.dispose_op != FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
snapShot.byteBuffer.rewind();
bitmap.copyPixelsToBuffer(snapShot.byteBuffer);
}
}
snapShot.dispose_op = ((APNGFrame) frame).dispose_op;
canvas.save();
if (((APNGFrame) frame).blend_op == FCTLChunk.APNG_BLEND_OP_SOURCE) {
canvas.clipRect(
frame.frameX / sampleSize,
frame.frameY / sampleSize,
(frame.frameX + frame.frameWidth) / sampleSize,
(frame.frameY + frame.frameHeight) / sampleSize);
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
}
snapShot.dstRect.set(frame.frameX / sampleSize,
frame.frameY / sampleSize,
(frame.frameX + frame.frameWidth) / sampleSize,
(frame.frameY + frame.frameHeight) / sampleSize);
canvas.restore();
}
//开始真正绘制当前帧的内容
Bitmap inBitmap = obtainBitmap(frame.frameWidth, frame.frameHeight);
recycleBitmap(frame.draw(canvas, paint, sampleSize, inBitmap, getWriter()));
recycleBitmap(inBitmap);
frameBuffer.rewind();
bitmap.copyPixelsToBuffer(frameBuffer);
recycleBitmap(bitmap);
} catch (Throwable t) {
Log.e(TAG, "Failed to render!", t);
}
}
}
@@ -0,0 +1,147 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import org.signal.glide.apng.io.APNGReader;
import org.signal.glide.apng.io.APNGWriter;
import org.signal.glide.common.decode.Frame;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.CRC32;
/**
* @Description: APNG4Android
* @Author: pengfei.zhou
* @CreateDate: 2019-05-13
*/
public class APNGFrame extends Frame<APNGReader, APNGWriter> {
public final byte blend_op;
public final byte dispose_op;
byte[] ihdrData;
List<Chunk> imageChunks = new ArrayList<>();
List<Chunk> prefixChunks = new ArrayList<>();
private static final byte[] sPNGSignatures = {(byte) 137, 80, 78, 71, 13, 10, 26, 10};
private static final byte[] sPNGEndChunk = {0, 0, 0, 0, 0x49, 0x45, 0x4E, 0x44, (byte) 0xAE, 0x42, 0x60, (byte) 0x82};
private static ThreadLocal<CRC32> sCRC32 = new ThreadLocal<>();
private CRC32 getCRC32() {
CRC32 crc32 = sCRC32.get();
if (crc32 == null) {
crc32 = new CRC32();
sCRC32.set(crc32);
}
return crc32;
}
public APNGFrame(APNGReader reader, FCTLChunk fctlChunk) {
super(reader);
blend_op = fctlChunk.blend_op;
dispose_op = fctlChunk.dispose_op;
frameDuration = fctlChunk.delay_num * 1000 / (fctlChunk.delay_den == 0 ? 100 : fctlChunk.delay_den);
frameWidth = fctlChunk.width;
frameHeight = fctlChunk.height;
frameX = fctlChunk.x_offset;
frameY = fctlChunk.y_offset;
}
private int encode(APNGWriter apngWriter) throws IOException {
int fileSize = 8 + 13 + 12;
//prefixChunks
for (Chunk chunk : prefixChunks) {
fileSize += chunk.length + 12;
}
//imageChunks
for (Chunk chunk : imageChunks) {
if (chunk instanceof IDATChunk) {
fileSize += chunk.length + 12;
} else if (chunk instanceof FDATChunk) {
fileSize += chunk.length + 8;
}
}
fileSize += sPNGEndChunk.length;
apngWriter.reset(fileSize);
apngWriter.putBytes(sPNGSignatures);
//IHDR Chunk
apngWriter.writeInt(13);
int start = apngWriter.position();
apngWriter.writeFourCC(IHDRChunk.ID);
apngWriter.writeInt(frameWidth);
apngWriter.writeInt(frameHeight);
apngWriter.putBytes(ihdrData);
CRC32 crc32 = getCRC32();
crc32.reset();
crc32.update(apngWriter.toByteArray(), start, 17);
apngWriter.writeInt((int) crc32.getValue());
//prefixChunks
for (Chunk chunk : prefixChunks) {
if (chunk instanceof IENDChunk) {
continue;
}
reader.reset();
reader.skip(chunk.offset);
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
apngWriter.skip(chunk.length + 12);
}
//imageChunks
for (Chunk chunk : imageChunks) {
if (chunk instanceof IDATChunk) {
reader.reset();
reader.skip(chunk.offset);
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
apngWriter.skip(chunk.length + 12);
} else if (chunk instanceof FDATChunk) {
apngWriter.writeInt(chunk.length - 4);
start = apngWriter.position();
apngWriter.writeFourCC(IDATChunk.ID);
reader.reset();
// skip to fdat data position
reader.skip(chunk.offset + 4 + 4 + 4);
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length - 4);
apngWriter.skip(chunk.length - 4);
crc32.reset();
crc32.update(apngWriter.toByteArray(), start, chunk.length);
apngWriter.writeInt((int) crc32.getValue());
}
}
//endChunk
apngWriter.putBytes(sPNGEndChunk);
return fileSize;
}
@Override
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
try {
int length = encode(writer);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
options.inMutable = true;
options.inBitmap = reusedBitmap;
byte[] bytes = writer.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, length, options);
assert bitmap != null;
canvas.drawBitmap(bitmap, (float) frameX / sampleSize, (float) frameY / sampleSize, paint);
return bitmap;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
@@ -0,0 +1,143 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
import android.content.Context;
import org.signal.glide.apng.io.APNGReader;
import org.signal.glide.common.io.Reader;
import org.signal.glide.common.io.StreamReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* @link {https://www.w3.org/TR/PNG/#5PNG-file-signature}
* @Author: pengfei.zhou
* @CreateDate: 2019-05-13
*/
public class APNGParser {
static class FormatException extends IOException {
FormatException() {
super("APNG Format error");
}
}
public static boolean isAPNG(String filePath) {
InputStream inputStream = null;
try {
inputStream = new FileInputStream(filePath);
return isAPNG(new StreamReader(inputStream));
} catch (Exception e) {
return false;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static boolean isAPNG(Context context, String assetPath) {
InputStream inputStream = null;
try {
inputStream = context.getAssets().open(assetPath);
return isAPNG(new StreamReader(inputStream));
} catch (Exception e) {
return false;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static boolean isAPNG(Context context, int resId) {
InputStream inputStream = null;
try {
inputStream = context.getResources().openRawResource(resId);
return isAPNG(new StreamReader(inputStream));
} catch (Exception e) {
return false;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static boolean isAPNG(Reader in) {
APNGReader reader = (in instanceof APNGReader) ? (APNGReader) in : new APNGReader(in);
try {
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
throw new FormatException();
}
while (reader.available() > 0) {
Chunk chunk = parseChunk(reader);
if (chunk instanceof ACTLChunk) {
return true;
}
}
} catch (IOException e) {
return false;
}
return false;
}
public static List<Chunk> parse(APNGReader reader) throws IOException {
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
throw new FormatException();
}
List<Chunk> chunks = new ArrayList<>();
while (reader.available() > 0) {
chunks.add(parseChunk(reader));
}
return chunks;
}
private static Chunk parseChunk(APNGReader reader) throws IOException {
int offset = reader.position();
int size = reader.readInt();
int fourCC = reader.readFourCC();
Chunk chunk;
if (fourCC == ACTLChunk.ID) {
chunk = new ACTLChunk();
} else if (fourCC == FCTLChunk.ID) {
chunk = new FCTLChunk();
} else if (fourCC == FDATChunk.ID) {
chunk = new FDATChunk();
} else if (fourCC == IDATChunk.ID) {
chunk = new IDATChunk();
} else if (fourCC == IENDChunk.ID) {
chunk = new IENDChunk();
} else if (fourCC == IHDRChunk.ID) {
chunk = new IHDRChunk();
} else {
chunk = new Chunk();
}
chunk.offset = offset;
chunk.fourcc = fourCC;
chunk.length = size;
chunk.parse(reader);
chunk.crc = reader.readInt();
return chunk;
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
import android.text.TextUtils;
import org.signal.glide.apng.io.APNGReader;
import java.io.IOException;
/**
* @Description: Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(231-1)字节
* Chunk Type Code (数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成
* Chunk Data (数据块数据) 可变长度 存储按照Chunk Type Code指定的数据
* CRC (循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码
* @Link https://www.w3.org/TR/PNG
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
class Chunk {
int length;
int fourcc;
int crc;
int offset;
static int fourCCToInt(String fourCC) {
if (TextUtils.isEmpty(fourCC) || fourCC.length() != 4) {
return 0xbadeffff;
}
return (fourCC.charAt(0) & 0xff)
| (fourCC.charAt(1) & 0xff) << 8
| (fourCC.charAt(2) & 0xff) << 16
| (fourCC.charAt(3) & 0xff) << 24
;
}
void parse(APNGReader reader) throws IOException {
int available = reader.available();
innerParse(reader);
int offset = available - reader.available();
if (offset > length) {
throw new IOException("Out of chunk area");
} else if (offset < length) {
reader.skip(length - offset);
}
}
void innerParse(APNGReader reader) throws IOException {
}
}
@@ -0,0 +1,121 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
import org.signal.glide.apng.io.APNGReader;
import java.io.IOException;
/**
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
* @see {link=https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fcTL.27:_The_Frame_Control_Chunk}
*/
class FCTLChunk extends Chunk {
static final int ID = fourCCToInt("fcTL");
int sequence_number;
/**
* x_offset >= 0
* y_offset >= 0
* width > 0
* height > 0
* x_offset + width <= 'IHDR' width
* y_offset + height <= 'IHDR' height
*/
/**
* Width of the following frame.
*/
int width;
/**
* Height of the following frame.
*/
int height;
/**
* X position at which to render the following frame.
*/
int x_offset;
/**
* Y position at which to render the following frame.
*/
int y_offset;
/**
* The delay_num and delay_den parameters together specify a fraction indicating the time to
* display the current frame, in seconds. If the denominator is 0, it is to be treated as if it
* were 100 (that is, delay_num then specifies 1/100ths of a second).
* If the the value of the numerator is 0 the decoder should render the next frame as quickly as
* possible, though viewers may impose a reasonable lower bound.
* <p>
* Frame timings should be independent of the time required for decoding and display of each frame,
* so that animations will run at the same rate regardless of the performance of the decoder implementation.
*/
/**
* Frame delay fraction numerator.
*/
short delay_num;
/**
* Frame delay fraction denominator.
*/
short delay_den;
/**
* Type of frame area disposal to be done after rendering this frame.
* dispose_op specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
* If the first 'fcTL' chunk uses a dispose_op of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
*/
byte dispose_op;
/**
* Type of frame area rendering for this frame.
*/
byte blend_op;
/**
* No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
*/
static final int APNG_DISPOSE_OP_NON = 0;
/**
* The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
*/
static final int APNG_DISPOSE_OP_BACKGROUND = 1;
/**
* The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
*/
static final int APNG_DISPOSE_OP_PREVIOUS = 2;
/**
* blend_op<code> specifies whether the frame is to be alpha blended into the current output buffer content,
* or whether it should completely replace its region in the output buffer.
*/
/**
* All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
*/
static final int APNG_BLEND_OP_SOURCE = 0;
/**
* The frame should be composited onto the output buffer based on its alpha,
* using a simple OVER operation as described in the Alpha Channel Processing section of the Extensions
* to the PNG Specification, Version 1.2.0. Note that the second variation of the sample code is applicable.
*/
static final int APNG_BLEND_OP_OVER = 1;
@Override
void innerParse(APNGReader reader) throws IOException {
sequence_number = reader.readInt();
width = reader.readInt();
height = reader.readInt();
x_offset = reader.readInt();
y_offset = reader.readInt();
delay_num = reader.readShort();
delay_den = reader.readShort();
dispose_op = reader.peek();
blend_op = reader.peek();
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
import org.signal.glide.apng.io.APNGReader;
import java.io.IOException;
/**
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fdAT.27:_The_Frame_Data_Chunk
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
class FDATChunk extends Chunk {
static final int ID = fourCCToInt("fdAT");
int sequence_number;
@Override
void innerParse(APNGReader reader) throws IOException {
sequence_number = reader.readInt();
}
}
@@ -0,0 +1,15 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
/**
* @Description: 作用描述
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
class IDATChunk extends Chunk {
static final int ID = fourCCToInt("IDAT");
}
@@ -0,0 +1,15 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
/**
* @Description: 作用描述
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
class IENDChunk extends Chunk {
static final int ID = Chunk.fourCCToInt("IEND");
}
@@ -0,0 +1,45 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
import org.signal.glide.apng.io.APNGReader;
import java.io.IOException;
/**
* The IHDR chunk shall be the first chunk in the PNG datastream. It contains:
* <p>
* Width 4 bytes
* Height 4 bytes
* Bit depth 1 byte
* Colour type 1 byte
* Compression method 1 byte
* Filter method 1 byte
* Interlace method 1 byte
*
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
class IHDRChunk extends Chunk {
static final int ID = fourCCToInt("IHDR");
/**
* 图像宽度,以像素为单位
*/
int width;
/**
* 图像高度,以像素为单位
*/
int height;
byte[] data = new byte[5];
@Override
void innerParse(APNGReader reader) throws IOException {
width = reader.readInt();
height = reader.readInt();
reader.read(data, 0, data.length);
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.decode;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import org.signal.glide.apng.io.APNGReader;
import org.signal.glide.apng.io.APNGWriter;
import org.signal.glide.common.decode.Frame;
import java.io.IOException;
/**
* @Description: APNG4Android
* @Author: pengfei.zhou
* @CreateDate: 2019-05-13
*/
public class StillFrame extends Frame<APNGReader, APNGWriter> {
public StillFrame(APNGReader reader) {
super(reader);
}
@Override
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
options.inMutable = true;
options.inBitmap = reusedBitmap;
Bitmap bitmap = null;
try {
reader.reset();
bitmap = BitmapFactory.decodeStream(reader.toInputStream(), null, options);
assert bitmap != null;
paint.setXfermode(null);
canvas.drawBitmap(bitmap, 0, 0, paint);
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.io;
import android.text.TextUtils;
import org.signal.glide.common.io.FilterReader;
import org.signal.glide.common.io.Reader;
import java.io.IOException;
/**
* @Description: APNGReader
* @Author: pengfei.zhou
* @CreateDate: 2019-05-13
*/
public class APNGReader extends FilterReader {
private static ThreadLocal<byte[]> __intBytes = new ThreadLocal<>();
protected static byte[] ensureBytes() {
byte[] bytes = __intBytes.get();
if (bytes == null) {
bytes = new byte[4];
__intBytes.set(bytes);
}
return bytes;
}
public APNGReader(Reader in) {
super(in);
}
public int readInt() throws IOException {
byte[] buf = ensureBytes();
read(buf, 0, 4);
return buf[3] & 0xFF |
(buf[2] & 0xFF) << 8 |
(buf[1] & 0xFF) << 16 |
(buf[0] & 0xFF) << 24;
}
public short readShort() throws IOException {
byte[] buf = ensureBytes();
read(buf, 0, 2);
return (short) (buf[1] & 0xFF |
(buf[0] & 0xFF) << 8);
}
/**
* @return read FourCC and match chars
*/
public boolean matchFourCC(String chars) throws IOException {
if (TextUtils.isEmpty(chars) || chars.length() != 4) {
return false;
}
int fourCC = readFourCC();
for (int i = 0; i < 4; i++) {
if (((fourCC >> (i * 8)) & 0xff) != chars.charAt(i)) {
return false;
}
}
return true;
}
public int readFourCC() throws IOException {
byte[] buf = ensureBytes();
read(buf, 0, 4);
return buf[0] & 0xff | (buf[1] & 0xff) << 8 | (buf[2] & 0xff) << 16 | (buf[3] & 0xff) << 24;
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.apng.io;
import org.signal.glide.common.io.ByteBufferWriter;
import java.nio.ByteOrder;
/**
* @Description: APNGWriter
* @Author: pengfei.zhou
* @CreateDate: 2019-05-13
*/
public class APNGWriter extends ByteBufferWriter {
public APNGWriter() {
super();
}
public void writeFourCC(int val) {
putByte((byte) (val & 0xff));
putByte((byte) ((val >> 8) & 0xff));
putByte((byte) ((val >> 16) & 0xff));
putByte((byte) ((val >> 24) & 0xff));
}
public void writeInt(int val) {
putByte((byte) ((val >> 24) & 0xff));
putByte((byte) ((val >> 16) & 0xff));
putByte((byte) ((val >> 8) & 0xff));
putByte((byte) (val & 0xff));
}
@Override
public void reset(int size) {
super.reset(size);
this.byteBuffer.order(ByteOrder.BIG_ENDIAN);
}
}
@@ -0,0 +1,253 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.DrawFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.NonNull;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import org.signal.glide.Log;
import org.signal.glide.common.decode.FrameSeqDecoder;
import org.signal.glide.common.loader.Loader;
import java.nio.ByteBuffer;
import java.util.HashSet;
import java.util.Set;
/**
* @Description: Frame animation drawable
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
public abstract class FrameAnimationDrawable<Decoder extends FrameSeqDecoder> extends Drawable implements Animatable2Compat, FrameSeqDecoder.RenderListener {
private static final String TAG = FrameAnimationDrawable.class.getSimpleName();
private final Paint paint = new Paint();
private final Decoder frameSeqDecoder;
private DrawFilter drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private Matrix matrix = new Matrix();
private Set<AnimationCallback> animationCallbacks = new HashSet<>();
private Bitmap bitmap;
private static final int MSG_ANIMATION_START = 1;
private static final int MSG_ANIMATION_END = 2;
private Handler uiHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ANIMATION_START:
for (AnimationCallback animationCallback : animationCallbacks) {
animationCallback.onAnimationStart(FrameAnimationDrawable.this);
}
break;
case MSG_ANIMATION_END:
for (AnimationCallback animationCallback : animationCallbacks) {
animationCallback.onAnimationEnd(FrameAnimationDrawable.this);
}
break;
}
}
};
private Runnable invalidateRunnable = new Runnable() {
@Override
public void run() {
invalidateSelf();
}
};
private boolean autoPlay = true;
public FrameAnimationDrawable(Decoder frameSeqDecoder) {
paint.setAntiAlias(true);
this.frameSeqDecoder = frameSeqDecoder;
}
public FrameAnimationDrawable(Loader provider) {
paint.setAntiAlias(true);
this.frameSeqDecoder = createFrameSeqDecoder(provider, this);
}
public void setAutoPlay(boolean autoPlay) {
this.autoPlay = autoPlay;
}
protected abstract Decoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener);
/**
* @param loopLimit <=0为无限播放,>0为实际播放次数
*/
public void setLoopLimit(int loopLimit) {
frameSeqDecoder.setLoopLimit(loopLimit);
}
public void reset() {
frameSeqDecoder.reset();
}
public void pause() {
frameSeqDecoder.pause();
}
public void resume() {
frameSeqDecoder.resume();
}
public boolean isPaused() {
return frameSeqDecoder.isPaused();
}
@Override
public void start() {
if (autoPlay) {
frameSeqDecoder.start();
} else {
this.frameSeqDecoder.addRenderListener(this);
if (!this.frameSeqDecoder.isRunning()) {
this.frameSeqDecoder.start();
}
}
}
@Override
public void stop() {
if (autoPlay) {
frameSeqDecoder.stop();
} else {
this.frameSeqDecoder.removeRenderListener(this);
this.frameSeqDecoder.stopIfNeeded();
}
}
@Override
public boolean isRunning() {
return frameSeqDecoder.isRunning();
}
@Override
public void draw(Canvas canvas) {
if (bitmap == null || bitmap.isRecycled()) {
return;
}
canvas.setDrawFilter(drawFilter);
canvas.drawBitmap(bitmap, matrix, paint);
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(left, top, right, bottom);
boolean sampleSizeChanged = frameSeqDecoder.setDesiredSize(getBounds().width(), getBounds().height());
matrix.setScale(
1.0f * getBounds().width() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().width(),
1.0f * getBounds().height() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().height());
if (sampleSizeChanged)
this.bitmap = Bitmap.createBitmap(
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
Bitmap.Config.ARGB_8888);
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public void onStart() {
Message.obtain(uiHandler, MSG_ANIMATION_START).sendToTarget();
}
@Override
public void onRender(ByteBuffer byteBuffer) {
if (!isRunning()) {
return;
}
if (this.bitmap == null || this.bitmap.isRecycled()) {
this.bitmap = Bitmap.createBitmap(
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
Bitmap.Config.ARGB_8888);
}
byteBuffer.rewind();
if (byteBuffer.remaining() < this.bitmap.getByteCount()) {
Log.e(TAG, "onRender:Buffer not large enough for pixels");
return;
}
this.bitmap.copyPixelsFromBuffer(byteBuffer);
uiHandler.post(invalidateRunnable);
}
@Override
public void onEnd() {
Message.obtain(uiHandler, MSG_ANIMATION_END).sendToTarget();
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
if (this.autoPlay) {
if (visible) {
if (!isRunning()) {
start();
}
} else if (isRunning()) {
stop();
}
}
return super.setVisible(visible, restart);
}
@Override
public int getIntrinsicWidth() {
try {
return frameSeqDecoder.getBounds().width();
} catch (Exception exception) {
return 0;
}
}
@Override
public int getIntrinsicHeight() {
try {
return frameSeqDecoder.getBounds().height();
} catch (Exception exception) {
return 0;
}
}
@Override
public void registerAnimationCallback(@NonNull AnimationCallback animationCallback) {
this.animationCallbacks.add(animationCallback);
}
@Override
public boolean unregisterAnimationCallback(@NonNull AnimationCallback animationCallback) {
return this.animationCallbacks.remove(animationCallback);
}
@Override
public void clearAnimationCallbacks() {
this.animationCallbacks.clear();
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.decode;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import org.signal.glide.common.io.Reader;
import org.signal.glide.common.io.Writer;
/**
* @Description: One frame in an animation
* @Author: pengfei.zhou
* @CreateDate: 2019-05-13
*/
public abstract class Frame<R extends Reader, W extends Writer> {
protected final R reader;
public int frameWidth;
public int frameHeight;
public int frameX;
public int frameY;
public int frameDuration;
public Frame(R reader) {
this.reader = reader;
}
public abstract Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, W writer);
}
@@ -0,0 +1,539 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.decode;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.glide.Log;
import org.signal.glide.common.executor.FrameDecoderExecutor;
import org.signal.glide.common.io.Reader;
import org.signal.glide.common.io.Writer;
import org.signal.glide.common.loader.Loader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
/**
* @Description: Abstract Frame Animation Decoder
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
public abstract class FrameSeqDecoder<R extends Reader, W extends Writer> {
private static final String TAG = FrameSeqDecoder.class.getSimpleName();
private final int taskId;
private final Loader mLoader;
private final Handler workerHandler;
protected List<Frame> frames = new ArrayList<>();
protected int frameIndex = -1;
private int playCount;
private Integer loopLimit = null;
private Set<RenderListener> renderListeners = new HashSet<>();
private AtomicBoolean paused = new AtomicBoolean(true);
private static final Rect RECT_EMPTY = new Rect();
private Runnable renderTask = new Runnable() {
@Override
public void run() {
if (paused.get()) {
return;
}
if (canStep()) {
long start = System.currentTimeMillis();
long delay = step();
long cost = System.currentTimeMillis() - start;
workerHandler.postDelayed(this, Math.max(0, delay - cost));
for (RenderListener renderListener : renderListeners) {
renderListener.onRender(frameBuffer);
}
} else {
stop();
}
}
};
protected int sampleSize = 1;
private Set<Bitmap> cacheBitmaps = new HashSet<>();
protected Map<Bitmap, Canvas> cachedCanvas = new WeakHashMap<>();
protected ByteBuffer frameBuffer;
protected volatile Rect fullRect;
private W mWriter = getWriter();
private R mReader = null;
/**
* If played all the needed
*/
private boolean finished = false;
private enum State {
IDLE,
RUNNING,
INITIALIZING,
FINISHING,
}
private volatile State mState = State.IDLE;
public Loader getLoader() {
return mLoader;
}
protected abstract W getWriter();
protected abstract R getReader(Reader reader);
protected Bitmap obtainBitmap(int width, int height) {
Bitmap ret = null;
Iterator<Bitmap> iterator = cacheBitmaps.iterator();
while (iterator.hasNext()) {
int reuseSize = width * height * 4;
ret = iterator.next();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (ret != null && ret.getAllocationByteCount() >= reuseSize) {
iterator.remove();
if (ret.getWidth() != width || ret.getHeight() != height) {
ret.reconfigure(width, height, Bitmap.Config.ARGB_8888);
}
ret.eraseColor(0);
return ret;
}
} else {
if (ret != null && ret.getByteCount() >= reuseSize) {
if (ret.getWidth() == width && ret.getHeight() == height) {
iterator.remove();
ret.eraseColor(0);
}
return ret;
}
}
}
try {
Bitmap.Config config = Bitmap.Config.ARGB_8888;
ret = Bitmap.createBitmap(width, height, config);
} catch (OutOfMemoryError e) {
e.printStackTrace();
}
return ret;
}
protected void recycleBitmap(Bitmap bitmap) {
if (bitmap != null && !cacheBitmaps.contains(bitmap)) {
cacheBitmaps.add(bitmap);
}
}
/**
* 解码器的渲染回调
*/
public interface RenderListener {
/**
* 播放开始
*/
void onStart();
/**
* 帧播放
*/
void onRender(ByteBuffer byteBuffer);
/**
* 播放结束
*/
void onEnd();
}
/**
* @param loader webp的reader
* @param renderListener 渲染的回调
*/
public FrameSeqDecoder(Loader loader, @Nullable RenderListener renderListener) {
this.mLoader = loader;
if (renderListener != null) {
this.renderListeners.add(renderListener);
}
this.taskId = FrameDecoderExecutor.getInstance().generateTaskId();
this.workerHandler = new Handler(FrameDecoderExecutor.getInstance().getLooper(taskId));
}
public void addRenderListener(final RenderListener renderListener) {
this.workerHandler.post(new Runnable() {
@Override
public void run() {
renderListeners.add(renderListener);
}
});
}
public void removeRenderListener(final RenderListener renderListener) {
this.workerHandler.post(new Runnable() {
@Override
public void run() {
renderListeners.remove(renderListener);
}
});
}
public void stopIfNeeded() {
this.workerHandler.post(new Runnable() {
@Override
public void run() {
if (renderListeners.size() == 0) {
stop();
}
}
});
}
public Rect getBounds() {
if (fullRect == null) {
if (mState == State.FINISHING) {
Log.e(TAG, "In finishing,do not interrupt");
}
final Thread thread = Thread.currentThread();
workerHandler.post(new Runnable() {
@Override
public void run() {
try {
if (fullRect == null) {
if (mReader == null) {
mReader = getReader(mLoader.obtain());
} else {
mReader.reset();
}
initCanvasBounds(read(mReader));
}
} catch (Exception e) {
e.printStackTrace();
fullRect = RECT_EMPTY;
} finally {
LockSupport.unpark(thread);
}
}
});
LockSupport.park(thread);
}
return fullRect;
}
private void initCanvasBounds(Rect rect) {
fullRect = rect;
frameBuffer = ByteBuffer.allocate((rect.width() * rect.height() / (sampleSize * sampleSize) + 1) * 4);
if (mWriter == null) {
mWriter = getWriter();
}
}
private int getFrameCount() {
return this.frames.size();
}
/**
* @return Loop Count defined in file
*/
protected abstract int getLoopCount();
public void start() {
if (fullRect == RECT_EMPTY) {
return;
}
if (mState == State.RUNNING || mState == State.INITIALIZING) {
Log.i(TAG, debugInfo() + " Already started");
return;
}
if (mState == State.FINISHING) {
Log.e(TAG, debugInfo() + " Processing,wait for finish at " + mState);
}
mState = State.INITIALIZING;
if (Looper.myLooper() == workerHandler.getLooper()) {
innerStart();
} else {
workerHandler.post(new Runnable() {
@Override
public void run() {
innerStart();
}
});
}
}
@WorkerThread
private void innerStart() {
paused.compareAndSet(true, false);
final long start = System.currentTimeMillis();
try {
if (frames.size() == 0) {
try {
if (mReader == null) {
mReader = getReader(mLoader.obtain());
} else {
mReader.reset();
}
initCanvasBounds(read(mReader));
} catch (Throwable e) {
e.printStackTrace();
}
}
} finally {
Log.i(TAG, debugInfo() + " Set state to RUNNING,cost " + (System.currentTimeMillis() - start));
mState = State.RUNNING;
}
if (getNumPlays() == 0 || !finished) {
this.frameIndex = -1;
renderTask.run();
for (RenderListener renderListener : renderListeners) {
renderListener.onStart();
}
} else {
Log.i(TAG, debugInfo() + " No need to started");
}
}
@WorkerThread
private void innerStop() {
workerHandler.removeCallbacks(renderTask);
frames.clear();
for (Bitmap bitmap : cacheBitmaps) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
cacheBitmaps.clear();
if (frameBuffer != null) {
frameBuffer = null;
}
cachedCanvas.clear();
try {
if (mReader != null) {
mReader.close();
mReader = null;
}
if (mWriter != null) {
mWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
release();
mState = State.IDLE;
for (RenderListener renderListener : renderListeners) {
renderListener.onEnd();
}
}
public void stop() {
if (fullRect == RECT_EMPTY) {
return;
}
if (mState == State.FINISHING || mState == State.IDLE) {
Log.i(TAG, debugInfo() + "No need to stop");
return;
}
if (mState == State.INITIALIZING) {
Log.e(TAG, debugInfo() + "Processing,wait for finish at " + mState);
}
mState = State.FINISHING;
if (Looper.myLooper() == workerHandler.getLooper()) {
innerStop();
} else {
workerHandler.post(new Runnable() {
@Override
public void run() {
innerStop();
}
});
}
}
private String debugInfo() {
return "";
}
protected abstract void release();
public boolean isRunning() {
return mState == State.RUNNING || mState == State.INITIALIZING;
}
public boolean isPaused() {
return paused.get();
}
public void setLoopLimit(int limit) {
this.loopLimit = limit;
}
public void reset() {
this.playCount = 0;
this.frameIndex = -1;
this.finished = false;
}
public void pause() {
workerHandler.removeCallbacks(renderTask);
paused.compareAndSet(false, true);
}
public void resume() {
paused.compareAndSet(true, false);
workerHandler.removeCallbacks(renderTask);
workerHandler.post(renderTask);
}
public int getSampleSize() {
return sampleSize;
}
public boolean setDesiredSize(int width, int height) {
boolean sampleSizeChanged = false;
int sample = getDesiredSample(width, height);
if (sample != this.sampleSize) {
this.sampleSize = sample;
sampleSizeChanged = true;
final boolean tempRunning = isRunning();
workerHandler.removeCallbacks(renderTask);
workerHandler.post(new Runnable() {
@Override
public void run() {
innerStop();
try {
initCanvasBounds(read(getReader(mLoader.obtain())));
if (tempRunning) {
innerStart();
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
return sampleSizeChanged;
}
protected int getDesiredSample(int desiredWidth, int desiredHeight) {
if (desiredWidth == 0 || desiredHeight == 0) {
return 1;
}
int radio = Math.min(getBounds().width() / desiredWidth, getBounds().height() / desiredHeight);
int sample = 1;
while ((sample * 2) <= radio) {
sample *= 2;
}
return sample;
}
protected abstract Rect read(R reader) throws IOException;
private int getNumPlays() {
return this.loopLimit != null ? this.loopLimit : this.getLoopCount();
}
private boolean canStep() {
if (!isRunning()) {
return false;
}
if (frames.size() == 0) {
return false;
}
if (getNumPlays() <= 0) {
return true;
}
if (this.playCount < getNumPlays() - 1) {
return true;
} else if (this.playCount == getNumPlays() - 1 && this.frameIndex < this.getFrameCount() - 1) {
return true;
}
finished = true;
return false;
}
@WorkerThread
private long step() {
this.frameIndex++;
if (this.frameIndex >= this.getFrameCount()) {
this.frameIndex = 0;
this.playCount++;
}
Frame frame = getFrame(this.frameIndex);
if (frame == null) {
return 0;
}
renderFrame(frame);
return frame.frameDuration;
}
protected abstract void renderFrame(Frame frame);
private Frame getFrame(int index) {
if (index < 0 || index >= frames.size()) {
return null;
}
return frames.get(index);
}
/**
* Get Indexed frame
*
* @param index <0 means reverse from last index
*/
public Bitmap getFrameBitmap(int index) throws IOException {
if (mState != State.IDLE) {
Log.e(TAG, debugInfo() + ",stop first");
return null;
}
mState = State.RUNNING;
paused.compareAndSet(true, false);
if (frames.size() == 0) {
if (mReader == null) {
mReader = getReader(mLoader.obtain());
} else {
mReader.reset();
}
initCanvasBounds(read(mReader));
}
if (index < 0) {
index += this.frames.size();
}
if (index < 0) {
index = 0;
}
frameIndex = -1;
while (frameIndex < index) {
if (canStep()) {
step();
} else {
break;
}
}
frameBuffer.rewind();
Bitmap bitmap = Bitmap.createBitmap(getBounds().width() / getSampleSize(), getBounds().height() / getSampleSize(), Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(frameBuffer);
innerStop();
return bitmap;
}
}
@@ -0,0 +1,70 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.executor;
import android.os.HandlerThread;
import android.os.Looper;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description: com.github.penfeizhou.animation.executor
* @Author: pengfei.zhou
* @CreateDate: 2019-11-21
*/
public class FrameDecoderExecutor {
private static int sPoolNumber = 4;
private ArrayList<HandlerThread> mHandlerThreadGroup = new ArrayList<>();
private AtomicInteger counter = new AtomicInteger(0);
private FrameDecoderExecutor() {
}
static class Inner {
static final FrameDecoderExecutor sInstance = new FrameDecoderExecutor();
}
public void setPoolSize(int size) {
sPoolNumber = size;
}
public static FrameDecoderExecutor getInstance() {
return Inner.sInstance;
}
public Looper getLooper(int taskId) {
int idx = taskId % sPoolNumber;
if (idx >= mHandlerThreadGroup.size()) {
HandlerThread handlerThread = new HandlerThread("FrameDecoderExecutor-" + idx);
handlerThread.start();
mHandlerThreadGroup.add(handlerThread);
Looper looper = handlerThread.getLooper();
if (looper != null) {
return looper;
} else {
return Looper.getMainLooper();
}
} else {
if (mHandlerThreadGroup.get(idx) != null) {
Looper looper = mHandlerThreadGroup.get(idx).getLooper();
if (looper != null) {
return looper;
} else {
return Looper.getMainLooper();
}
} else {
return Looper.getMainLooper();
}
}
}
public int generateTaskId() {
return counter.getAndIncrement();
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.io;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* @Description: APNG4Android
* @Author: pengfei.zhou
* @CreateDate: 2019-05-14
*/
public class ByteBufferReader implements Reader {
private final ByteBuffer byteBuffer;
public ByteBufferReader(ByteBuffer byteBuffer) {
this.byteBuffer = byteBuffer;
byteBuffer.position(0);
}
@Override
public long skip(long total) throws IOException {
byteBuffer.position((int) (byteBuffer.position() + total));
return total;
}
@Override
public byte peek() throws IOException {
return byteBuffer.get();
}
@Override
public void reset() throws IOException {
byteBuffer.position(0);
}
@Override
public int position() {
return byteBuffer.position();
}
@Override
public int read(byte[] buffer, int start, int byteCount) throws IOException {
byteBuffer.get(buffer, start, byteCount);
return byteCount;
}
@Override
public int available() throws IOException {
return byteBuffer.limit() - byteBuffer.position();
}
@Override
public void close() throws IOException {
}
@Override
public InputStream toInputStream() throws IOException {
return new ByteArrayInputStream(byteBuffer.array());
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.io;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* @Description: ByteBufferWriter
* @Author: pengfei.zhou
* @CreateDate: 2019-05-12
*/
public class ByteBufferWriter implements Writer {
protected ByteBuffer byteBuffer;
public ByteBufferWriter() {
reset(10 * 1024);
}
@Override
public void putByte(byte b) {
byteBuffer.put(b);
}
@Override
public void putBytes(byte[] b) {
byteBuffer.put(b);
}
@Override
public int position() {
return byteBuffer.position();
}
@Override
public void skip(int length) {
byteBuffer.position(length + position());
}
@Override
public byte[] toByteArray() {
return byteBuffer.array();
}
@Override
public void close() {
}
@Override
public void reset(int size) {
if (byteBuffer == null || size > byteBuffer.capacity()) {
byteBuffer = ByteBuffer.allocate(size);
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
}
byteBuffer.clear();
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @Description: FileReader
* @Author: pengfei.zhou
* @CreateDate: 2019-05-23
*/
public class FileReader extends FilterReader {
private final File mFile;
public FileReader(File file) throws IOException {
super(new StreamReader(new FileInputStream(file)));
mFile = file;
}
@Override
public void reset() throws IOException {
reader.close();
reader = new StreamReader(new FileInputStream(mFile));
}
}
@@ -0,0 +1,63 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.io;
import java.io.IOException;
import java.io.InputStream;
/**
* @Description: FilterReader
* @Author: pengfei.zhou
* @CreateDate: 2019-05-23
*/
public class FilterReader implements Reader {
protected Reader reader;
public FilterReader(Reader in) {
this.reader = in;
}
@Override
public long skip(long total) throws IOException {
return reader.skip(total);
}
@Override
public byte peek() throws IOException {
return reader.peek();
}
@Override
public void reset() throws IOException {
reader.reset();
}
@Override
public int position() {
return reader.position();
}
@Override
public int read(byte[] buffer, int start, int byteCount) throws IOException {
return reader.read(buffer, start, byteCount);
}
@Override
public int available() throws IOException {
return reader.available();
}
@Override
public void close() throws IOException {
reader.close();
}
@Override
public InputStream toInputStream() throws IOException {
reset();
return reader.toInputStream();
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.io;
import java.io.IOException;
import java.io.InputStream;
/**
* @link {https://developers.google.com/speed/webp/docs/riff_container#terminology_basics}
* @Author: pengfei.zhou
* @CreateDate: 2019-05-11
*/
public interface Reader {
long skip(long total) throws IOException;
byte peek() throws IOException;
void reset() throws IOException;
int position();
int read(byte[] buffer, int start, int byteCount) throws IOException;
int available() throws IOException;
/**
* close io
*/
void close() throws IOException;
InputStream toInputStream() throws IOException;
}
@@ -0,0 +1,64 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.io;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @Author: pengfei.zhou
* @CreateDate: 2019-05-11
*/
public class StreamReader extends FilterInputStream implements Reader {
private int position;
public StreamReader(InputStream in) {
super(in);
try {
in.reset();
} catch (IOException e) {
// e.printStackTrace();
}
}
@Override
public byte peek() throws IOException {
byte ret = (byte) read();
position++;
return ret;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int ret = super.read(b, off, len);
position += Math.max(0, ret);
return ret;
}
@Override
public synchronized void reset() throws IOException {
super.reset();
position = 0;
}
@Override
public long skip(long n) throws IOException {
long ret = super.skip(n);
position += ret;
return ret;
}
@Override
public int position() {
return position;
}
@Override
public InputStream toInputStream() throws IOException {
return this;
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.io;
import java.io.IOException;
/**
* @Description: APNG4Android
* @Author: pengfei.zhou
* @CreateDate: 2019-05-12
*/
public interface Writer {
void reset(int size);
void putByte(byte b);
void putBytes(byte[] b);
int position();
void skip(int length);
byte[] toByteArray();
void close() throws IOException;
}
@@ -0,0 +1,32 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.loader;
import android.content.Context;
import java.io.IOException;
import java.io.InputStream;
/**
* @Description: 从Asset中读取流
* @Author: pengfei.zhou
* @CreateDate: 2019/3/28
*/
public class AssetStreamLoader extends StreamLoader {
private final Context mContext;
private final String mAssetName;
public AssetStreamLoader(Context context, String assetName) {
mContext = context.getApplicationContext();
mAssetName = assetName;
}
@Override
protected InputStream getInputStream() throws IOException {
return mContext.getAssets().open(mAssetName);
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.loader;
import org.signal.glide.common.io.ByteBufferReader;
import org.signal.glide.common.io.Reader;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* @Description: ByteBufferLoader
* @Author: pengfei.zhou
* @CreateDate: 2019-05-15
*/
public abstract class ByteBufferLoader implements Loader {
public abstract ByteBuffer getByteBuffer();
@Override
public Reader obtain() throws IOException {
return new ByteBufferReader(getByteBuffer());
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.loader;
import org.signal.glide.common.io.FileReader;
import org.signal.glide.common.io.Reader;
import java.io.File;
import java.io.IOException;
/**
* @Description: 从文件加载流
* @Author: pengfei.zhou
* @CreateDate: 2019/3/28
*/
public class FileLoader implements Loader {
private final File mFile;
private Reader mReader;
public FileLoader(String path) {
mFile = new File(path);
}
@Override
public synchronized Reader obtain() throws IOException {
return new FileReader(mFile);
}
}
@@ -0,0 +1,19 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.loader;
import org.signal.glide.common.io.Reader;
import java.io.IOException;
/**
* @Description: Loader
* @Author: pengfei.zhou
* @CreateDate: 2019-05-14
*/
public interface Loader {
Reader obtain() throws IOException;
}
@@ -0,0 +1,32 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.loader;
import android.content.Context;
import java.io.IOException;
import java.io.InputStream;
/**
* @Description: 从资源加载流
* @Author: pengfei.zhou
* @CreateDate: 2019/3/28
*/
public class ResourceStreamLoader extends StreamLoader {
private final Context mContext;
private final int mResId;
public ResourceStreamLoader(Context context, int resId) {
mContext = context.getApplicationContext();
mResId = resId;
}
@Override
protected InputStream getInputStream() throws IOException {
return mContext.getResources().openRawResource(mResId);
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.common.loader;
import org.signal.glide.common.io.Reader;
import org.signal.glide.common.io.StreamReader;
import java.io.IOException;
import java.io.InputStream;
/**
* @Author: pengfei.zhou
* @CreateDate: 2019/3/28
*/
public abstract class StreamLoader implements Loader {
protected abstract InputStream getInputStream() throws IOException;
public final synchronized Reader obtain() throws IOException {
return new StreamReader(getInputStream());
}
}
@@ -1,7 +1,7 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.account.AccountAttributes;
public final class AppCapabilities { public final class AppCapabilities {
@@ -9,12 +9,13 @@ public final class AppCapabilities {
} }
private static final boolean UUID_CAPABLE = false; private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
/** /**
* @param storageCapable Whether or not the user can use storage service. This is another way of * @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not. * asking if the user has set a Signal PIN or not.
*/ */
public static SignalServiceProfile.Capabilities getCapabilities(boolean storageCapable) { public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new SignalServiceProfile.Capabilities(UUID_CAPABLE, FeatureFlags.groupsV2(), storageCapable); return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, FeatureFlags.groupsV1AutoMigration());
} }
} }
@@ -34,6 +34,11 @@ public final class AppInitialization {
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION); TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode()); TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true); TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
TextSecurePreferences.setPasswordDisabled(context, true);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setReadReceiptsEnabled(context, true);
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch(); ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch(); SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false)); ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
@@ -22,6 +22,7 @@ import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
@@ -32,9 +33,8 @@ import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.Conscrypt; import org.conscrypt.Conscrypt;
import org.signal.aesgcmprovider.AesGcmProvider; import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.glide.SignalGlideCodecs;
import org.signal.ringrtc.CallManager; import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -42,10 +42,12 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.gcm.FcmJobService; import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
@@ -66,8 +68,11 @@ import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.webrtc.voiceengine.WebRtcAudioManager; import org.webrtc.voiceengine.WebRtcAudioManager;
@@ -87,15 +92,14 @@ import java.util.concurrent.TimeUnit;
* *
* @author Moxie Marlinspike * @author Moxie Marlinspike
*/ */
@Trace
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver { public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
private static final String TAG = ApplicationContext.class.getSimpleName(); private static final String TAG = ApplicationContext.class.getSimpleName();
private ExpiringMessageManager expiringMessageManager; private ExpiringMessageManager expiringMessageManager;
private ViewOnceMessageManager viewOnceMessageManager; private ViewOnceMessageManager viewOnceMessageManager;
private TypingStatusRepository typingStatusRepository; private PersistentLogger persistentLogger;
private TypingStatusSender typingStatusSender;
private PersistentLogger persistentLogger;
private volatile boolean isAppVisible; private volatile boolean isAppVisible;
@@ -117,8 +121,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
initializeMessageRetrieval(); initializeMessageRetrieval();
initializeExpiringMessageManager(); initializeExpiringMessageManager();
initializeRevealableMessageManager(); initializeRevealableMessageManager();
initializeTypingStatusRepository();
initializeTypingStatusSender();
initializeGcmCheck(); initializeGcmCheck();
initializeSignedPreKeyCheck(); initializeSignedPreKeyCheck();
initializePeriodicTasks(); initializePeriodicTasks();
@@ -127,12 +129,12 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
initializePendingMessages(); initializePendingMessages();
initializeBlobProvider(); initializeBlobProvider();
initializeCleanup(); initializeCleanup();
initializeGlideCodecs();
FeatureFlags.init(); FeatureFlags.init();
NotificationChannels.create(this); NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary(); RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync(); StorageSyncHelper.scheduleRoutineSync();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
RegistrationUtil.maybeMarkRegistrationComplete(this); RegistrationUtil.maybeMarkRegistrationComplete(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this); ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
@@ -141,6 +143,9 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
} }
ApplicationDependencies.getJobManager().beginJobLoop(); ApplicationDependencies.getJobManager().beginJobLoop();
DynamicTheme.setDefaultDayNightMode(this);
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
} }
@@ -150,10 +155,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
Log.i(TAG, "App is now visible."); Log.i(TAG, "App is now visible.");
FeatureFlags.refreshIfNecessary(); FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp(); ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
executePendingContactSync(); executePendingContactSync();
KeyCachingService.onAppForegrounded(this); KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin(); ApplicationDependencies.getFrameRateTracker().begin();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded(); ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
checkBuildExpiration();
} }
@Override @Override
@@ -173,14 +181,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
return viewOnceMessageManager; return viewOnceMessageManager;
} }
public TypingStatusRepository getTypingStatusRepository() {
return typingStatusRepository;
}
public TypingStatusSender getTypingStatusSender() {
return typingStatusSender;
}
public boolean isAppVisible() { public boolean isAppVisible() {
return isAppVisible; return isAppVisible;
} }
@@ -189,6 +189,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
return persistentLogger; return persistentLogger;
} }
public void checkBuildExpiration() {
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
Log.w(TAG, "Build expired!");
SignalStore.misc().markClientDeprecated();
}
}
private void initializeSecurityProvider() { private void initializeSecurityProvider() {
try { try {
Class.forName("org.signal.aesgcmprovider.AesGcmCipher"); Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
@@ -273,14 +280,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
this.viewOnceMessageManager = new ViewOnceMessageManager(this); this.viewOnceMessageManager = new ViewOnceMessageManager(this);
} }
private void initializeTypingStatusRepository() {
this.typingStatusRepository = new TypingStatusRepository();
}
private void initializeTypingStatusSender() {
this.typingStatusSender = new TypingStatusSender(this);
}
private void initializePeriodicTasks() { private void initializePeriodicTasks() {
RotateSignedPreKeyListener.schedule(this); RotateSignedPreKeyListener.schedule(this);
DirectoryRefreshListener.schedule(this); DirectoryRefreshListener.schedule(this);
@@ -378,9 +377,39 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
}); });
} }
private void initializeGlideCodecs() {
SignalGlideCodecs.setLogProvider(new org.signal.glide.Log.Provider() {
@Override
public void v(@NonNull String tag, @NonNull String message) {
Log.v(tag, message);
}
@Override
public void d(@NonNull String tag, @NonNull String message) {
Log.d(tag, message);
}
@Override
public void i(@NonNull String tag, @NonNull String message) {
Log.i(tag, message);
}
@Override
public void w(@NonNull String tag, @NonNull String message) {
Log.w(tag, message);
}
@Override
public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
Log.e(tag, message, throwable);
}
});
}
@Override @Override
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base))); DynamicLanguageContextWrapper.updateContext(base);
super.attachBaseContext(base);
} }
private static class ProviderInitializationException extends RuntimeException { private static class ProviderInitializationException extends RuntimeException {
@@ -17,32 +17,40 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.Preference; import androidx.preference.Preference;
import org.thoughtcrime.securesms.help.HelpFragment; import org.thoughtcrime.securesms.help.HelpFragment;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment; import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment; import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment; import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment; import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment; import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment; import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment; import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference; import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity; import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
@@ -56,10 +64,13 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
implements SharedPreferences.OnSharedPreferenceChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener
{ {
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName(); private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile"; private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
private static final String PREFERENCE_CATEGORY_USERNAME = "preference_category_username";
private static final String PREFERENCE_CATEGORY_SMS_MMS = "preference_category_sms_mms"; private static final String PREFERENCE_CATEGORY_SMS_MMS = "preference_category_sms_mms";
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications"; private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications";
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection"; private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
@@ -69,10 +80,15 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices"; private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help"; private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced"; private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
private static final String WAS_CONFIGURATION_UPDATED = "was_configuration_updated";
private final DynamicTheme dynamicTheme = new DynamicTheme(); private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private boolean wasConfigurationUpdated = false;
@Override @Override
protected void onPreCreate() { protected void onPreCreate() {
dynamicTheme.onCreate(this); dynamicTheme.onCreate(this);
@@ -86,11 +102,21 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) { if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
initFragment(android.R.id.content, new NotificationsPreferenceFragment()); 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) { } else if (icicle == null) {
initFragment(android.R.id.content, new ApplicationPreferenceFragment()); initFragment(android.R.id.content, new ApplicationPreferenceFragment());
} else {
wasConfigurationUpdated = icicle.getBoolean(WAS_CONFIGURATION_UPDATED);
} }
} }
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated);
super.onSaveInstanceState(outState);
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
@@ -112,20 +138,28 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
if (fragmentManager.getBackStackEntryCount() > 0) { if (fragmentManager.getBackStackEntryCount() > 0) {
fragmentManager.popBackStack(); fragmentManager.popBackStack();
} else { } else {
// TODO [greyson] Navigation if (wasConfigurationUpdated) {
Intent intent = new Intent(this, MainActivity.class); setResult(MainActivity.RESULT_CONFIG_CHANGED);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); } else {
startActivity(intent); setResult(RESULT_OK);
}
finish(); finish();
} }
return true; return true;
} }
@Override
public void onBackPressed() {
onSupportNavigateUp();
}
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(TextSecurePreferences.THEME_PREF)) { if (key.equals(TextSecurePreferences.THEME_PREF)) {
DynamicTheme.setDefaultDayNightMode(this);
recreate(); recreate();
} else if (key.equals(TextSecurePreferences.LANGUAGE_PREF)) { } else if (key.equals(TextSecurePreferences.LANGUAGE_PREF)) {
wasConfigurationUpdated = true;
recreate(); recreate();
Intent intent = new Intent(this, KeyCachingService.class); Intent intent = new Intent(this, KeyCachingService.class);
@@ -134,6 +168,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
} }
} }
public void pushFragment(@NonNull Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
.replace(android.R.id.content, fragment)
.addToBackStack(null)
.commit();
}
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment { public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
@Override @Override
@@ -142,6 +184,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
this.findPreference(PREFERENCE_CATEGORY_PROFILE) this.findPreference(PREFERENCE_CATEGORY_PROFILE)
.setOnPreferenceClickListener(new ProfileClickListener()); .setOnPreferenceClickListener(new ProfileClickListener());
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
.setOnPreferenceClickListener(new UsernameClickListener());
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS) this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS) this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
@@ -159,7 +203,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
this.findPreference(PREFERENCE_CATEGORY_HELP) this.findPreference(PREFERENCE_CATEGORY_HELP)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
this.findPreference(PREFERENCE_CATEGORY_ADVANCED) this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
this.findPreference(PREFERENCE_CATEGORY_DONATE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
tintIcons(); tintIcons();
} }
@@ -168,12 +214,30 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
if (Build.VERSION.SDK_INT >= 21) return; if (Build.VERSION.SDK_INT >= 21) return;
Preference preference = this.findPreference(PREFERENCE_CATEGORY_SMS_MMS); Preference preference = this.findPreference(PREFERENCE_CATEGORY_SMS_MMS);
preference.getIcon().setColorFilter(ThemeUtil.getThemedColor(requireContext(), R.attr.icon_tint), PorterDuff.Mode.SRC_IN); preference.getIcon().setColorFilter(ContextCompat.getColor(requireContext(), R.color.signal_icon_tint_primary), PorterDuff.Mode.SRC_IN);
} }
@Override @Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
if (FeatureFlags.usernames()) {
UsernamePreference pref = (UsernamePreference) findPreference(PREFERENCE_CATEGORY_USERNAME);
pref.setVisible(shouldDisplayUsernameReminder());
pref.setOnLongClickListener(v -> {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.ApplicationPreferencesActivity_hide_reminder)
.setPositiveButton(R.string.ApplicationPreferencesActivity_hide, (dialog, which) -> {
dialog.dismiss();
SignalStore.misc().hideUsernameReminder();
findPreference(PREFERENCE_CATEGORY_USERNAME).setVisible(false);
})
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setCancelable(true)
.show();
return true;
});
}
} }
@Override @Override
@@ -188,6 +252,11 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
private void setCategorySummaries() { private void setCategorySummaries() {
((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh(); ((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
if (FeatureFlags.usernames()) {
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
.setVisible(shouldDisplayUsernameReminder());
}
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS) this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
.setSummary(SmsMmsPreferenceFragment.getSummary(getActivity())); .setSummary(SmsMmsPreferenceFragment.getSummary(getActivity()));
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS) this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
@@ -207,6 +276,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
} }
} }
private static boolean shouldDisplayUsernameReminder() {
return FeatureFlags.usernames() && !Recipient.self().getUsername().isPresent() && SignalStore.misc().shouldShowUsernameReminder();
}
private class CategoryClickListener implements Preference.OnPreferenceClickListener { private class CategoryClickListener implements Preference.OnPreferenceClickListener {
private String category; private String category;
@@ -247,6 +320,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
case PREFERENCE_CATEGORY_HELP: case PREFERENCE_CATEGORY_HELP:
fragment = new HelpFragment(); fragment = new HelpFragment();
break; break;
case PREFERENCE_CATEGORY_DONATE:
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url));
break;
default: default:
throw new AssertionError(); throw new AssertionError();
} }
@@ -255,14 +331,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
Bundle args = new Bundle(); Bundle args = new Bundle();
fragment.setArguments(args); fragment.setArguments(args);
FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); ((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end);
fragmentTransaction.replace(android.R.id.content, fragment);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
} }
return true; return true;
@@ -276,6 +345,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
return true; return true;
} }
} }
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
requireActivity().startActivity(EditProfileActivity.getIntentForUsernameEdit(preference.getContext()));
return true;
}
}
} }
} }
@@ -10,8 +10,6 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.transition.TransitionInflater; import android.transition.TransitionInflater;
import android.view.View; import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -36,6 +34,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FullscreenHelper;
/** /**
* Activity for displaying avatars full screen. * Activity for displaying avatars full screen.
@@ -72,27 +71,21 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set)); getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
} }
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
ImageView avatar = findViewById(R.id.avatar);
ImageView avatar = findViewById(R.id.avatar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
WindowManager.LayoutParams.FLAG_FULLSCREEN);
showSystemUI();
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Context context = getApplicationContext(); Context context = getApplicationContext();
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA)); RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
Recipient.live(recipientId).observe(this, recipient -> { Recipient.live(recipientId).observe(this, recipient -> {
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar()) ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
: recipient.getContactPhoto(); : 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) 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(); : recipient.getFallbackContactPhoto();
Resources resources = this.getResources(); Resources resources = this.getResources();
@@ -132,47 +125,13 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
toolbar.setTitle(recipient.getDisplayName(context)); toolbar.setTitle(recipient.getDisplayName(context));
}); });
avatar.setOnClickListener(v -> toggleUiVisibility()); FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout)); findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
}
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) { fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
for (View view : views) { fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
view.animate()
.alpha(hide ? 0 : 1)
.start();
}
});
}
private void toggleUiVisibility() {
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
showSystemUI();
} else {
hideSystemUI();
}
}
private void hideSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN );
}
private void showSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
} }
@Override @Override
@@ -3,22 +3,28 @@ package org.thoughtcrime.securesms;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.ConfigurationUtil;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper; import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import java.util.Objects;
/** /**
* Base class for all activities. The vast majority of activities shouldn't extend this directly. * Base class for all activities. The vast majority of activities shouldn't extend this directly.
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by * Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
@@ -37,7 +43,6 @@ public abstract class BaseActivity extends AppCompatActivity {
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
initializeScreenshotSecurity(); initializeScreenshotSecurity();
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
} }
@Override @Override
@@ -72,19 +77,39 @@ public abstract class BaseActivity extends AppCompatActivity {
ActivityCompat.startActivity(this, intent, bundle); ActivityCompat.startActivity(this, intent, bundle);
} }
@TargetApi(VERSION_CODES.LOLLIPOP) @Override
protected void setStatusBarColor(int color) { protected void attachBaseContext(@NonNull Context newBase) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { super.attachBaseContext(newBase);
getWindow().setStatusBarColor(color);
} Configuration configuration = new Configuration(newBase.getResources().getConfiguration());
int appCompatNightMode = getDelegate().getLocalNightMode() != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED ? getDelegate().getLocalNightMode()
: AppCompatDelegate.getDefaultNightMode();
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
applyOverrideConfiguration(configuration);
} }
@Override @Override
protected void attachBaseContext(Context newBase) { public void applyOverrideConfiguration(@NonNull Configuration overrideConfiguration) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase))); DynamicLanguageContextWrapper.prepareOverrideConfiguration(this, overrideConfiguration);
super.applyOverrideConfiguration(overrideConfiguration);
} }
private void logEvent(@NonNull String event) { private void logEvent(@NonNull String event) {
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event); Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
} }
public final @NonNull ActionBar requireSupportActionBar() {
return Objects.requireNonNull(getSupportActionBar());
}
private static int mapNightModeToConfigurationUiMode(@NonNull Context context, @AppCompatDelegate.NightMode int appCompatNightMode) {
if (appCompatNightMode == AppCompatDelegate.MODE_NIGHT_YES) {
return Configuration.UI_MODE_NIGHT_YES;
} else if (appCompatNightMode == AppCompatDelegate.MODE_NIGHT_NO) {
return Configuration.UI_MODE_NIGHT_NO;
}
return ConfigurationUtil.getNightModeConfiguration(context.getApplicationContext());
}
} }
@@ -1,14 +1,18 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.net.Uri;
import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.view.View; 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.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationMessage; import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
@@ -22,7 +26,8 @@ import java.util.Locale;
import java.util.Set; import java.util.Set;
public interface BindableConversationItem extends Unbindable { public interface BindableConversationItem extends Unbindable {
void bind(@NonNull ConversationMessage messageRecord, void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord, @NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests, @NonNull GlideRequests glideRequests,
@@ -30,7 +35,7 @@ public interface BindableConversationItem extends Unbindable {
@NonNull Set<ConversationMessage> batchSelected, @NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient recipients, @NonNull Recipient recipients,
@Nullable String searchQuery, @Nullable String searchQuery,
boolean pulseHighlight); boolean pulseMention);
ConversationMessage getConversationMessage(); ConversationMessage getConversationMessage();
@@ -49,5 +54,15 @@ public interface BindableConversationItem extends Unbindable {
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms); void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId); void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord); 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);
void onJoinGroupCallClicked();
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);
} }
} }
@@ -1,139 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cursoradapter.widget.CursorAdapter;
import androidx.fragment.app.ListFragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.loaders.BlockedContactsLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.preferences.BlockedContactListItem;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class BlockedContactsActivity extends PassphraseRequiredActivity {
private final DynamicTheme dynamicTheme = new DynamicTheme();
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
public void onCreate(Bundle bundle, boolean ready) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.BlockedContactsActivity_blocked_contacts);
initFragment(android.R.id.content, new BlockedContactsFragment());
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
public static class BlockedContactsFragment
extends ListFragment
implements LoaderManager.LoaderCallbacks<Cursor>, ListView.OnItemClickListener
{
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
return inflater.inflate(R.layout.blocked_contacts_fragment, container, false);
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setListAdapter(new BlockedContactAdapter(requireActivity(), GlideApp.with(this), null));
LoaderManager.getInstance(this).initLoader(0, null, this);
}
@Override
public void onStart() {
super.onStart();
LoaderManager.getInstance(this).restartLoader(0, null, this);
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
getListView().setOnItemClickListener(this);
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new BlockedContactsLoader(getActivity());
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
if (getListAdapter() != null) {
((CursorAdapter) getListAdapter()).changeCursor(data);
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
if (getListAdapter() != null) {
((CursorAdapter) getListAdapter()).changeCursor(null);
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Recipient recipient = ((BlockedContactListItem)view).getRecipient();
BlockUnblockDialog.showUnblockFor(requireContext(), getLifecycle(), recipient, () -> {
RecipientUtil.unblock(requireContext(), recipient);
LoaderManager.getInstance(this).restartLoader(0, null, this);
});
}
private static class BlockedContactAdapter extends CursorAdapter {
private final GlideRequests glideRequests;
BlockedContactAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, @Nullable Cursor c) {
super(context, c);
this.glideRequests = glideRequests;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return LayoutInflater.from(context)
.inflate(R.layout.blocked_contact_list_item, parent, false);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RecipientDatabase.ID)));
LiveRecipient recipient = Recipient.live(recipientId);
((BlockedContactListItem) view).set(glideRequests, recipient);
}
}
}
}
@@ -13,6 +13,7 @@ import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
@@ -113,8 +114,8 @@ public class ConfirmIdentityDialog extends AlertDialog {
} }
private void processOutgoingMessageRecord(MessageRecord messageRecord) { private void processOutgoingMessageRecord(MessageRecord messageRecord) {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext()); MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext()); MessageDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
if (messageRecord.isMms()) { if (messageRecord.isMms()) {
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(), mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
@@ -137,8 +138,8 @@ public class ConfirmIdentityDialog extends AlertDialog {
private void processIncomingMessageRecord(MessageRecord messageRecord) { private void processIncomingMessageRecord(MessageRecord messageRecord) {
try { try {
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext()); PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext()); MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
smsDatabase.removeMismatchedIdentity(messageRecord.getId(), smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
mismatch.getRecipientId(getContext()), mismatch.getRecipientId(getContext()),
@@ -113,7 +113,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
} }
@Override @Override
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) { public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
return true; return true;
} }
@@ -21,6 +21,7 @@ import android.Manifest;
import android.animation.LayoutTransition; import android.animation.LayoutTransition;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
@@ -29,7 +30,6 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.animation.CycleInterpolator;
import android.widget.Button; import android.widget.Button;
import android.widget.HorizontalScrollView; import android.widget.HorizontalScrollView;
import android.widget.TextView; import android.widget.TextView;
@@ -56,6 +56,7 @@ import com.google.android.material.chip.ChipGroup;
import com.pnikosis.materialishprogress.ProgressWheel; import com.pnikosis.materialishprogress.ProgressWheel;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.components.emoji.WarningTextView;
import org.thoughtcrime.securesms.contacts.ContactChip; import org.thoughtcrime.securesms.contacts.ContactChip;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; 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.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact; import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; 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.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
@@ -70,7 +73,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.UsernameUtil;
@@ -83,7 +85,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Set; import java.util.Set;
/** /**
@@ -104,11 +105,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
public static final int NO_LIMIT = Integer.MAX_VALUE; public static final int NO_LIMIT = Integer.MAX_VALUE;
public static final String DISPLAY_MODE = "display_mode"; 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 REFRESHABLE = "refreshable";
public static final String RECENTS = "recents"; public static final String RECENTS = "recents";
public static final String TOTAL_CAPACITY = "total_capacity"; public static final String SELECTION_LIMITS = "selection_limits";
public static final String CURRENT_SELECTION = "current_selection"; public static final String CURRENT_SELECTION = "current_selection";
public static final String HIDE_COUNT = "hide_count";
private ConstraintLayout constraintLayout; private ConstraintLayout constraintLayout;
private TextView emptyText; private TextView emptyText;
@@ -124,15 +125,17 @@ public final class ContactSelectionListFragment extends LoggingFragment
private ContactSelectionListAdapter cursorRecyclerViewAdapter; private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup; private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer; private HorizontalScrollView chipGroupScrollContainer;
private TextView groupLimit; private WarningTextView groupLimit;
@Nullable private FixedViewsAdapter headerAdapter; @Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter; @Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback; @Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback; @Nullable private ScrollCallback scrollCallback;
private GlideRequests glideRequests; private GlideRequests glideRequests;
private int selectionLimit; private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection; private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean hideCount;
@Override @Override
public void onAttach(@NonNull Context context) { public void onAttach(@NonNull Context context) {
@@ -207,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(TOTAL_CAPACITY, NO_LIMIT);
currentSelection = getCurrentSelection(); currentSelection = getCurrentSelection();
updateGroupLimit(getChipCount()); updateGroupLimit(getChipCount());
@@ -218,12 +230,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
private void updateGroupLimit(int chipCount) { private void updateGroupLimit(int chipCount) {
if (selectionLimit != NO_LIMIT) { int members = currentSelection.size() + chipCount;
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit)); groupLimit.setText(getResources().getQuantityString(R.plurals.ContactSelectionListFragment_d_members, members, members));
groupLimit.setVisibility(View.VISIBLE); groupLimit.setVisibility(isMulti && !hideCount ? View.VISIBLE : View.GONE);
} else { groupLimit.setWarning(selectionWarningLimitExceeded());
groupLimit.setVisibility(View.GONE);
}
} }
@Override @Override
@@ -255,7 +265,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
public boolean isMulti() { public boolean isMulti() {
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false); return isMulti;
} }
private void initializeCursor() { private void initializeCursor() {
@@ -265,7 +275,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
glideRequests, glideRequests,
null, null,
new ListClickListener(), new ListClickListener(),
isMulti(), isMulti,
currentSelection); currentSelection);
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader(); RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
@@ -451,15 +461,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber()) SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
: SelectedContact.forPhone(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(); Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return; return;
} }
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) { if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (selectionLimitReached()) { if (selectionHardLimitReached()) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show(); GroupLimitDialog.showHardLimitMessage(requireContext());
groupLimit.animate().scaleX(1.3f).scaleY(1.3f).setInterpolator(new CycleInterpolator(0.5f)).start();
return; return;
} }
@@ -475,7 +484,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber()); SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
if (onContactSelectedListener != null) { if (onContactSelectedListener != null) {
if (onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null)) { if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
markContactSelected(selected); markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE); cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
} }
@@ -487,13 +496,13 @@ public final class ContactSelectionListFragment extends LoggingFragment
new AlertDialog.Builder(requireContext()) new AlertDialog.Builder(requireContext())
.setTitle(R.string.ContactSelectionListFragment_username_not_found) .setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber())) .setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
.setPositiveButton(R.string.ContactSelectionListFragment_okay, (dialog, which) -> dialog.dismiss()) .setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show(); .show();
} }
}); });
} else { } else {
if (onContactSelectedListener != null) { if (onContactSelectedListener != null) {
if (onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber())) { if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
markContactSelected(selectedContact); markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE); cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
} }
@@ -509,16 +518,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (onContactSelectedListener != null) { if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber()); onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
} }
}} }
}
} }
private boolean selectionLimitReached() { private boolean selectionHardLimitReached() {
return getChipCount() >= selectionLimit; 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) { private void markContactSelected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.addSelectedContact(selectedContact); cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
if (isMulti()) { if (isMulti) {
addChipForSelectedContact(selectedContact); addChipForSelectedContact(selectedContact);
} }
} }
@@ -589,6 +607,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void addChip(@NonNull ContactChip chip) { private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip); chipGroup.addView(chip);
updateGroupLimit(getChipCount()); updateGroupLimit(getChipCount());
if (selectionWarningLimitReachedExactly()) {
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
}
} }
private int getChipCount() { private int getChipCount() {
@@ -632,7 +653,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
public interface OnContactSelectedListener { public interface OnContactSelectedListener {
/** @return True if the contact is allowed to be selected, otherwise false. */ /** @return True if the contact is allowed to be selected, otherwise false. */
boolean onContactSelected(Optional<RecipientId> recipientId, String number); boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
void onContactDeselected(Optional<RecipientId> recipientId, String number); void onContactDeselected(Optional<RecipientId> recipientId, String number);
} }
@@ -28,7 +28,7 @@ public final class GroupMembersDialog {
public void display() { public void display() {
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity) AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
.setTitle(R.string.ConversationActivity_group_members) .setTitle(R.string.ConversationActivity_group_members)
.setIconAttribute(R.attr.group_members_dialog_icon) .setIcon(R.drawable.ic_group_24)
.setCancelable(true) .setCancelable(true)
.setView(R.layout.dialog_group_members) .setView(R.layout.dialog_group_members)
.setPositiveButton(android.R.string.ok, null) .setPositiveButton(android.R.string.ok, null)
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChange
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact; import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.MessageSender;
@@ -40,7 +41,6 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener { public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
@@ -63,7 +63,8 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
@Override @Override
protected void onCreate(Bundle savedInstanceState, boolean ready) { protected void onCreate(Bundle savedInstanceState, boolean ready) {
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_SMS); 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); getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
setContentView(R.layout.invite_activity); setContentView(R.layout.invite_activity);
@@ -103,7 +104,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment); contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url))); inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
updateSmsButtonText(); updateSmsButtonText(contactsFragment.getSelectedContacts().size());
contactsFragment.setOnContactSelectedListener(this); contactsFragment.setOnContactSelectedListener(this);
shareButton.setOnClickListener(new ShareClickListener()); shareButton.setOnClickListener(new ShareClickListener());
@@ -121,14 +122,14 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
} }
@Override @Override
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) { public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
updateSmsButtonText(); updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
return true; return true;
} }
@Override @Override
public void onContactDeselected(Optional<RecipientId> recipientId, String number) { public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
updateSmsButtonText(); updateSmsButtonText(contactsFragment.getSelectedContacts().size());
} }
private void sendSmsInvites() { private void sendSmsInvites() {
@@ -138,12 +139,11 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
.toArray(new SelectedContact[0])); .toArray(new SelectedContact[0]));
} }
private void updateSmsButtonText() { private void updateSmsButtonText(int count) {
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends, smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
selectedContacts.size(), count,
selectedContacts.size())); count));
smsSendButton.setEnabled(!selectedContacts.isEmpty()); smsSendButton.setEnabled(count > 0);
} }
@Override public void onBackPressed() { @Override public void onBackPressed() {
@@ -157,17 +157,17 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
private void cancelSmsSelection() { private void cancelSmsSelection() {
setPrimaryColorsToolbarNormal(); setPrimaryColorsToolbarNormal();
contactsFragment.reset(); contactsFragment.reset();
updateSmsButtonText(); updateSmsButtonText(contactsFragment.getSelectedContacts().size());
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE); ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
} }
private void setPrimaryColorsToolbarNormal() { private void setPrimaryColorsToolbarNormal() {
primaryToolbar.setBackgroundColor(0); primaryToolbar.setBackgroundColor(0);
primaryToolbar.getNavigationIcon().setColorFilter(null); primaryToolbar.getNavigationIcon().setColorFilter(null);
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.title_text_color_primary)); primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_primary));
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23) {
getWindow().setStatusBarColor(ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor)); WindowUtil.setStatusBarColor(getWindow(), ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor));
getWindow().setNavigationBarColor(ThemeUtil.getThemedColor(this, android.R.attr.navigationBarColor)); getWindow().setNavigationBarColor(ThemeUtil.getThemedColor(this, android.R.attr.navigationBarColor));
WindowUtil.setLightStatusBarFromTheme(this); WindowUtil.setLightStatusBarFromTheme(this);
} }
@@ -177,11 +177,11 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
private void setPrimaryColorsToolbarForSms() { private void setPrimaryColorsToolbarForSms() {
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine)); primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine));
primaryToolbar.getNavigationIcon().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.conversation_subtitle_color), PorterDuff.Mode.SRC_IN); primaryToolbar.getNavigationIcon().setColorFilter(ContextCompat.getColor(this, R.color.signal_text_toolbar_subtitle), PorterDuff.Mode.SRC_IN);
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color)); primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_toolbar_title));
if (Build.VERSION.SDK_INT >= 23) { if (Build.VERSION.SDK_INT >= 23) {
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.core_ultramarine)); WindowUtil.setStatusBarColor(getWindow(), ContextCompat.getColor(this, R.color.core_ultramarine));
WindowUtil.clearLightStatusBar(getWindow()); WindowUtil.clearLightStatusBar(getWindow());
} }
@@ -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);
}
}
@@ -1,14 +1,23 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
@Trace
public class MainActivity extends PassphraseRequiredActivity { public class MainActivity extends PassphraseRequiredActivity {
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final MainNavigator navigator = new MainNavigator(this); private final MainNavigator navigator = new MainNavigator(this);
@@ -18,6 +27,14 @@ public class MainActivity extends PassphraseRequiredActivity {
setContentView(R.layout.main_activity); setContentView(R.layout.main_activity);
navigator.onCreate(savedInstanceState); navigator.onCreate(savedInstanceState);
handleGroupLinkInIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleGroupLinkInIntent(intent);
} }
@Override @Override
@@ -39,7 +56,22 @@ public class MainActivity extends PassphraseRequiredActivity {
} }
} }
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == MainNavigator.REQUEST_CONFIG_CHANGES && resultCode == RESULT_CONFIG_CHANGED) {
recreate();
}
}
public @NonNull MainNavigator getNavigator() { public @NonNull MainNavigator getNavigator() {
return navigator; return navigator;
} }
private void handleGroupLinkInIntent(Intent intent) {
Uri data = intent.getData();
if (data != null) {
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString());
}
}
} }
@@ -18,6 +18,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
public class MainNavigator { public class MainNavigator {
public static final int REQUEST_CONFIG_CHANGES = 901;
private final MainActivity activity; private final MainActivity activity;
public MainNavigator(@NonNull MainActivity activity) { public MainNavigator(@NonNull MainActivity activity) {
@@ -65,10 +67,9 @@ public class MainNavigator {
public void goToAppSettings() { public void goToAppSettings() {
Intent intent = new Intent(activity, ApplicationPreferencesActivity.class); Intent intent = new Intent(activity, ApplicationPreferencesActivity.class);
activity.startActivity(intent); activity.startActivityForResult(intent, REQUEST_CONFIG_CHANGES);
} }
public void goToArchiveList() { public void goToArchiveList() {
getFragmentManager().beginTransaction() getFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end) .setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
@@ -18,12 +18,14 @@ package org.thoughtcrime.securesms;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.view.Menu; import android.view.Menu;
@@ -31,14 +33,14 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.ShareCompat;
import androidx.core.util.Pair; import androidx.core.util.Pair;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@@ -63,14 +65,17 @@ import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel; import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareActivity; import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.StorageUtil;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
@@ -119,6 +124,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private boolean cameFromAllMedia; private boolean cameFromAllMedia;
private boolean showThread; private boolean showThread;
private MediaDatabase.Sorting sorting; private MediaDatabase.Sorting sorting;
private FullscreenHelper fullscreenHelper;
private @Nullable Cursor cursor = null; private @Nullable Cursor cursor = null;
@@ -133,10 +139,16 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize()); intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption()); intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent); intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
intent.setDataAndType(attachment.getDataUri(), mediaRecord.getContentType()); intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
return intent; return intent;
} }
@Override
protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
super.attachBaseContext(newBase);
}
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@Override @Override
protected void onCreate(Bundle bundle, boolean ready) { protected void onCreate(Bundle bundle, boolean ready) {
@@ -147,10 +159,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class); viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, fullscreenHelper = new FullscreenHelper(this);
WindowManager.LayoutParams.FLAG_FULLSCREEN);
showSystemUI();
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -196,7 +205,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (threadRecipient != null) { if (threadRecipient != null) {
if (mediaItem.outgoing || threadRecipient.isGroup()) { if (mediaItem.outgoing || threadRecipient.isGroup()) {
if (threadRecipient.isLocalNumber()) { if (threadRecipient.isSelf()) {
from = getString(R.string.note_to_self); from = getString(R.string.note_to_self);
} else { } else {
to = threadRecipient.getDisplayName(this); to = threadRecipient.getDisplayName(this);
@@ -261,6 +270,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
albumRail = findViewById(R.id.media_preview_album_rail); albumRail = findViewById(R.id.media_preview_album_rail);
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false); 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.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
albumRail.setAdapter(albumRailAdapter); albumRail.setAdapter(albumRailAdapter);
@@ -273,9 +283,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
anchorMarginsToBottomInsets(detailsContainer); anchorMarginsToBottomInsets(detailsContainer);
anchorMarginsToTopInsets(toolbarLayout); fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout); fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
} }
private void initializeResources() { private void initializeResources() {
@@ -379,6 +389,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") @SuppressWarnings("CodeBlock2Expr")
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private void saveToDisk() { private void saveToDisk() {
@@ -386,21 +417,30 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (mediaItem != null) { if (mediaItem != null) {
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> { SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
if (StorageUtil.canWriteToMediaStore()) {
performSavetoDisk(mediaItem);
return;
}
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary() .ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied)) .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()) .onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> { .onAllGranted(() -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this); performSavetoDisk(mediaItem);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
}) })
.execute(); .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") @SuppressLint("StaticFieldLeak")
private void deleteMedia() { private void deleteMedia() {
MediaItem mediaItem = getCurrentMediaItem(); MediaItem mediaItem = getCurrentMediaItem();
@@ -409,7 +449,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
} }
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIconAttribute(R.attr.dialog_alert_icon); builder.setIcon(R.drawable.ic_warning);
builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title); builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message); builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
builder.setCancelable(true); builder.setCancelable(true);
@@ -431,36 +471,45 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
} }
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.clear(); menu.clear();
MenuInflater inflater = this.getMenuInflater(); MenuInflater inflater = this.getMenuInflater();
inflater.inflate(R.menu.media_preview, menu); inflater.inflate(R.menu.media_preview, menu);
super.onCreateOptionsMenu(menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (!isMediaInDb()) { if (!isMediaInDb()) {
menu.findItem(R.id.media_preview__overview).setVisible(false); menu.findItem(R.id.media_preview__overview).setVisible(false);
menu.findItem(R.id.delete).setVisible(false); 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) { if (cameFromAllMedia) {
menu.findItem(R.id.media_preview__overview).setVisible(false); menu.findItem(R.id.media_preview__overview).setVisible(false);
} }
super.onPrepareOptionsMenu(menu);
return true; return true;
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(@NonNull MenuItem item) {
super.onOptionsItemSelected(item); super.onOptionsItemSelected(item);
switch (item.getItemId()) { int itemId = item.getItemId();
case R.id.media_preview__overview: showOverview(); return true;
case R.id.media_preview__forward: forward(); return true; if (itemId == R.id.media_preview__overview) { showOverview(); return true; }
case R.id.save: saveToDisk(); return true; if (itemId == R.id.media_preview__forward) { forward(); return true; }
case R.id.delete: deleteMedia(); return true; if (itemId == R.id.media_preview__share) { share(); return true; }
case android.R.id.home: finish(); 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; return false;
} }
@@ -541,7 +590,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
@Override @Override
public boolean singleTapOnMedia() { public boolean singleTapOnMedia() {
toggleUiVisibility(); fullscreenHelper.toggleUiVisibility();
return true; return true;
} }
@@ -551,32 +600,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
finish(); finish();
} }
private void toggleUiVisibility() {
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
showSystemUI();
} else {
hideSystemUI();
}
}
private void hideSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN );
}
private void showSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
}
private class ViewPagerListener extends ExtendedOnPageChangedListener { private class ViewPagerListener extends ExtendedOnPageChangedListener {
@Override @Override
@@ -692,33 +715,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
}); });
} }
private static void anchorMarginsToTopInsets(@NonNull View viewToAnchor) {
ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor, (view, insets) -> {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
layoutParams.setMargins(insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(),
layoutParams.bottomMargin);
view.setLayoutParams(layoutParams);
return insets;
});
}
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
for (View view : views) {
view.animate()
.alpha(hide ? 0 : 1)
.start();
}
});
}
private static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter { private static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
@SuppressLint("UseSparseArrays") @SuppressLint("UseSparseArrays")
@@ -796,7 +792,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
return new MediaItem(Recipient.live(recipientId).get(), return new MediaItem(Recipient.live(recipientId).get(),
Recipient.live(threadRecipientId).get(), Recipient.live(threadRecipientId).get(),
attachment, attachment,
Objects.requireNonNull(attachment.getDataUri()), Objects.requireNonNull(attachment.getUri()),
mediaRecord.getContentType(), mediaRecord.getContentType(),
mediaRecord.getDate(), mediaRecord.getDate(),
mediaRecord.isOutgoing()); mediaRecord.isOutgoing());
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@@ -60,21 +61,22 @@ public class NewConversationActivity extends ContactSelectionActivity
} }
@Override @Override
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) { public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
if (recipientId.isPresent()) { if (recipientId.isPresent()) {
launch(Recipient.resolved(recipientId.get())); launch(Recipient.resolved(recipientId.get()));
} else { } else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient."); Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
if (FeatureFlags.cds() && NetworkConstraint.isMet(this)) {
Log.i(TAG, "[onContactSelected] CDS enabled. Doing contact refresh."); if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
AlertDialog progress = SimpleProgressDialog.show(this); AlertDialog progress = SimpleProgressDialog.show(this);
SimpleTask.run(getLifecycle(), () -> { SimpleTask.run(getLifecycle(), () -> {
Recipient resolved = Recipient.external(this, number); Recipient resolved = Recipient.external(this, number);
if (!resolved.isRegistered()) { if (!resolved.isRegistered() || !resolved.hasUuid()) {
Log.i(TAG, "[onContactSelected] Not registered. Doing a directory refresh."); Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
try { try {
DirectoryHelper.refreshDirectoryFor(this, resolved, false); DirectoryHelper.refreshDirectoryFor(this, resolved, false);
resolved = Recipient.resolved(resolved.getId()); resolved = Recipient.resolved(resolved.getId());
@@ -98,11 +100,11 @@ public class NewConversationActivity extends ContactSelectionActivity
private void launch(Recipient recipient) { private void launch(Recipient recipient) {
Intent intent = new Intent(this, ConversationActivity.class); Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId()); intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA)); intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
intent.setDataAndType(getIntent().getData(), getIntent().getType()); intent.setDataAndType(getIntent().getData(), getIntent().getType());
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient); long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread); intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT); intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
@@ -138,11 +140,11 @@ public class NewConversationActivity extends ContactSelectionActivity
} }
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
menu.clear(); menu.clear();
getMenuInflater().inflate(R.menu.new_conversation_activity, menu); getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
super.onPrepareOptionsMenu(menu); super.onCreateOptionsMenu(menu);
return true; return true;
} }
@@ -66,12 +66,6 @@ public class PassphraseCreateActivity extends PassphraseActivity {
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this); IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this); VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
TextSecurePreferences.setLastExperienceVersionCode(PassphraseCreateActivity.this, Util.getCanonicalVersionCode());
TextSecurePreferences.setPasswordDisabled(PassphraseCreateActivity.this, true);
TextSecurePreferences.setReadReceiptsEnabled(PassphraseCreateActivity.this, true);
TextSecurePreferences.setTypingIndicatorsEnabled(PassphraseCreateActivity.this, true);
TextSecurePreferences.setHasSeenWelcomeScreen(PassphraseCreateActivity.this, false);
return null; return null;
} }
@@ -132,12 +132,13 @@ public class PassphrasePromptActivity extends PassphraseActivity {
} }
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater(); MenuInflater inflater = this.getMenuInflater();
menu.clear(); menu.clear();
inflater.inflate(R.menu.log_submit, menu); inflater.inflate(R.menu.log_submit, menu);
super.onPrepareOptionsMenu(menu);
super.onCreateOptionsMenu(menu);
return true; return true;
} }
@@ -42,7 +42,6 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
@Override @Override
protected void onCreate(Bundle icicle, boolean ready) { protected void onCreate(Bundle icicle, boolean ready) {
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
super.onCreate(icicle, ready); super.onCreate(icicle, ready);
initializeToolbar(); initializeToolbar();
@@ -48,12 +48,12 @@ public class SmsSendtoActivity extends Activity {
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
} else { } else {
Recipient recipient = Recipient.external(this, destination.getDestination()); Recipient recipient = Recipient.external(this, destination.getDestination());
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient); long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
nextIntent = new Intent(this, ConversationActivity.class); nextIntent = new Intent(this, ConversationActivity.class);
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody()); nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());
nextIntent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); nextIntent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId()); nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
} }
return nextIntent; return nextIntent;
} }
@@ -22,6 +22,7 @@ import android.animation.ValueAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@@ -56,6 +57,7 @@ import android.widget.Toast;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat; import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
@@ -85,16 +87,19 @@ import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.fingerprint.Fingerprint; import org.whispersystems.libsignal.fingerprint.Fingerprint;
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Locale; import java.util.Locale;
import java.util.UUID;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK; import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
@@ -224,9 +229,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
private void setActionBarNotificationBarColor(MaterialColor color) { private void setActionBarNotificationBarColor(MaterialColor color) {
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this))); getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this));
getWindow().setStatusBarColor(color.toStatusBarColor(this));
}
} }
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener { public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
@@ -307,16 +310,26 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
byte[] localId; byte[] localId;
byte[] remoteId; byte[] remoteId;
if (FeatureFlags.cds() && recipient.resolve().getUuid().isPresent()) { Recipient resolved = recipient.resolve();
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
Log.i(TAG, "Using UUID (version 2)."); Log.i(TAG, "Using UUID (version 2).");
version = 2; version = 2;
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext())); localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
remoteId = UuidUtil.toByteArray(recipient.resolve().getUuid().get()); remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
} else { } else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
Log.i(TAG, "Using E164 (version 1)."); Log.i(TAG, "Using E164 (version 1).");
version = 1; version = 1;
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes(); 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); this.recipient.observe(this, this::setRecipientText);
@@ -19,17 +19,12 @@ package org.thoughtcrime.securesms;
import android.Manifest; import android.Manifest;
import android.app.PictureInPictureParams; import android.app.PictureInPictureParams;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.util.Rational; import android.util.Rational;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
@@ -37,7 +32,6 @@ import android.view.WindowManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
@@ -45,10 +39,12 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput; import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity; import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
@@ -56,24 +52,19 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter; import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.VerifySpan;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK; public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
public class WebRtcCallActivity extends AppCompatActivity {
private static final String TAG = WebRtcCallActivity.class.getSimpleName(); private static final String TAG = WebRtcCallActivity.class.getSimpleName();
private static final int STANDARD_DELAY_FINISH = 1000; private static final int STANDARD_DELAY_FINISH = 1000;
public static final int BUSY_SIGNAL_DELAY_FINISH = 5500;
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION"; public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION"; public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
@@ -95,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity); setContentView(R.layout.webrtc_call_activity);
//noinspection ConstantConditions
getSupportActionBar().hide(); getSupportActionBar().hide();
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL); setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
@@ -134,6 +126,11 @@ public class WebRtcCallActivity extends AppCompatActivity {
if (!isInPipMode()) { if (!isInPipMode()) {
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
} }
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
finish();
}
} }
@Override @Override
@@ -142,11 +139,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
super.onStop(); super.onStop();
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
}
@Override CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
public void onConfigurationChanged(Configuration newConfiguration) { if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
super.onConfigurationChanged(newConfiguration); Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
startService(intent);
}
} }
@Override @Override
@@ -172,7 +171,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
} }
private boolean enterPipModeIfPossible() { private boolean enterPipModeIfPossible() {
if (isSystemPipEnabledAndAvailable()) { if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder() PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(9, 16)) .setAspectRatio(new Rational(9, 16))
.build(); .build();
@@ -206,21 +205,29 @@ public class WebRtcCallActivity extends AppCompatActivity {
} }
private void initializeResources() { private void initializeResources() {
callScreen = ViewUtil.findById(this, R.id.callScreen); callScreen = findViewById(R.id.callScreen);
callScreen.setControlsListener(new ControlsListener()); callScreen.setControlsListener(new ControlsListener());
} }
private void initializeViewModel() { private void initializeViewModel() {
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class); viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
viewModel.setIsInPipMode(isInPipMode()); viewModel.setIsInPipMode(isInPipMode());
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled); viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls); viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent); viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime); viewModel.getCallTime().observe(this, this::handleCallTime);
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard); viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null) {
if (state.needsNewRequestSizes()) {
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS);
startService(intent);
}
}
});
} }
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) { private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
@@ -385,19 +392,17 @@ public class WebRtcCallActivity extends AppCompatActivity {
startService(intent); startService(intent);
} }
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
callScreen.setRecipient(event.getRecipient());
}
private void handleOutgoingCall(@NonNull WebRtcViewModel event) { private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
callScreen.setRecipient(event.getRecipient()); if (event.getGroupState().isNotIdle()) {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling)); callScreen.setStatusFromGroupCallState(event.getGroupState());
} else {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
}
} }
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) { private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
Log.i(TAG, "handleTerminate called: " + hangupType.name()); Log.i(TAG, "handleTerminate called: " + hangupType.name());
callScreen.setRecipient(recipient);
callScreen.setStatusFromHangupType(hangupType); callScreen.setStatusFromHangupType(hangupType);
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
@@ -408,99 +413,74 @@ public class WebRtcCallActivity extends AppCompatActivity {
delayedFinish(); delayedFinish();
} }
private void handleCallRinging(@NonNull WebRtcViewModel event) { private void handleCallRinging() {
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_ringing)); callScreen.setStatus(getString(R.string.RedPhone_ringing));
} }
private void handleCallBusy(@NonNull WebRtcViewModel event) { private void handleCallBusy() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_busy)); callScreen.setStatus(getString(R.string.RedPhone_busy));
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
} }
private void handleCallConnected(@NonNull WebRtcViewModel event) { private void handleCallConnected(@NonNull WebRtcViewModel event) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES); getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
callScreen.setRecipient(event.getRecipient()); if (event.getGroupState().isNotIdleOrConnected()) {
callScreen.setStatusFromGroupCallState(event.getGroupState());
}
} }
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) { private void handleRecipientUnavailable() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable)); callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
delayedFinish(); delayedFinish();
} }
private void handleServerFailure(@NonNull WebRtcViewModel event) { private void handleServerFailure() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_network_failed)); callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish(); delayedFinish();
} }
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) { private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
AlertDialog.Builder dialog = new AlertDialog.Builder(this); new AlertDialog.Builder(this)
dialog.setTitle(R.string.RedPhone_number_not_registered); .setTitle(R.string.RedPhone_number_not_registered)
dialog.setIconAttribute(R.attr.dialog_alert_icon); .setIcon(R.drawable.ic_warning)
dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice); .setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
dialog.setCancelable(true); .setCancelable(true)
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() { .setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
@Override .setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
public void onClick(DialogInterface dialog, int which) { .show();
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.show();
} }
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) { private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
final IdentityKey theirKey = event.getIdentityKey(); final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
final Recipient recipient = event.getRecipient(); final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
if (theirKey == null) { if (theirKey == null) {
handleTerminate(recipient, HangupMessage.Type.NORMAL); handleTerminate(recipient, HangupMessage.Type.NORMAL);
} }
String name = recipient.getDisplayName(this); SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId());
String introduction = getString(R.string.WebRtcCallScreen_new_safety_numbers, name, name); }
SpannableString spannableString = new SpannableString(introduction + " " + getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
spannableString.setSpan(new VerifySpan(this, recipient.getId(), theirKey), introduction.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @Override
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_OFFER_TYPE, OfferMessage.Type.AUDIO_CALL.getCode());
AppCompatTextView untrustedIdentityExplanation = new AppCompatTextView(this); startService(intent);
untrustedIdentityExplanation.setText(spannableString); }
untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(this) @Override
.setView(untrustedIdentityExplanation) public void onMessageResentAfterSafetyNumberChange() { }
.setPositiveButton(R.string.WebRtcCallScreen_accept, (d, w) -> {
synchronized (SESSION_LOCK) {
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirKey, true);
}
d.dismiss(); @Override
public void onCanceled() {
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class); handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
startService(intent);
})
.setNegativeButton(R.string.WebRtcCallScreen_end_call, (d, w) -> {
d.dismiss();
handleTerminate(recipient, HangupMessage.Type.NORMAL);
})
.show();
} }
private boolean isSystemPipEnabledAndAvailable() { private boolean isSystemPipEnabledAndAvailable() {
@@ -517,32 +497,30 @@ public class WebRtcCallActivity extends AppCompatActivity {
} }
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN) @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(final WebRtcViewModel event) { public void onEventMainThread(@NonNull WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event); Log.i(TAG, "Got message from service: " + event);
viewModel.setRecipient(event.getRecipient()); viewModel.setRecipient(event.getRecipient());
callScreen.setRecipient(event.getRecipient());
switch (event.getState()) { switch (event.getState()) {
case CALL_PRE_JOIN: handleCallPreJoin(event); break;
case CALL_CONNECTED: handleCallConnected(event); break; case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(event); break; case NETWORK_FAILURE: handleServerFailure(); break;
case CALL_RINGING: handleCallRinging(event); break; case CALL_RINGING: handleCallRinging(); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break; case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break; case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break; case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break; case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break; case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
case NO_SUCH_USER: handleNoSuchUser(event); break; case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break; case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
case CALL_INCOMING: handleIncomingCall(event); break;
case CALL_OUTGOING: handleOutgoingCall(event); break; case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break; case CALL_BUSY: handleCallBusy(); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break; case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
} }
callScreen.setLocalRenderer(event.getLocalRenderer()); boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
callScreen.setRemoteRenderer(event.getRemoteRenderer());
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
viewModel.updateFromWebRtcViewModel(event, enableVideo); viewModel.updateFromWebRtcViewModel(event, enableVideo);
@@ -552,8 +530,32 @@ public class WebRtcCallActivity extends AppCompatActivity {
} }
} }
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
if (event.getGroupState().isNotIdle()) {
callScreen.setStatusFromGroupCallState(event.getGroupState());
}
}
private final class ControlsListener implements WebRtcCallView.ControlsListener { private final class ControlsListener implements WebRtcCallView.ControlsListener {
@Override
public void onStartCall(boolean isVideoCall) {
enableVideoIfAvailable = isVideoCall;
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
startService(intent);
MessageSender.onMessageSent();
}
@Override
public void onCancelStartCall() {
finish();
}
@Override @Override
public void onControlsFadeOut() { public void onControlsFadeOut() {
if (videoTooltip != null) { if (videoTooltip != null) {
@@ -618,9 +620,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
} }
@Override @Override
public void onDownCaretPressed() { public void onShowParticipantsList() {
CallParticipantsListDialog.show(getSupportFragmentManager());
}
@Override
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
viewModel.setIsViewingFocusedParticipant(page);
} }
} }
} }
@@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.animation;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import androidx.annotation.NonNull;
public class ResizeAnimation extends Animation {
private final View target;
private final int targetWidthPx;
private final int targetHeightPx;
private int startWidth;
private int startHeight;
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
this.target = target;
this.targetWidthPx = targetWidthPx;
this.targetHeightPx = targetHeightPx;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
ViewGroup.LayoutParams params = target.getLayoutParams();
params.width = newWidth;
params.height = newHeight;
target.setLayoutParams(params);
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
this.startWidth = width;
this.startHeight = height;
}
@Override
public boolean willChangeBounds() {
return true;
}
}
@@ -106,10 +106,7 @@ public abstract class Attachment {
} }
@Nullable @Nullable
public abstract Uri getDataUri(); public abstract Uri getUri();
@Nullable
public abstract Uri getThumbnailUri();
public int getTransferState() { public int getTransferState() {
return transferState; return transferState;
@@ -57,7 +57,7 @@ public class DatabaseAttachment extends Attachment {
@Override @Override
@Nullable @Nullable
public Uri getDataUri() { public Uri getUri() {
if (hasData) { if (hasData) {
return PartAuthority.getAttachmentDataUri(attachmentId); return PartAuthority.getAttachmentDataUri(attachmentId);
} else { } else {
@@ -65,16 +65,6 @@ public class DatabaseAttachment extends Attachment {
} }
} }
@Override
@Nullable
public Uri getThumbnailUri() {
if (hasThumbnail) {
return PartAuthority.getAttachmentThumbnailUri(attachmentId);
} else {
return null;
}
}
public AttachmentId getAttachmentId() { public AttachmentId getAttachmentId() {
return attachmentId; return attachmentId;
} }
@@ -15,13 +15,7 @@ public class MmsNotificationAttachment extends Attachment {
@Nullable @Nullable
@Override @Override
public Uri getDataUri() { public Uri getUri() {
return null;
}
@Nullable
@Override
public Uri getThumbnailUri() {
return null; return null;
} }
@@ -42,17 +42,10 @@ public class PointerAttachment extends Attachment {
@Nullable @Nullable
@Override @Override
public Uri getDataUri() { public Uri getUri() {
return null; return null;
} }
@Nullable
@Override
public Uri getThumbnailUri() {
return null;
}
public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) { public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) {
List<Attachment> results = new LinkedList<>(); List<Attachment> results = new LinkedList<>();
@@ -20,12 +20,7 @@ public class TombstoneAttachment extends Attachment {
} }
@Override @Override
public @Nullable Uri getDataUri() { public @Nullable Uri getUri() {
return null;
}
@Override
public @Nullable Uri getThumbnailUri() {
return null; return null;
} }
} }
@@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
public class UriAttachment extends Attachment { public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri; private final @NonNull Uri dataUri;
private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri, public UriAttachment(@NonNull Uri uri,
@NonNull String contentType, @NonNull String contentType,
@@ -29,11 +28,10 @@ public class UriAttachment extends Attachment {
@Nullable AudioHash audioHash, @Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties) @Nullable TransformProperties transformProperties)
{ {
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties); this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
} }
public UriAttachment(@NonNull Uri dataUri, public UriAttachment(@NonNull Uri dataUri,
@Nullable Uri thumbnailUri,
@NonNull String contentType, @NonNull String contentType,
int transferState, int transferState,
long size, long size,
@@ -51,22 +49,15 @@ public class UriAttachment extends Attachment {
@Nullable TransformProperties transformProperties) @Nullable TransformProperties transformProperties)
{ {
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties); super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.dataUri = dataUri; this.dataUri = dataUri;
this.thumbnailUri = thumbnailUri;
} }
@Override @Override
@NonNull @NonNull
public Uri getDataUri() { public Uri getUri() {
return dataUri; return dataUri;
} }
@Override
@Nullable
public Uri getThumbnailUri() {
return thumbnailUri;
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri); return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);
@@ -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;
}
}
}
@@ -4,6 +4,10 @@ package org.thoughtcrime.securesms.backup;
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; 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.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
@@ -13,10 +17,14 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.R; 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.registration.fragments.RestoreBackupFragment;
import org.thoughtcrime.securesms.service.LocalBackupListener; import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil; import org.thoughtcrime.securesms.util.BackupUtil;
@@ -26,25 +34,49 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
public class BackupDialog { 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(); String[] password = BackupUtil.generateBackupPassphrase();
AlertDialog dialog = new AlertDialog.Builder(context) AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_enable_local_backups) .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) .setPositiveButton(R.string.BackupDialog_enable_backups, null)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.create(); .create();
dialog.setOnShowListener(created -> { 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 button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(v -> { button.setOnClickListener(v -> {
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check); CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
if (confirmationCheckBox.isChecked()) { 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, " ")); BackupPassphrase.set(context, Util.join(password, " "));
TextSecurePreferences.setNextBackupTime(context, 0);
TextSecurePreferences.setBackupEnabled(context, true); TextSecurePreferences.setBackupEnabled(context, true);
LocalBackupListener.schedule(context); LocalBackupListener.schedule(context);
preference.setChecked(true); onBackupsEnabled.run();
created.dismiss(); created.dismiss();
} else { } else {
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show(); 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) new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_delete_backups) .setTitle(R.string.BackupDialog_delete_backups)
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups) .setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> { .setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
BackupPassphrase.set(context, null); BackupUtil.disableBackups(context);
TextSecurePreferences.setBackupEnabled(context, false);
BackupUtil.deleteAllBackups(); onBackupsDisabled.run();
preference.setChecked(false);
}) })
.create() .create()
.show(); .show();

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