Compare commits

..

222 Commits

Author SHA1 Message Date
Cody Henthorne
45267f3590 Bump version to 5.24.10 2021-09-29 13:30:50 -04:00
Cody Henthorne
3fb8c6eda8 Updated language translations. 2021-09-29 13:26:03 -04:00
Cody Henthorne
27ce0fd65e Fix overlapping text when message contains mixed LTR and RTL text.
Fixes #11638
2021-09-29 13:17:58 -04:00
Alex Hart
7e91132e7e Fix multiple chatcolors issues from beta feedback.
- Fix issue where custom color would come out as black
- Completely remove mask view in favour of using the item decoration.
- Fix issue where video gifs wouldn't "cut through" bubble.
- Fix issue where multiselect shade would only appear if bottom or top item was not visible
2021-09-29 13:17:58 -04:00
Cody Henthorne
705839068a Fix crash when forwarding unknown media types. 2021-09-29 13:17:57 -04:00
Alex Hart
6625ac02d5 Fix NPE when eventListener is not set. 2021-09-29 13:17:57 -04:00
Alex Hart
4b3580d98a Fix issue where mentions did not propagate in message send flow. 2021-09-29 13:17:57 -04:00
Cody Henthorne
6dbbec2631 Bump version to 5.24.9 2021-09-28 17:22:57 -04:00
Cody Henthorne
a7b6ebe7fc Updated language translations. 2021-09-28 17:19:03 -04:00
Cody Henthorne
76f52b9086 Fix various bugs around unread counts and scroll to bottom. 2021-09-28 17:12:25 -04:00
Greyson Parrelli
3310246351 Inline MP4 GIF flag.
This reverts commit 91645e6adc.
2021-09-28 17:12:25 -04:00
Alex Hart
f3d0b4a671 Fix incorrect gradient rotation. 2021-09-28 17:12:25 -04:00
Cody Henthorne
83b9fbac11 Bump version to 5.24.8 2021-09-28 11:53:40 -04:00
Cody Henthorne
5ca843825f Updated language translations. 2021-09-28 11:48:40 -04:00
rainlion
e92c83401b Fix a bug that unchanged returns true even if TransformationMethod is changed. 2021-09-28 11:42:51 -04:00
Fumiaki Yoshimatsu
e18d9e665f Take padded bytes into account when decrypting a stream of data.
Fixes #11573
2021-09-28 11:42:51 -04:00
Greyson Parrelli
cc99febe32 Allow use of the new CDSH service in staging. 2021-09-28 11:42:51 -04:00
Greyson Parrelli
e72be42eff Put SMS messages in a separate sending queue. 2021-09-28 11:42:51 -04:00
Alex Hart
bad382e2f3 Fix stretchy chat colors on Android 12. 2021-09-28 11:42:51 -04:00
Cody Henthorne
e637f15a43 Refactor call audio routing and bluetooth management. 2021-09-28 11:42:51 -04:00
Cody Henthorne
6c55916cda Fix backup restore moving forward when backgrounded. 2021-09-28 11:42:51 -04:00
Greyson Parrelli
fbabab0b70 Track down issues around empty preupload results. 2021-09-28 11:42:50 -04:00
Alex Hart
e268887255 Fix crash if animating view was removed from parent. 2021-09-28 11:42:50 -04:00
Alex Hart
6b07922757 Add error logging for media gallery objects. 2021-09-28 11:42:50 -04:00
Alex Hart
a464e57079 Fix media session reconnect issue for some devices. 2021-09-27 09:28:53 -03:00
Alex Hart
b5af691cc4 Add badges to Avatars in a variety of places. 2021-09-24 13:39:28 -03:00
Alex Hart
5c1b57e4ba Implement ExoPlayerPool for better reuse and performance. 2021-09-24 13:10:48 -03:00
Greyson Parrelli
a5c51ff801 Handle exception when reading from the log database. 2021-09-24 11:57:03 -04:00
Christelle Gloor
d755e1e29e Set onClick to entire row, not just the checkbox. 2021-09-24 11:29:59 -03:00
Alex Hart
b9361112b6 Resize the image when entering crop mode. 2021-09-24 10:58:37 -03:00
Greyson Parrelli
32101f7dda Update reaction text for GIFs. 2021-09-24 09:27:54 -04:00
Alex Hart
29e697265c Do not try to start next activity if we are not attached. 2021-09-24 09:21:09 -03:00
Alex Hart
4cd9ccc0f1 Fix crash when blocking and leaving a spam group. 2021-09-24 09:13:56 -03:00
Alex Hart
8936d81bc7 Fix 4.4 crash in image editor. 2021-09-23 17:12:14 -03:00
Alex Hart
cc36f83d77 Fix horizontal translation of video player when in multiselect mode. 2021-09-23 14:49:13 -03:00
Greyson Parrelli
64996a8db7 Register mavenLocal() repo for all projects. 2021-09-23 11:35:21 -03:00
Greyson Parrelli
7267d77dcb Add support for syncing default reactions. 2021-09-23 11:35:21 -03:00
Greyson Parrelli
2281e83607 Log RecipientId for MissingAddressErrors. 2021-09-23 11:35:21 -03:00
Alex Hart
e6b03b1a4a Implement ability to select featured badge to display on profile. 2021-09-23 11:35:21 -03:00
AsamK
fb86fdfcd9 Fix syncing reactions in note to self to linked devices.
Fixes #11027
2021-09-23 11:35:21 -03:00
Alex Hart
77cf029fdc Implement ability to view badges and modify whether they appear.
Note: this is available in staging only.
2021-09-23 11:35:21 -03:00
Alex Hart
556ca5a573 Bump version to 5.24.7 2021-09-23 11:32:51 -03:00
Alex Hart
91645e6adc Revert "Inline MP4 GIF flag."
This reverts commit e2e0caa94a.
2021-09-23 11:17:54 -03:00
Alex Hart
4d6bb95aa4 Bump version to 5.24.6 2021-09-23 10:09:22 -03:00
Alex Hart
55ee68fa2d Updated language translations. 2021-09-23 10:08:38 -03:00
Alex Hart
747bc7c3bf Swap out expiring pinned mobilecoin cert. 2021-09-23 09:48:39 -03:00
Alex Hart
9c17201eaf Bump version to 5.24.5 2021-09-22 16:40:02 -03:00
Alex Hart
a24b3d9a60 Updated language translations. 2021-09-22 16:39:38 -03:00
Alex Hart
c93d882fe1 Don't allow API<23 to display gif videos in conversation list. 2021-09-22 16:21:13 -03:00
Alex Hart
67403a6a9f Bump version to 5.24.4 2021-09-21 17:00:21 -03:00
Alex Hart
5f9e72bb3c Updated language translations. 2021-09-21 16:59:58 -03:00
Greyson Parrelli
091b38ceb8 Use the GIF content type for quoted MP4 GIFs. 2021-09-21 15:52:08 -04:00
Cody Henthorne
83dfb984fb Update to RingRTC v2.13.1 2021-09-21 15:41:01 -04:00
Greyson Parrelli
9f14831fc4 Do not crash on issues with the log database. 2021-09-21 14:16:31 -04:00
Alex Hart
48bfcc9b16 Bump version to 5.24.3 2021-09-21 13:31:24 -03:00
Alex Hart
7028ca9411 Updated language translations. 2021-09-21 13:30:48 -03:00
Cody Henthorne
5175375483 Fix crash when getting update body on main thread. 2021-09-21 11:17:31 -04:00
Greyson Parrelli
e2dbaa605b Fix potential stack overflow when getting identity record. 2021-09-21 09:16:58 -04:00
Alex Hart
93fd6e7a55 Fix issue with media controller lifecycle.
We were connecting and disconnecting in onStart and onStop,
which can get called in different orders depending on what the
system does. This results in sometimes trying to connect to an
already connected media session.
2021-09-21 10:09:43 -03:00
Alex Hart
b070e6962f Remove view from parent before trying to insert into a new container. 2021-09-21 10:03:42 -03:00
Alex Hart
b1d1b7e31e Fix NullPointerException when getting ringtone title. 2021-09-21 09:57:59 -03:00
Alex Hart
1a5ae603d5 Bump version to 5.24.2 2021-09-20 16:25:44 -03:00
Alex Hart
3b42bda63d Updated language translations. 2021-09-20 16:25:44 -03:00
Alex Hart
2f70a71a6c Fix several kotlin formatting issues from bug fixes. 2021-09-20 16:25:44 -03:00
Cody Henthorne
aae368c049 Clear profile upload flag when unregistering. 2021-09-20 16:25:44 -03:00
Alex Hart
dee3d2ff2d Fix restrictive sizing of speed toggle in voice note bar. 2021-09-20 16:25:44 -03:00
Alex Hart
da2e2a99af Remove outdated stableId pattern from ConversationAdapter. 2021-09-20 11:53:47 -03:00
Alex Hart
0c00426c0c Fix internal preference issue with creating a clipboard service. 2021-09-20 11:41:04 -03:00
Alex Hart
ccc96d5bfa Fix gif display when list is changed and view holders are not reused. 2021-09-20 11:19:33 -03:00
Alex Hart
82c12c2f6b Do not allow content to play if no media item is available. 2021-09-20 10:52:48 -03:00
Alex Hart
9416beb4aa Trigger pending transition at right time for video gifs. 2021-09-20 10:35:01 -03:00
Alex Hart
39b80a48c7 Ensure message details video container matches placement of recycler. 2021-09-20 10:22:43 -03:00
Alex Hart
d5491a2e84 Fix vector load crash on Kitkat.
Fixes #11628
2021-09-20 10:19:37 -03:00
Alex Hart
07b19402e6 Fix wallpaper gallery toolbar behaviour.
Fixes #11619
2021-09-20 10:12:59 -03:00
Alex Hart
318b4703f2 Bump version to 5.24.1 2021-09-17 16:15:26 -03:00
Alex Hart
d389697f27 Updated language translations. 2021-09-17 16:14:25 -03:00
Alex Hart
7bcc338a49 Implement radial dial.
Co-authored-by: Alan Evans <alan@signal.org>
2021-09-17 13:09:13 -03:00
Cody Henthorne
ce2c2002c6 Revert thread updates to running inline again. 2021-09-17 11:50:46 -04:00
Greyson Parrelli
d5fbd10406 Create a SignalDataSource class for all of our ExoPlayer needs.
Also fixes an issue around GIF playback within a conversation.
2021-09-17 09:58:11 -04:00
Cody Henthorne
6f6da699a3 Fix groups not showing after pin restore. 2021-09-17 09:56:49 -04:00
Cody Henthorne
62d8c115ba Enable group call notification settings when group ringing is enabled. 2021-09-17 09:53:28 -04:00
Alex Hart
fd01ee2a87 Add stopwatches for a few possible pain points in MediaGallery. 2021-09-16 16:29:51 -03:00
Cody Henthorne
9aa517ad99 Fix UI bugs in dark mode change number flow. 2021-09-16 14:14:38 -04:00
Greyson Parrelli
6c3e1b6a29 Add internal preference to disable storage syncing.
Added to help debug certain scenarios, particularly around working with
emulator snapshots, since storage sync will often bring in state from earlier
snapshots you weren't expecting.
2021-09-16 13:32:25 -04:00
Alex Hart
5d5063ef5f Bump version to 5.24.0 2021-09-16 14:17:38 -03:00
Alex Hart
b48668455c Updated language translations. 2021-09-16 14:17:38 -03:00
Lucio Maciel
18ba5fa291 Fix emoji avatar missing after edit. 2021-09-16 14:17:38 -03:00
Cody Henthorne
5e968eb831 Prevent group leave event from bumping conversation. 2021-09-16 14:17:38 -03:00
Aaron Labiaga
b4465953d8 Set LocusID on shortcut and notification for on device intelligence. 2021-09-16 14:17:38 -03:00
Ducros Alix
08a7da3339 Add greek characters to the accent insensitive search of names.
Fixes #11534
2021-09-16 14:17:38 -03:00
essentialols
c1a08616ab Add 1.5x playback speed for voice messages. 2021-09-16 14:17:38 -03:00
Cody Henthorne
3761859681 Fix kotlin compiler warnings. 2021-09-16 14:17:38 -03:00
RiseT
8984b763fb Replace typographical apostrophe by standard one. 2021-09-16 14:17:38 -03:00
Alex Hart
59c62671b9 Do not launch ShareActivity as singleTask.
Fixes #11620
2021-09-16 14:17:38 -03:00
Alex Hart
bcfe8909e5 Add image editor sample app. 2021-09-16 14:17:38 -03:00
Lucio Maciel
c43fe44e3e Fix transformation method issues. 2021-09-16 14:17:38 -03:00
Greyson Parrelli
4ac1134a9b Point to a new remote emoji version file.
There was a bug in older versions around caching, so by switching to a
new version file, we can make sure only fixed versions get the new
emoji.
2021-09-16 14:17:38 -03:00
Greyson Parrelli
08d03cb456 Clear emoji cache after downloading a new set. 2021-09-16 14:17:38 -03:00
Greyson Parrelli
e5c172a819 Turn off noisy eventbus logs.
Fixes #11617
2021-09-16 14:17:38 -03:00
Alan Evans
4569011e0b Two point thumb control for scale and rotate. 2021-09-16 14:17:38 -03:00
Greyson Parrelli
1031a4e96c Improve logging around message sending and processing. 2021-09-16 14:17:38 -03:00
Peter Thatcher
cdf8e4e1ed Only try to connect to bluetooth a limited number of times in a call. 2021-09-16 14:17:38 -03:00
Alex Hart
b589449c34 Consolidate app dependencies using gradle version catalogs. 2021-09-16 14:17:38 -03:00
Cody Henthorne
7d7dd101df Fix note bug on payment details. 2021-09-16 14:17:38 -03:00
Cody Henthorne
e687fea567 Fix race condition overriding profile on registration. 2021-09-16 14:17:38 -03:00
Cody Henthorne
e2cb522e87 Prevent part files from being deleted prematurely. 2021-09-16 14:17:38 -03:00
Alex Hart
662ba85c5a Upgrade to Gradle 7.2 and AGP 7.0.2 2021-09-16 14:17:38 -03:00
Greyson Parrelli
d29ebc7768 Update included emoji to 13.1 2021-09-14 09:35:56 -04:00
Alex Hart
95fabd7ed1 Initial modularization of core image editor code. 2021-09-14 09:35:56 -04:00
Jim Gustafson
5d5251054c Update to RingRTC v2.13.0 2021-09-14 09:35:56 -04:00
Sgn-32
c766ba9808 Use more icons in ConversationListItem 2021-09-14 09:35:56 -04:00
Greyson Parrelli
8b5fe79849 Update our image viewer versions. 2021-09-14 09:35:56 -04:00
Greyson Parrelli
903c5c6db6 Add an internal recipient details screen. 2021-09-14 09:35:56 -04:00
Greyson Parrelli
e2e0caa94a Inline MP4 GIF flag. 2021-09-14 09:35:56 -04:00
Greyson Parrelli
520fe481e9 Bump version to 5.23.7 2021-09-14 09:25:33 -04:00
Greyson Parrelli
7262aefa34 Updated language translations. 2021-09-14 09:24:54 -04:00
Greyson Parrelli
8815cdc3de Fix potential crash during notification processing. 2021-09-14 09:18:27 -04:00
Greyson Parrelli
8df86962e9 Fix potential crash with a bad group update body. 2021-09-14 09:08:04 -04:00
Greyson Parrelli
573c0fad7f Update libsignal-client to 0.9.4 2021-09-14 09:07:43 -04:00
Greyson Parrelli
a8419d5f02 Fix potential crash when reading bad GV1 ids in block sync. 2021-09-14 08:54:07 -04:00
Greyson Parrelli
6088f16e3a Bump version to 5.23.6 2021-09-10 12:32:45 -04:00
Greyson Parrelli
1a5ae592a7 Updated language translations. 2021-09-10 12:32:17 -04:00
Greyson Parrelli
6880dfeb62 Show 'Note to Self' for yourself in the media send flow. 2021-09-10 12:24:26 -04:00
Cody Henthorne
dfecb0efd8 Only show change number event when previous e164 known and different. 2021-09-10 12:12:07 -04:00
Greyson Parrelli
2eaadd4337 Allow multi-line text in media send flow. 2021-09-10 10:47:32 -04:00
Greyson Parrelli
655f3c1219 Bump version to 5.23.5 2021-09-09 17:57:08 -04:00
Greyson Parrelli
1494a3559d Stop broadcasting the change number capability. 2021-09-09 17:51:18 -04:00
Greyson Parrelli
f61d7a9f77 Bump version to 5.23.4 2021-09-09 17:18:57 -04:00
Greyson Parrelli
ee0ab8f035 Updated language translations. 2021-09-09 17:18:57 -04:00
Alex Hart
6e85c74e3f Adjust camera button arc width. 2021-09-09 16:08:26 -04:00
Alex Hart
3b1aa5b176 Add shade behind trash icon for better visibility on white images. 2021-09-09 16:08:26 -04:00
Alex Hart
715ad0d459 Add text styles support to image editor.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2021-09-09 16:08:26 -04:00
Greyson Parrelli
05f7dce503 Fix potentional ClassCastException. 2021-09-09 11:57:37 -04:00
Greyson Parrelli
ecfbeb69c5 Allow images to be cached in the image editor. 2021-09-09 11:31:11 -04:00
Greyson Parrelli
c7fb343b93 Fix undesireable undo behavior when deleting. 2021-09-09 11:07:21 -04:00
Greyson Parrelli
b0d0814b88 Prevent multitouch from accidentally deleting stickers. 2021-09-09 11:07:21 -04:00
Cody Henthorne
9f3765d368 Fix some vector assets not rendering properly on older OS versions. 2021-09-09 10:55:05 -04:00
Lucio Maciel
fe82c4e487 Fix image partially shown after message sent. 2021-09-09 11:47:46 -03:00
Alex Hart
e9bbb1b9ae Fix fade when re-entering text edit. 2021-09-09 11:35:49 -03:00
Cody Henthorne
fd3e88707c Fix preupload in new Media Send flow. 2021-09-09 10:06:49 -04:00
Greyson Parrelli
8e8def8b03 Fix crash when opening camera without storage permissions. 2021-09-09 09:44:22 -04:00
Lucio Maciel
5b1069018f Fix long chat name overlapping the timestamp. 2021-09-09 10:11:38 -03:00
Greyson Parrelli
abc71f4fb4 Bump version to 5.23.3 2021-09-08 21:14:38 -04:00
Greyson Parrelli
2f5e95c0e3 Updated language translations. 2021-09-08 21:13:16 -04:00
Greyson Parrelli
d1fd70a807 Fix delete button collision detection. 2021-09-08 21:08:22 -04:00
Greyson Parrelli
1b924c606a Use a dashed line for object highlighting. 2021-09-08 21:04:21 -04:00
Greyson Parrelli
9b175fa0dd Add some padding to text selection. 2021-09-08 21:04:21 -04:00
Cody Henthorne
7e7bbad788 Ensure change number operation status before returning to normal app usage. 2021-09-08 21:04:21 -04:00
Alex Hart
d8c82add78 Increase color circle radius in slider. 2021-09-08 21:04:21 -04:00
Alex Hart
6e1657b1bd Clamp start time value to be >= 0 2021-09-08 21:04:21 -04:00
Alex Hart
1f7b1d91c4 Improve trash can using in-renderer object. 2021-09-08 21:04:21 -04:00
Greyson Parrelli
e7833df539 Fix display of names with emojis in forward selection. 2021-09-08 21:04:21 -04:00
Alex Hart
fae80a242d Update animation interpolators in media send flow. 2021-09-08 21:04:06 -04:00
Cody Henthorne
77ff25ec49 Add Change Number capability and Conversation Update item. 2021-09-08 21:04:06 -04:00
Greyson Parrelli
bb446ac1d5 Update SQLCipher to 4.4.3-S3 2021-09-08 21:04:05 -04:00
Cody Henthorne
b6a4d01d42 Fix QR scan crash and add exchange data fallback for Create Payment. 2021-09-08 21:04:05 -04:00
Alex Hart
bd4dd25460 Add brush width preview. 2021-09-08 21:04:05 -04:00
Alex Hart
f86c1fe508 Support different width ranges for different brushes. 2021-09-08 21:04:05 -04:00
Alex Hart
38f6efbcae Fix NPE in VideoPlayer error handler. 2021-09-08 08:34:13 -03:00
Greyson Parrelli
30a542234b Bump version to 5.23.2 2021-09-07 23:13:34 -04:00
Greyson Parrelli
8c9bf678fa Updated language translations. 2021-09-07 23:13:34 -04:00
Greyson Parrelli
4b465b74e8 Save message in media flow as you type. 2021-09-07 23:13:19 -04:00
Greyson Parrelli
58a22f0eea Add black and white to the color picker. 2021-09-07 23:13:19 -04:00
Greyson Parrelli
ddad9acef1 Add support for drag + drop in the media send flow. 2021-09-07 23:13:19 -04:00
Lucio Maciel
1dbb6013cb Fix alignment on Update messages. 2021-09-07 23:13:19 -04:00
Lucio Maciel
9cc1ae4a29 Fix Verify Identity screen on smaller devices. 2021-09-07 23:13:19 -04:00
Alex Hart
4eb24c3303 Add fade below text layer when editing text. 2021-09-07 23:13:19 -04:00
Alex Hart
ec1935572e Fix bug where dialog would not dismiss after toggling between keyboards. 2021-09-07 23:13:19 -04:00
Alex Hart
e419d70d51 Do not crash when we try to play from IDLE state. 2021-09-07 23:13:19 -04:00
Alex Hart
2af5526879 Add new flash icons. 2021-09-07 23:13:19 -04:00
Alex Hart
6a5aa089ae Fix crash if sensors disabled in developer mode. 2021-09-07 23:13:19 -04:00
Alex Hart
6b5f4ca8c2 Fix onBackPressed / toolbar navigation behaviour in MediaGalleryFragment. 2021-09-07 23:13:19 -04:00
Alex Hart
53e110560a Fix onBack behaviour of media gallery fragment. 2021-09-07 23:13:19 -04:00
Alex Hart
82e9c620e8 Show progress spinner if media send takes more than 300ms. 2021-09-07 23:13:19 -04:00
Lucio Maciel
076facbdc2 Fixes on Chat list. 2021-09-07 23:13:19 -04:00
Alex Hart
a805f9b6b4 Utilize fast-in-extra-slow-out interpolator. 2021-09-07 23:13:19 -04:00
Alex Hart
969e763997 Fix several design feedback items for new media selection flow. 2021-09-07 23:13:19 -04:00
Alex Hart
9347227ff5 Reposition video editor and add new play button. 2021-09-07 23:13:19 -04:00
Cody Henthorne
c9ba0432a0 Fix bug with currency localization. 2021-09-07 23:13:19 -04:00
Greyson Parrelli
e3b7fe7509 Remove database notifications from within a transaction.
Having them in a transaction means there's a race where other threads
may not see the new database changes.
2021-09-07 23:13:13 -04:00
Cody Henthorne
5332669321 Potentially fix bad configuration change data with change to landscape. 2021-09-07 23:13:13 -04:00
Alex Hart
a086305c38 Improve behaviour of media send flow in landscape. 2021-09-07 23:13:13 -04:00
Greyson Parrelli
a712622891 Revert "Update URL for reaching Signal chat server."
This reverts commit 6179c087fb.
2021-09-07 22:58:17 -04:00
Alex Hart
1514f91687 Support deletion and guides when manipulating objects.
* Fix issue with avatar selection
* Remove save button on video editor screen (we never supported this)
* Fix mentions
2021-09-07 22:58:17 -04:00
Cody Henthorne
0dfa6aab09 Bump version to 5.23.1 2021-09-03 20:43:59 -04:00
Cody Henthorne
4b6dbac758 Updated language translations. 2021-09-03 20:38:17 -04:00
Cody Henthorne
b816f901a5 Fix test for mac. 2021-09-03 20:33:03 -04:00
Lucio Maciel
76d1490810 Adjust conversation list item height and name margin. 2021-09-03 20:19:56 -04:00
Cody Henthorne
f2ab0b6423 Initial work to support Change Number. 2021-09-03 20:19:56 -04:00
Lucio Maciel
e09d162c1e Update conversations list UI. 2021-09-03 20:19:55 -04:00
Greyson Parrelli
c84de8fa60 Add a cache for GIFs. 2021-09-03 20:19:55 -04:00
Greyson Parrelli
8e020c05f6 Improve IdentityDatabase e164 check. 2021-09-03 09:15:08 -04:00
Greyson Parrelli
8c9eb880cf Bump version to 5.23.0 2021-09-02 21:36:18 -04:00
Greyson Parrelli
d7ddd85a90 Updated language translations. 2021-09-02 21:35:27 -04:00
Alex Hart
7d994b2ae1 Set proper money separator when presenting custom amount string to user in MoneyView. 2021-09-02 21:24:54 -04:00
Alex Hart
664d6475d9 Refresh media selection and sending flow with a shiny new UX. 2021-09-02 21:24:54 -04:00
Greyson Parrelli
a940487611 Improve logging around rate-limiting. 2021-09-02 21:24:54 -04:00
Sgn-32
9f995d61f4 Fix padding for Payments icon and title. 2021-09-02 21:24:54 -04:00
Leonid Zavodnik
a6690e1bde Update exoplayer version to v2.15
Fixes #11547
2021-09-02 21:24:54 -04:00
Greyson Parrelli
d507df2e7e Increase max log size to 15mb. 2021-09-02 21:24:54 -04:00
Greyson Parrelli
fa26eb2017 Switch back to mainline SQLCipher with true WAL mode. 2021-09-02 21:24:54 -04:00
Greyson Parrelli
0b53ba8950 Improve MMS database insertion performance. 2021-09-02 21:24:54 -04:00
Greyson Parrelli
7447e2497b Default the retry receipt flag to true. 2021-09-02 21:24:54 -04:00
Greyson Parrelli
7ac83625d3 Add a write-through cache to the identity store. 2021-09-02 21:24:53 -04:00
Cody Henthorne
50dfe7bc25 Update Staging KBS values. 2021-09-02 21:24:53 -04:00
Cody Henthorne
8e32592218 Clarify networking call order during registration flow. 2021-09-02 21:24:53 -04:00
Lucio Maciel
a3d72fc06c Update message details UI. 2021-09-02 21:24:53 -04:00
Greyson Parrelli
f5a6d61362 Add support for granular conversation data changes. 2021-09-02 21:24:53 -04:00
Greyson Parrelli
bca2205945 Add measurements, improve MSL insert. 2021-09-02 21:24:53 -04:00
Alex Hart
1241f4c0e9 Enable MobileCoin in Germany, France, and Switzerland. 2021-09-02 21:24:53 -04:00
Graham Campbell
f6253ad0bb Corrected Google trademark notice 2021-09-02 21:24:53 -04:00
Lucio Maciel
083301185c Update verify identity UI. 2021-09-02 21:24:53 -04:00
Lucio Maciel
0273d0f285 Save receipt timestamps on sms/mms database. 2021-09-02 21:24:53 -04:00
Cody Henthorne
3dc1ce3353 Bump version to 5.22.7 2021-09-02 16:44:02 -04:00
Cody Henthorne
f8e077b824 Updated language translations. 2021-09-02 16:43:30 -04:00
Greyson Parrelli
aec2ca1d87 Update libsignal-client to 0.9.0 2021-09-02 11:21:15 -04:00
Cody Henthorne
6e7a18ea11 Bump version to 5.22.6 2021-09-01 12:55:04 -04:00
Cody Henthorne
fe54ec9d6c Updated language translations. 2021-09-01 12:49:23 -04:00
Greyson Parrelli
1819af3000 Fix possible crash when a contact merge results in no UUID.
After merging contacts, it's possible that we don't have a valid
UUID. We need to be careful not to use it.

Kind of a bummer, but the storage sync flow is currently the only flow
where we have this 'possibly not valid UUID'. In the future we should
probably use something else instead of a SignalServiceAddress to keep
that abstraction clean.
2021-09-01 10:46:42 -04:00
Cody Henthorne
3c177c4883 Bump version to 5.22.5 2021-08-31 10:18:33 -04:00
Cody Henthorne
2c871a36d0 Updated language translations. 2021-08-31 10:18:10 -04:00
Greyson Parrelli
6bde55f73b Only check remote registrationIds for active records. 2021-08-31 09:46:37 -04:00
958 changed files with 32859 additions and 13324 deletions

View File

@@ -16,10 +16,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: set up JDK 1.8
- name: set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1

View File

@@ -8,6 +8,38 @@
<option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" />
<option name="ALIGN_MULTILINE_ANNOTATION_PARAMETERS" value="true" />
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">

View File

@@ -63,4 +63,4 @@ Copyright 2013-2021 Signal
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
Google Play and the Google Play logo are trademarks of Google Inc.
Google Play and the Google Play logo are trademarks of Google LLC.

View File

@@ -11,12 +11,6 @@ apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'app.cash.exhaustive'
repositories {
maven {
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
content {
includeGroupByRegex "com\\.github\\.chrisbanes.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
content {
@@ -35,6 +29,10 @@ repositories {
includeGroupByRegex "com\\.amulyakhare.*"
}
}
maven {
url "https://www.jitpack.io"
}
google()
mavenCentral()
mavenLocal()
@@ -43,9 +41,6 @@ repositories {
}
jcenter {
content {
includeVersion "com.google.android.exoplayer", "exoplayer-core", "2.9.1"
includeVersion "com.google.android.exoplayer", "exoplayer-ui", "2.9.1"
includeVersion "com.google.android.exoplayer", "extension-mediasession", "2.9.1"
includeVersion "mobi.upod", "time-duration-picker", "1.1.3"
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
@@ -71,8 +66,8 @@ protobuf {
}
}
def canonicalVersionCode = 908
def canonicalVersionName = "5.22.4"
def canonicalVersionCode = 930
def canonicalVersionName = "5.24.10"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
@@ -149,11 +144,12 @@ android {
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_CDSH_URL", "\"https://cdsh.staging.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_SFU_URL", "\"https://sfu.voip.signal.org\""
@@ -162,6 +158,8 @@ android {
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
buildConfigField "String", "CDSH_CODE_HASH", "\"ec31a51880d19a5e9e0fed404740c1a3ff53a553125564b745acce475f0fded8\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
@@ -172,7 +170,7 @@ android {
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44}"
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44,49,33,41}"
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
@@ -334,7 +332,7 @@ android {
applicationIdSuffix ".staging"
buildConfigField "String", "SIGNAL_URL", "\"https://chat.staging.signal.org\""
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\""
@@ -342,7 +340,7 @@ android {
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
"\"51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982\", " +
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
@@ -399,153 +397,159 @@ android {
}
dependencies {
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.fragment:fragment-ktx:1.3.5'
implementation libs.androidx.core.ktx
implementation libs.androidx.fragment.ktx
lintChecks project(':lintchecks')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring libs.android.tools.desugar
implementation ('androidx.appcompat:appcompat:1.2.0') {
force = true
implementation (libs.androidx.appcompat) {
version {
strictly '1.2.0'
}
}
implementation "androidx.window:window:1.0.0-alpha09"
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.navigation:navigation-fragment:2.1.0'
implementation 'androidx.navigation:navigation-ui: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-common-java8:2.1.0'
implementation 'androidx.lifecycle:lifecycle-reactivestreams-ktx:2.1.0'
implementation "androidx.camera:camera-core:1.0.0-beta11"
implementation "androidx.camera:camera-camera2:1.0.0-beta11"
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.autofill:autofill:1.0.0"
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.sharetarget:sharetarget:1.1.0"
implementation libs.androidx.window
implementation libs.androidx.recyclerview
implementation libs.material.material
implementation libs.androidx.legacy.support
implementation libs.androidx.cardview
implementation libs.androidx.preference
implementation libs.androidx.legacy.preference
implementation libs.androidx.gridlayout
implementation libs.androidx.exifinterface
implementation libs.androidx.constraintlayout
implementation libs.androidx.multidex
implementation libs.androidx.navigation.fragment.ktx
implementation libs.androidx.navigation.ui.ktx
implementation libs.androidx.lifecycle.extensions
implementation libs.androidx.lifecycle.viewmodel.savedstate
implementation libs.androidx.lifecycle.common.java8
implementation libs.androidx.lifecycle.reactivestreams.ktx
implementation libs.androidx.camera.core
implementation libs.androidx.camera.camera2
implementation libs.androidx.camera.lifecycle
implementation libs.androidx.camera.view
implementation libs.androidx.concurrent.futures
implementation libs.androidx.autofill
implementation libs.androidx.biometric
implementation libs.androidx.sharetarget
implementation ('com.google.firebase:firebase-messaging:22.0.0') {
implementation (libs.firebase.messaging) {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
implementation 'com.google.android.gms:play-services-maps:16.1.0'
implementation 'com.google.android.gms:play-services-auth:16.0.1'
implementation libs.google.play.services.maps
implementation libs.google.play.services.auth
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'com.google.android.exoplayer:extension-mediasession:2.9.1'
implementation libs.bundles.exoplayer
implementation 'org.conscrypt:conscrypt-android:2.0.0'
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation libs.conscrypt.android
implementation libs.signal.aesgcmprovider
implementation project(':libsignal-service')
implementation project(':paging')
implementation project(':core-util')
implementation project(':video')
implementation project(':device-transfer')
implementation project(':image-editor')
implementation 'org.signal:zkgroup-android:0.7.0'
implementation "org.whispersystems:signal-client-android:${LIBSIGNAL_CLIENT_VERSION}"
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
implementation libs.signal.zkgroup.android
implementation libs.signal.client.android
implementation libs.google.protobuf.javalite
implementation('com.mobilecoin:android-sdk:1.1.0') {
implementation(libs.mobilecoin) {
exclude group: 'com.google.protobuf'
}
implementation 'org.signal:argon2:13.1@aar'
implementation(libs.signal.argon2) {
artifact {
type = "aar"
}
}
implementation 'org.signal:ringrtc-android:2.11.1'
implementation libs.signal.ringrtc
implementation "me.leolin:ShortcutBadger:1.1.22"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation 'com.github.bumptech.glide:glide:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
kapt 'androidx.annotation:annotation:1.1.0'
implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0'
implementation 'pl.tajchert:waitingdots:0.1.0'
implementation 'com.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0'
implementation 'mobi.upod:time-duration-picker:1.1.3'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.zxing:core:3.2.1'
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
implementation libs.leolin.shortcutbadger
implementation libs.emilsjolander.stickylistheaders
implementation libs.jpardogo.materialtabstrip
implementation libs.apache.httpclient.android
implementation libs.photoview
implementation libs.glide.glide
kapt libs.glide.compiler
kapt libs.androidx.annotation
implementation libs.roundedimageview
implementation libs.materialish.progress
implementation libs.greenrobot.eventbus
implementation libs.waitingdots
implementation libs.floatingactionbutton
implementation libs.google.zxing.android.integration
implementation libs.time.duration.picker
implementation libs.textdrawable
implementation libs.google.zxing.core
implementation (libs.subsampling.scale.image.view) {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
implementation (libs.numberpickerview) {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
implementation (libs.android.tooltips) {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
implementation (libs.android.smsmms) {
exclude group: 'com.squareup.okhttp', module: 'okhttp'
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation 'com.annimon:stream:1.1.8'
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
implementation libs.stream
implementation (libs.colorpicker) {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
implementation 'com.airbnb.android:lottie:3.6.0'
implementation libs.lottie
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
implementation libs.stickyheadergrid
implementation libs.circular.progress.button
implementation "net.zetetic:android-database-sqlcipher:4.4.3"
implementation "androidx.sqlite:sqlite:2.1.0"
implementation libs.signal.android.database.sqlcipher
implementation libs.androidx.sqlite
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
implementation (libs.google.ez.vcard) {
exclude group: 'com.fasterxml.jackson.core'
exclude group: 'org.freemarker'
}
implementation 'dnsjava:dnsjava:2.1.9'
implementation libs.dnsjava
flipperImplementation 'com.facebook.flipper:flipper:0.91.0'
flipperImplementation 'com.facebook.soloader:soloader:0.10.1'
flipperImplementation libs.facebook.flipper
flipperImplementation libs.facebook.soloader
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'org.mockito:mockito-core:2.8.9'
testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.4'
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
testImplementation testLibs.junit.junit
testImplementation testLibs.assertj.core
testImplementation testLibs.mockito.core
testImplementation testLibs.powermock.api.mockito
testImplementation testLibs.powermock.module.junit4.core
testImplementation testLibs.powermock.module.junit4.rule
testImplementation testLibs.powermock.classloading.xstream
testImplementation 'androidx.test:core:1.2.0'
testImplementation ('org.robolectric:robolectric:4.4') {
testImplementation testLibs.androidx.test.core
testImplementation (testLibs.robolectric.robolectric) {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
testImplementation 'org.robolectric:shadows-multidex:4.4'
testImplementation 'org.hamcrest:hamcrest:2.2'
testImplementation testLibs.robolectric.shadows.multidex
testImplementation testLibs.hamcrest.hamcrest
testImplementation(testFixtures(project(":libsignal-service")))
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation testLibs.androidx.test.ext.junit
androidTestImplementation testLibs.espresso.core
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0"
implementation libs.kotlin.stdlib.jdk8
implementation libs.kotlin.reflect
implementation libs.jackson.module.kotlin
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1'
implementation libs.rxjava3.rxandroid
implementation libs.rxjava3.rxkotlin
}
dependencyVerification {

View File

@@ -11,9 +11,9 @@ import androidx.annotation.Nullable;
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
import com.facebook.flipper.plugins.databases.DatabaseDriver;
import net.sqlcipher.DatabaseUtils;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteStatement;
import net.zetetic.database.DatabaseUtils;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;

View File

@@ -176,7 +176,6 @@
<activity android:name=".sharing.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -318,7 +317,8 @@
<activity android:name=".messagedetails.MessageDetailsActivity"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
@@ -366,17 +366,18 @@
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.MediaSendActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.v2.MediaSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseChangeActivity"
android:label="@string/AndroidManifest__change_passphrase"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".VerifyIdentityActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".components.settings.app.AppSettingsActivity"
@@ -389,6 +390,10 @@
</intent-filter>
</activity>
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.ConversationSettings"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 91 KiB

File diff suppressed because one or more lines are too long

View File

@@ -60,6 +60,7 @@ import androidx.camera.core.impl.LensFacingConverter;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.core.util.Consumer;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
@@ -130,6 +131,11 @@ public final class SignalCameraView extends FrameLayout {
// For accessibility event
private MotionEvent mUpEvent;
// BEGIN Custom Signal Code Block
private Consumer<Throwable> errorConsumer;
private Throwable pendingError;
// END Custom Signal Code Block
public SignalCameraView(@NonNull Context context) {
this(context, null);
}
@@ -167,14 +173,32 @@ public final class SignalCameraView extends FrameLayout {
* androidx.lifecycle.Lifecycle.State#DESTROYED} state.
* @throws IllegalStateException if camera permissions are not granted.
*/
// BEGIN Custom Signal Code Block
@RequiresPermission(permission.CAMERA)
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner) {
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner, Consumer<Throwable> errorConsumer) {
mCameraModule.bindToLifecycle(lifecycleOwner);
this.errorConsumer = errorConsumer;
if (pendingError != null) {
errorConsumer.accept(pendingError);
}
}
// END Custom Signal Code Block
private void init(Context context, @Nullable AttributeSet attrs) {
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
mCameraModule = new SignalCameraXModule(this);
// Begin custom signal code block
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
mCameraModule = new SignalCameraXModule(this, error -> {
if (errorConsumer != null) {
errorConsumer.accept(error);
} else {
pendingError = error;
}
});
// End custom signal code block
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);

View File

@@ -46,6 +46,7 @@ import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.util.Consumer;
import androidx.core.util.Preconditions;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
@@ -123,7 +124,9 @@ final class SignalCameraXModule {
@Nullable
ProcessCameraProvider mCameraProvider;
SignalCameraXModule(SignalCameraView view) {
// BEGIN Custom Signal Code Block
SignalCameraXModule(SignalCameraView view, Consumer<Throwable> errorConsumer) {
// END Custom Signal Code Block
mCameraView = view;
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
@@ -141,7 +144,9 @@ final class SignalCameraXModule {
@Override
public void onFailure(Throwable t) {
throw new RuntimeException("CameraX failed to initialize.", t);
// BEGIN Custom Signal Code Block
errorConsumer.accept(t);
// END Custom Signal Code Block
}
}, CameraXExecutors.mainThreadExecutor());
@@ -222,17 +227,10 @@ final class SignalCameraXModule {
// End Signal Custom Code Block
Rational targetAspectRatio;
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
// Begin Signal Custom Code Block
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
// End Signal Custom Code Block
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
} else {
// Begin Signal Custom Code Block
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
// End Signal Custom Code Block
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
}
// Begin Signal Custom Code Block
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
// End Signal Custom Code Block
// Begin Signal Custom Code Block
mImageCaptureBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());

View File

@@ -19,6 +19,6 @@ public final class AppCapabilities {
* asking if the user has set a Signal PIN or not.
*/
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS);
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, FeatureFlags.changeNumber());
}
}

View File

@@ -18,7 +18,6 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.os.Build;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -29,6 +28,7 @@ import androidx.multidex.MultiDexApplication;
import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.Conscrypt;
import org.greenrobot.eventbus.EventBus;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger;
@@ -125,7 +125,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
super.onCreate();
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load(this))
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load())
.addBlocking("logging", () -> {
initializeLogging();
Log.i(TAG, "onCreate()");
@@ -135,6 +135,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
})
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
.addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
@@ -171,6 +172,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
.addNonBlocking(EmojiSource::refresh)
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))

View File

@@ -86,7 +86,8 @@ public abstract class BaseActivity extends AppCompatActivity {
int appCompatNightMode = getDelegate().getLocalNightMode() != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED ? getDelegate().getLocalNightMode()
: AppCompatDelegate.getDefaultNightMode();
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
configuration.orientation = Configuration.ORIENTATION_UNDEFINED;
applyOverrideConfiguration(configuration);
}

View File

@@ -1,8 +1,6 @@
package org.thoughtcrime.securesms;
import android.graphics.Point;
import android.net.Uri;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
@@ -12,7 +10,6 @@ import androidx.lifecycle.Observer;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
@@ -29,7 +26,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
@@ -49,11 +45,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
boolean pulseMention,
boolean hasWallpaper,
boolean isMessageRequestAccepted,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean canPlayInline,
@NonNull Colorizer colorizer);
ConversationMessage getConversationMessage();
@NonNull ConversationMessage getConversationMessage();
void setEventListener(@Nullable EventListener listener);
@@ -71,7 +66,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onAddToContactsClicked(@NonNull Contact contact);
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
void onReactionClicked(@NonNull MultiselectPart multiselectPart, long messageId, boolean isMms);
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
@@ -92,6 +87,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onPlayInlineContent(ConversationMessage conversationMessage);
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);

View File

@@ -15,6 +15,7 @@ import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
@@ -50,6 +51,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_CREATE_SIGNAL_PIN = 7;
private static final int STATE_TRANSFER_ONGOING = 8;
private static final int STATE_TRANSFER_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -153,6 +155,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
default: return null;
}
}
@@ -176,6 +179,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_TRANSFER_ONGOING;
} else if (SignalStore.misc().isOldDeviceTransferLocked()) {
return STATE_TRANSFER_LOCKED;
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
return STATE_CHANGE_NUMBER_LOCK;
} else {
return STATE_NORMAL;
}
@@ -243,6 +248,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return MainActivity.clearTop(this);
}
private Intent getChangeNumberLockIntent() {
return ChangeNumberLockActivity.createIntent(this);
}
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
final Intent intent = new Intent(this, destination);
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);

View File

@@ -43,32 +43,38 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.widget.CompoundButton;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextSwitcher;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.OneShotPreDrawListener;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ShapeScrim;
import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -79,6 +85,7 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
@@ -109,13 +116,13 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
private static final String IDENTITY_EXTRA = "recipient_identity";
private static final String VERIFIED_EXTRA = "verified_state";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityDatabase.IdentityRecord identityRecord)
@NonNull IdentityRecord identityRecord)
{
return newIntent(context,
identityRecord.getRecipientId(),
@@ -124,7 +131,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
}
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityDatabase.IdentityRecord identityRecord,
@NonNull IdentityRecord identityRecord,
boolean verified)
{
return newIntent(context,
@@ -154,9 +161,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
@Override
protected void onCreate(Bundle state, boolean ready) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_number);
Bundle extras = new Bundle();
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
@@ -214,7 +218,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
public static class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener {
public static final String RECIPIENT_ID = "recipient_id";
public static final String REMOTE_NUMBER = "remote_number";
@@ -228,28 +232,41 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
private IdentityKey remoteIdentity;
private Fingerprint fingerprint;
private Toolbar toolbar;
private ScrollView scrollView;
private View container;
private View numbersContainer;
private View loading;
private View qrCodeContainer;
private ImageView qrCode;
private ImageView qrVerified;
private TextView tapLabel;
private TextSwitcher tapLabel;
private TextView description;
private View.OnClickListener clickListener;
private SwitchCompat verified;
private Button verifyButton;
private View toolbarShadow;
private View bottomShadow;
private TextView[] codes = new TextView[12];
private boolean animateSuccessOnDraw = false;
private boolean animateFailureOnDraw = false;
private boolean currentVerifiedState = false;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
this.toolbar = container.findViewById(R.id.toolbar);
this.scrollView = container.findViewById(R.id.scroll_view);
this.numbersContainer = container.findViewById(R.id.number_table);
this.loading = container.findViewById(R.id.loading);
this.qrCodeContainer = container.findViewById(R.id.qr_code_container);
this.qrCode = container.findViewById(R.id.qr_code);
this.verified = container.findViewById(R.id.verified_switch);
this.verifyButton = container.findViewById(R.id.verify_button);
this.qrVerified = container.findViewById(R.id.qr_verified);
this.description = container.findViewById(R.id.description);
this.tapLabel = container.findViewById(R.id.tap_label);
this.toolbarShadow = container.findViewById(R.id.toolbar_shadow);
this.bottomShadow = container.findViewById(R.id.verify_identity_bottom_shadow);
this.codes[0] = container.findViewById(R.id.code_first);
this.codes[1] = container.findViewById(R.id.code_second);
this.codes[2] = container.findViewById(R.id.code_third);
@@ -263,15 +280,25 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
this.codes[10] = container.findViewById(R.id.code_eleventh);
this.codes[11] = container.findViewById(R.id.code_twelth);
this.qrCode.setOnClickListener(clickListener);
this.qrCodeContainer.setOnClickListener(clickListener);
this.registerForContextMenu(numbersContainer);
this.verified.setChecked(getArguments().getBoolean(VERIFIED_STATE, false));
this.verified.setOnCheckedChangeListener(this);
updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false);
this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true)));
this.scrollView.getViewTreeObserver().addOnScrollChangedListener(this);
((AppCompatActivity)requireActivity()).setSupportActionBar(toolbar);
((AppCompatActivity)requireActivity()).setTitle(R.string.AndroidManifest__verify_safety_number);
return container;
}
@Override public void onDestroyView() {
this.scrollView.getViewTreeObserver().removeOnScrollChangedListener(this);
super.onDestroyView();
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
@@ -327,6 +354,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
@Override
protected void onPostExecute(Fingerprint fingerprint) {
if (getActivity() == null) return;
VerifyDisplayFragment.this.fingerprint = fingerprint;
setFingerprintViews(fingerprint, true);
getActivity().supportInvalidateOptionsMenu();
@@ -353,6 +381,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
animateFailureOnDraw = false;
animateVerifiedFailure();
}
ThreadUtil.postToMain(this::onScrollChanged);
}
@Override
@@ -410,9 +440,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
} else {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
}
this.animateFailureOnDraw = true;
} catch (Exception e) {
Log.w(TAG, e);
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show();
this.animateFailureOnDraw = true;
}
}
@@ -480,7 +512,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
}
private void setRecipientText(Recipient recipient) {
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
description.setMovementMethod(LinkMovementMethod.getInstance());
}
@@ -501,9 +533,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
if (animate) {
ViewUtil.fadeIn(qrCode, 1000);
ViewUtil.fadeIn(tapLabel, 1000);
ViewUtil.fadeOut(loading, 300, View.GONE);
} else {
qrCode.setVisibility(View.VISIBLE);
tapLabel.setVisibility(View.VISIBLE);
loading.setVisibility(View.GONE);
}
}
@@ -559,6 +593,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
qrVerified.setImageBitmap(qrSuccess);
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY);
tapLabel.setText(getString(R.string.verify_display_fragment__successful_match));
animateVerified();
}
@@ -569,6 +605,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
qrVerified.setImageBitmap(qrSuccess);
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY);
tapLabel.setText(getString(R.string.verify_display_fragment__failed_to_verify_safety_number));
animateVerified();
}
@@ -576,7 +614,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setInterpolator(new OvershootInterpolator());
scaleAnimation.setInterpolator(new FastOutSlowInInterpolator());
scaleAnimation.setDuration(800);
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
@@ -594,6 +632,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
scaleAnimation.setInterpolator(new AnticipateInterpolator());
scaleAnimation.setDuration(500);
ViewUtil.animateOut(qrVerified, scaleAnimation, View.GONE);
ViewUtil.fadeIn(qrCode, 800);
qrCodeContainer.setEnabled(true);
tapLabel.setText(getString(R.string.verify_display_fragment__tap_to_scan));
}
}, 2000);
}
@@ -602,40 +643,70 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
public void onAnimationRepeat(Animation animation) {}
});
ViewUtil.fadeOut(qrCode, 200, View.INVISIBLE);
ViewUtil.animateIn(qrVerified, scaleAnimation);
qrCodeContainer.setEnabled(false);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
final Recipient recipient = this.recipient.get();
final RecipientId recipientId = recipient.getId();
private void updateVerifyButton(boolean verified, boolean update) {
currentVerifiedState = verified;
SignalExecutors.BOUNDED.execute(() -> {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
if (isChecked) {
Log.i(TAG, "Saving identity: " + recipientId);
DatabaseFactory.getIdentityDatabase(getActivity())
.saveIdentity(recipientId,
remoteIdentity,
VerifiedStatus.VERIFIED, false,
System.currentTimeMillis(), true);
} else {
DatabaseFactory.getIdentityDatabase(getActivity())
.setVerified(recipientId,
remoteIdentity,
VerifiedStatus.DEFAULT);
if (verified) {
verifyButton.setText(R.string.verify_display_fragment__clear_verification);
} else {
verifyButton.setText(R.string.verify_display_fragment__mark_as_verified);
}
if (update) {
final RecipientId recipientId = recipient.getId();
SignalExecutors.BOUNDED.execute(() -> {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
if (verified) {
Log.i(TAG, "Saving identity: " + recipientId);
ApplicationDependencies.getIdentityStore()
.saveIdentityWithoutSideEffects(recipientId,
remoteIdentity,
VerifiedStatus.VERIFIED,
false,
System.currentTimeMillis(),
true);
} else {
ApplicationDependencies.getIdentityStore().setVerified(recipientId, remoteIdentity, VerifiedStatus.DEFAULT);
}
ApplicationDependencies.getJobManager()
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
remoteIdentity,
verified ? VerifiedStatus.VERIFIED
: VerifiedStatus.DEFAULT));
StorageSyncHelper.scheduleSyncForDataChange();
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), verified, false);
}
});
}
}
ApplicationDependencies.getJobManager()
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
remoteIdentity,
isChecked ? VerifiedStatus.VERIFIED
: VerifiedStatus.DEFAULT));
StorageSyncHelper.scheduleSyncForDataChange();
IdentityUtil.markIdentityVerified(getActivity(), recipient, isChecked, false);
@Override public void onScrollChanged() {
if (scrollView.canScrollVertically(-1)) {
if (toolbarShadow.getVisibility() != View.VISIBLE) {
ViewUtil.fadeIn(toolbarShadow, 250);
}
});
} else {
if (toolbarShadow.getVisibility() != View.GONE) {
ViewUtil.fadeOut(toolbarShadow, 250);
}
}
if (scrollView.canScrollVertically(1)) {
if (bottomShadow.getVisibility() != View.VISIBLE) {
ViewUtil.fadeIn(bottomShadow, 250);
}
} else {
ViewUtil.fadeOut(bottomShadow, 250);
}
}
}
@@ -643,12 +714,23 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
private View container;
private CameraView cameraView;
private ShapeScrim cameraScrim;
private ImageView cameraMarks;
private ScanningThread scanningThread;
private ScanListener scanListener;
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
this.cameraView = container.findViewById(R.id.scanner);
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
this.cameraView = container.findViewById(R.id.scanner);
this.cameraScrim = container.findViewById(R.id.camera_scrim);
this.cameraMarks = container.findViewById(R.id.camera_marks);
OneShotPreDrawListener.add(cameraScrim, () -> {
int width = cameraScrim.getScrimWidth();
int height = cameraScrim.getScrimHeight();
ViewUtil.updateLayoutParams(cameraMarks, width, height);
});
return container;
}
@@ -685,5 +767,4 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
}
}
}

View File

@@ -17,8 +17,6 @@
package org.thoughtcrime.securesms;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PictureInPictureParams;
@@ -79,6 +77,7 @@ import org.thoughtcrime.securesms.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
@@ -86,6 +85,8 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
private static final String TAG = Log.tag(WebRtcCallActivity.class);
@@ -366,15 +367,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void handleSetAudioHandset() {
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(false);
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.EARPIECE);
}
private void handleSetAudioSpeaker() {
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(true);
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE);
}
private void handleSetAudioBluetooth() {
ApplicationDependencies.getSignalCallManager().setAudioBluetooth(true);
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.BLUETOOTH);
}
private void handleSetMuteAudio(boolean enabled) {

View File

@@ -14,7 +14,6 @@ import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import androidx.annotation.RequiresApi
import org.thoughtcrime.securesms.components.AvatarImageView
private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
private const val WIDTH = "signal.circleavatartransition.width"
@@ -36,7 +35,7 @@ class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transitio
private fun captureValues(transitionValues: TransitionValues) {
val view: View = transitionValues.view
if (view is AvatarImageView) {
if (view.transitionName == "avatar") {
val topLeft = intArrayOf(0, 0)
view.getLocationOnScreen(topLeft)
transitionValues.values[POSITION_ON_SCREEN] = topLeft
@@ -51,7 +50,7 @@ class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transitio
}
val view: View = endValues.view
if (view !is AvatarImageView || view.transitionName != "avatar") {
if (view.transitionName != "avatar") {
return null
}

View File

@@ -48,8 +48,9 @@ object AvatarRenderer {
avatar: Avatar.Text,
inverted: Boolean = false,
size: Int = DIMENSIONS,
synchronous: Boolean = false
): Drawable {
return TextAvatarDrawable(context, avatar, inverted, size)
return TextAvatarDrawable(context, avatar, inverted, size, synchronous)
}
private fun renderVector(context: Context, avatar: Avatar.Vector, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
@@ -66,7 +67,7 @@ object AvatarRenderer {
private fun renderText(context: Context, avatar: Avatar.Text, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas ->
val textDrawable = createTextDrawable(context, avatar)
val textDrawable = createTextDrawable(context, avatar, synchronous = true)
canvas.drawColor(avatar.color.backgroundColor)
textDrawable.draw(canvas)

View File

@@ -3,52 +3,59 @@ package org.thoughtcrime.securesms.avatar
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.util.TypedValue
import android.view.Gravity
import android.widget.FrameLayout
import androidx.core.view.updateLayoutParams
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import android.text.Layout
import android.text.SpannableString
import android.text.StaticLayout
import android.text.TextPaint
import androidx.core.graphics.withTranslation
import org.thoughtcrime.securesms.components.emoji.EmojiProvider
/**
* Uses EmojiTextView to properly render a Text Avatar with emoji in it.
*/
class TextAvatarDrawable(
context: Context,
avatar: Avatar.Text,
private val context: Context,
private val avatar: Avatar.Text,
inverted: Boolean = false,
private val size: Int = AvatarRenderer.DIMENSIONS,
private val synchronous: Boolean = false
) : Drawable() {
private val layout: FrameLayout = FrameLayout(context)
private val textView: EmojiTextView = EmojiTextView(context)
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
init {
textView.typeface = AvatarRenderer.getTypeface(context)
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f))
textView.text = avatar.text
textView.gravity = Gravity.CENTER
textView.setTextColor(if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor)
textView.setForceCustomEmoji(true)
textPaint.typeface = AvatarRenderer.getTypeface(context)
textPaint.color = if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor
textPaint.density = context.resources.displayMetrics.density
layout.addView(textView)
textView.updateLayoutParams {
width = size
height = size
}
layout.measure(size, size)
layout.layout(0, 0, size, size)
setBounds(0, 0, size, size)
}
override fun getIntrinsicHeight(): Int = size
override fun getIntrinsicWidth(): Int = size
override fun draw(canvas: Canvas) {
layout.draw(canvas)
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
val width = bounds.width()
val candidates = EmojiProvider.getCandidates(avatar.text)
var hasEmoji = false
textPaint.textSize = textSize
val newText = if (candidates == null || candidates.size() == 0) {
SpannableString(avatar.text)
} else {
EmojiProvider.emojify(context, candidates, avatar.text, textPaint, synchronous)
}
if (newText == null) return
val layout = StaticLayout(SpannableString(newText), textPaint, width, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
layout.draw(canvas, getStartX(layout), ((bounds.height() / 2) - ((layout.height / 2))).toFloat())
}
private fun getStartX(layout: StaticLayout): Float {
val direction = layout.getParagraphDirection(0)
val lineWidth = layout.getLineWidth(0)
val width = bounds.width()
val xPos = (width - lineWidth) / 2
return if (direction == Layout.DIR_LEFT_TO_RIGHT) xPos else -xPos
}
override fun setAlpha(alpha: Int) = Unit
@@ -56,4 +63,10 @@ class TextAvatarDrawable(
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
override fun getOpacity(): Int = PixelFormat.OPAQUE
private fun Layout.draw(canvas: Canvas, x: Float, y: Float) {
canvas.withTranslation(x, y) {
draw(canvas)
}
}
}

View File

@@ -57,6 +57,16 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
}
}
override fun onCancelEditing() {
Navigation.findNavController(requireView()).popBackStack()
}
override fun onMainImageLoaded() {
}
override fun onMainImageFailedToLoad() {
}
companion object {
const val REQUEST_KEY_EDIT = "org.thoughtcrime.securesms.avatar.photo.EDIT"

View File

@@ -15,6 +15,7 @@ import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarBundler
@@ -30,7 +31,6 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
import java.util.Objects
/**
* Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults.
@@ -111,7 +111,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
putParcelable(SELECT_AVATAR_MEDIA, it)
}
)
Navigation.findNavController(v).popBackStack()
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
},
{
setFragmentResult(
@@ -120,7 +120,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
putBoolean(SELECT_AVATAR_CLEAR, true)
}
)
Navigation.findNavController(v).popBackStack()
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
}
)
}
@@ -147,9 +147,10 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
ViewUtil.hideKeyboard(requireContext(), requireView())
}
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
val media: Media = Objects.requireNonNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
val media: Media = requireNotNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
viewModel.onAvatarPhotoSelectionCompleted(media)
} else {
super.onActivityResult(requestCode, resultCode, data)
@@ -195,23 +196,24 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
}
}
fun openPhotoEditor(photo: Avatar.Photo) {
private fun openPhotoEditor(photo: Avatar.Photo) {
Navigation.findNavController(requireView())
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo)))
}
fun openVectorEditor(vector: Avatar.Vector) {
private fun openVectorEditor(vector: Avatar.Vector) {
Navigation.findNavController(requireView())
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector)))
}
fun openTextEditor(text: Avatar.Text?) {
private fun openTextEditor(text: Avatar.Text?) {
val bundle = if (text != null) AvatarBundler.bundleText(text) else null
Navigation.findNavController(requireView())
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle))
}
fun openCameraCapture() {
@Suppress("DEPRECATION")
private fun openCameraCapture() {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
@@ -226,7 +228,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
.execute()
}
fun openGallery() {
@Suppress("DEPRECATION")
private fun openGallery() {
Permissions.with(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.ifNecessary()

View File

@@ -106,7 +106,7 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
Navigation.findNavController(v).popBackStack()
}
textInput.setOnEditorActionListener { v, actionId, event ->
textInput.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
tabLayout.getTabAt(1)?.select()
true

View File

@@ -13,7 +13,7 @@ import androidx.documentfile.provider.DocumentFile;
import com.annimon.stream.function.Predicate;
import com.google.protobuf.ByteString;
import net.sqlcipher.database.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions;

View File

@@ -11,7 +11,7 @@ import android.util.Pair;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions;
@@ -162,9 +162,8 @@ public class FullBackupImporter extends FullBackupBase {
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
throws IOException
{
File partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE);
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
File dataFile = AttachmentDatabase.newFile(context);
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
ContentValues contentValues = new ContentValues();

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.badges
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.annotation.ColorInt
import androidx.annotation.Px
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.res.use
import androidx.lifecycle.Lifecycle
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.Badges.insetWithOutline
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
private val TAG = Log.tag(BadgeImageView::class.java)
class BadgeImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {
@Px
private var outlineWidth: Float = 0f
@ColorInt
private var outlineColor: Int = Color.BLACK
init {
context.obtainStyledAttributes(attrs, R.styleable.BadgeImageView).use {
outlineWidth = it.getDimension(R.styleable.BadgeImageView_badge_outline_width, 0f)
outlineColor = it.getColor(R.styleable.BadgeImageView_badge_outline_color, Color.BLACK)
}
}
fun setBadgeFromRecipient(recipient: Recipient?) {
if (recipient == null || recipient.badges.isEmpty()) {
setBadge(null)
} else {
setBadge(recipient.badges[0])
}
}
fun setBadge(badge: Badge?) {
visible = badge != null
val lifecycle = ViewUtil.getActivityLifecycle(this)
if (lifecycle?.currentState == Lifecycle.State.DESTROYED) {
Log.w(TAG, "Ignoring setBadge call for destroyed activity.")
return
}
GlideApp
.with(this)
.load(badge)
.into(this)
}
override fun setImageDrawable(drawable: Drawable?) {
if (drawable == null || outlineWidth == 0f) {
super.setImageDrawable(drawable)
} else {
super.setImageDrawable(
drawable.insetWithOutline(
outlineWidth, outlineColor
)
)
}
}
}

View File

@@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.badges
import android.content.Context
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ProfileUtil
class BadgeRepository(context: Context) {
private val context = context.applicationContext
fun setVisibilityForAllBadges(displayBadgesOnProfile: Boolean): Completable = Completable.fromAction {
val badges = Recipient.self().badges.map { it.copy(visible = displayBadgesOnProfile) }
ProfileUtil.uploadProfileWithBadges(context, badges)
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
recipientDatabase.setBadges(Recipient.self().id, badges)
}.subscribeOn(Schedulers.io())
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
val badges = Recipient.self().badges
val reOrderedBadges = listOf(featuredBadge) + (badges - featuredBadge)
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges)
}.subscribeOn(Schedulers.io())
}

View File

@@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.badges
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import androidx.annotation.Px
import androidx.core.graphics.withScale
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.SimpleColorFilter
import com.google.android.flexbox.AlignItems
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.flexbox.JustifyContent
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.BadgeAnimator
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.util.customizeOnDraw
object Badges {
fun Drawable.insetWithOutline(
@Px outlineWidth: Float,
@ColorInt outlineColor: Int
): Drawable {
val clone = mutate().constantState?.newDrawable()?.mutate()
clone?.colorFilter = SimpleColorFilter(outlineColor)
return customizeOnDraw { wrapped, canvas ->
clone?.bounds = wrapped.bounds
clone?.draw(canvas)
val scale = 1 - ((outlineWidth * 2) / canvas.width)
canvas.withScale(x = scale, y = scale, canvas.width / 2f, canvas.height / 2f) {
wrapped.draw(canvas)
}
}
}
fun Drawable.selectable(
@Px outlineWidth: Float,
@ColorInt outlineColor: Int,
@ColorInt gapColor: Int,
animator: BadgeAnimator
): Drawable {
val outline = mutate().constantState?.newDrawable()?.mutate()
outline?.colorFilter = SimpleColorFilter(outlineColor)
val gap = mutate().constantState?.newDrawable()?.mutate()
gap?.colorFilter = SimpleColorFilter(gapColor)
return customizeOnDraw { wrapped, canvas ->
outline?.bounds = wrapped.bounds
gap?.bounds = wrapped.bounds
outline?.draw(canvas)
val scale = 1 - ((outlineWidth * 2) / wrapped.bounds.width())
val interpolatedScale = scale + (1f - scale) * animator.getFraction()
canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) {
gap?.draw(canvas)
canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) {
wrapped.draw(canvas)
}
}
if (animator.shouldInvalidate()) {
invalidateSelf()
}
}
}
fun DSLConfiguration.displayBadges(badges: List<Badge>, selectedBadge: Badge? = null) {
badges
.map { Badge.Model(it, it == selectedBadge) }
.forEach { customPref(it) }
val empties = (4 - (badges.size % 4)) % 4
repeat(empties) {
customPref(Badge.EmptyModel())
}
}
fun createLayoutManagerForGridWithBadges(context: Context): RecyclerView.LayoutManager {
val layoutManager = FlexboxLayoutManager(context)
layoutManager.flexDirection = FlexDirection.ROW
layoutManager.alignItems = AlignItems.CENTER
layoutManager.justifyContent = JustifyContent.CENTER
return layoutManager
}
}

View File

@@ -0,0 +1,221 @@
package org.thoughtcrime.securesms.badges.models
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.bumptech.glide.load.Key
import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.transition.Transition
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.Badges.selectable
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.MappingViewHolder
import java.security.MessageDigest
typealias OnBadgeClicked = (Badge, Boolean) -> Unit
/**
* A Badge that can be collected and displayed by a user.
*/
data class Badge(
val id: String,
val category: Category,
val imageUrl: Uri,
val name: String,
val description: String,
val expirationTimestamp: Long,
val visible: Boolean
) : Parcelable, Key {
constructor(parcel: Parcel) : this(
requireNotNull(parcel.readString()),
Category.fromCode(requireNotNull(parcel.readString())),
requireNotNull(parcel.readParcelable(Uri::class.java.classLoader)),
requireNotNull(parcel.readString()),
requireNotNull(parcel.readString()),
parcel.readLong(),
parcel.readByte() == 1.toByte()
)
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(id)
parcel.writeString(category.code)
parcel.writeParcelable(imageUrl, flags)
parcel.writeString(name)
parcel.writeString(description)
parcel.writeLong(expirationTimestamp)
parcel.writeByte(if (visible) 1 else 0)
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(id.toByteArray(Key.CHARSET))
}
fun resolveDescription(shortName: String): String {
return description.replace("{short_name}", shortName)
}
class EmptyModel : PreferenceModel<EmptyModel>() {
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
}
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
private val name: TextView = itemView.findViewById(R.id.name)
init {
itemView.isEnabled = false
itemView.isFocusable = false
itemView.isClickable = false
itemView.visibility = View.INVISIBLE
name.text = " "
}
override fun bind(model: EmptyModel) = Unit
}
class Model(
val badge: Badge,
val isSelected: Boolean = false
) : PreferenceModel<Model>() {
override fun areItemsTheSame(newItem: Model): Boolean {
return newItem.badge.id == badge.id
}
override fun areContentsTheSame(newItem: Model): Boolean {
return super.areContentsTheSame(newItem) && badge == newItem.badge && isSelected == newItem.isSelected
}
override fun getChangePayload(newItem: Model): Any? {
return if (badge == newItem.badge && isSelected != newItem.isSelected) {
SELECTION_CHANGED
} else {
null
}
}
}
class ViewHolder(itemView: View, private val onBadgeClicked: OnBadgeClicked) : MappingViewHolder<Model>(itemView) {
private val badge: ImageView = itemView.findViewById(R.id.badge)
private val name: TextView = itemView.findViewById(R.id.name)
private val target = Target(badge)
override fun bind(model: Model) {
itemView.setOnClickListener {
onBadgeClicked(model.badge, model.isSelected)
}
if (payload.isNotEmpty()) {
if (model.isSelected) {
target.animateToStart()
} else {
target.animateToEnd()
}
return
}
GlideApp.with(badge)
.load(model.badge)
.into(target)
if (model.isSelected) {
target.setAnimationToStart()
} else {
target.setAnimationToEnd()
}
name.text = model.badge.name
}
}
enum class Category(val code: String) {
Donor("donor"),
Other("other"),
Testing("testing"); // Will be removed before final release
companion object {
fun fromCode(code: String): Category {
return when (code) {
"donor" -> Donor
"testing" -> Testing
else -> Other
}
}
}
}
private class Target(view: ImageView) : CustomViewTarget<ImageView, Drawable>(view) {
private val animator: BadgeAnimator = BadgeAnimator()
override fun onLoadFailed(errorDrawable: Drawable?) {
view.setImageDrawable(errorDrawable)
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
val drawable = resource.selectable(
DimensionUnit.DP.toPixels(2.5f),
ContextCompat.getColor(view.context, R.color.signal_inverse_primary),
ContextCompat.getColor(view.context, R.color.signal_background_primary),
animator
)
view.setImageDrawable(drawable)
}
override fun onResourceCleared(placeholder: Drawable?) {
view.setImageDrawable(placeholder)
}
fun setAnimationToStart() {
animator.setState(BadgeAnimator.State.START)
view.drawable?.invalidateSelf()
}
fun setAnimationToEnd() {
animator.setState(BadgeAnimator.State.END)
view.drawable?.invalidateSelf()
}
fun animateToStart() {
animator.setState(BadgeAnimator.State.REVERSE)
view.drawable?.invalidateSelf()
}
fun animateToEnd() {
animator.setState(BadgeAnimator.State.FORWARD)
view.drawable?.invalidateSelf()
}
}
companion object CREATOR : Parcelable.Creator<Badge> {
private val SELECTION_CHANGED = Any()
override fun createFromParcel(parcel: Parcel): Badge {
return Badge(parcel)
}
override fun newArray(size: Int): Array<Badge?> {
return arrayOfNulls(size)
}
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onBadgeClicked) }, R.layout.badge_preference_view))
mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.badge_preference_view))
}
}
}

View File

@@ -0,0 +1,97 @@
package org.thoughtcrime.securesms.badges.models
import org.thoughtcrime.securesms.util.Util
class BadgeAnimator {
val duration = 250L
var state: State = State.START
private set
private var startTime: Long = 0L
fun getFraction(): Float {
return when (state) {
State.START -> 0f
State.END -> 1f
State.FORWARD -> Util.clamp((System.currentTimeMillis() - startTime) / duration.toFloat(), 0f, 1f)
State.REVERSE -> 1f - Util.clamp((System.currentTimeMillis() - startTime) / duration.toFloat(), 0f, 1f)
}
}
fun setState(newState: State) {
shouldInvalidate()
if (state == newState) {
return
}
if (newState == State.END || newState == State.START) {
state = newState
startTime = 0L
return
}
if (state == State.START && newState == State.REVERSE) {
return
}
if (state == State.END && newState == State.FORWARD) {
return
}
if (state == State.START && newState == State.FORWARD) {
state = State.FORWARD
startTime = System.currentTimeMillis()
return
}
if (state == State.END && newState == State.REVERSE) {
state = State.REVERSE
startTime = System.currentTimeMillis()
return
}
if (state == State.FORWARD && newState == State.REVERSE) {
val elapsed = System.currentTimeMillis() - startTime
val delta = duration - elapsed
startTime -= delta
state = State.REVERSE
return
}
if (state == State.REVERSE && newState == State.FORWARD) {
val elapsed = System.currentTimeMillis() - startTime
val delta = duration - elapsed
startTime -= delta
state = State.FORWARD
return
}
}
fun shouldInvalidate(): Boolean {
if (state == State.START || state == State.END) {
return false
}
if (state == State.FORWARD && getFraction() == 1f) {
state = State.END
return false
}
if (state == State.REVERSE && getFraction() == 0f) {
state = State.START
return false
}
return true
}
enum class State {
START,
FORWARD,
REVERSE,
END
}
}

View File

@@ -0,0 +1,74 @@
package org.thoughtcrime.securesms.badges.models
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import androidx.core.content.ContextCompat
import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.transition.Transition
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.Badges.insetWithOutline
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.MappingViewHolder
object FeaturedBadgePreview {
fun register(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
}
data class Model(val badge: Badge?) : PreferenceModel<Model>() {
override fun areItemsTheSame(newItem: Model): Boolean {
return newItem.badge?.id == badge?.id
}
override fun areContentsTheSame(newItem: Model): Boolean {
return super.areContentsTheSame(newItem) && badge == newItem.badge
}
}
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
private val badge: ImageView = itemView.findViewById(R.id.badge)
private val target: Target = Target(badge)
override fun bind(model: Model) {
avatar.setRecipient(Recipient.self())
avatar.disableQuickContact()
if (model.badge != null) {
GlideApp.with(badge)
.load(model.badge)
.into(target)
} else {
GlideApp.with(badge).clear(badge)
badge.setImageDrawable(null)
}
}
}
private class Target(view: ImageView) : CustomViewTarget<ImageView, Drawable>(view) {
override fun onLoadFailed(errorDrawable: Drawable?) {
view.setImageDrawable(errorDrawable)
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
view.setImageDrawable(
resource.insetWithOutline(
DimensionUnit.DP.toPixels(2.5f),
ContextCompat.getColor(view.context, R.color.signal_background_primary)
)
)
}
override fun onResourceCleared(placeholder: Drawable?) {
view.setImageDrawable(placeholder)
}
}
}

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.badges.models
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.MappingModel
import org.thoughtcrime.securesms.util.MappingViewHolder
data class LargeBadge(
val badge: Badge
) {
class Model(val largeBadge: LargeBadge, val shortName: String) : MappingModel<Model> {
override fun areItemsTheSame(newItem: Model): Boolean {
return newItem.largeBadge.badge.id == largeBadge.badge.id
}
override fun areContentsTheSame(newItem: Model): Boolean {
return newItem.largeBadge == largeBadge && newItem.shortName == shortName
}
}
class EmptyModel : MappingModel<EmptyModel> {
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
override fun areContentsTheSame(newItem: EmptyModel): Boolean = true
}
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
override fun bind(model: EmptyModel) {
}
}
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
private val badge: ImageView = itemView.findViewById(R.id.badge)
private val name: TextView = itemView.findViewById(R.id.name)
private val description: TextView = itemView.findViewById(R.id.description)
override fun bind(model: Model) {
GlideApp.with(badge)
.load(model.largeBadge.badge)
.into(badge)
name.text = model.largeBadge.badge.name
description.text = model.largeBadge.badge.resolveDescription(model.shortName)
}
}
companion object {
fun register(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
}
}
}

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.badges.self.featured
enum class SelectFeaturedBadgeEvent {
NO_BADGE_SELECTED,
FAILED_TO_UPDATE_PROFILE,
SAVE_SUCCESSFUL
}

View File

@@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.badges.self.featured
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.Badges.displayBadges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.FeaturedBadgePreview
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.LifecycleDisposable
/**
* Fragment which allows user to select one of their badges to be their "Featured" badge.
*/
class SelectFeaturedBadgeFragment : DSLSettingsFragment(
titleId = R.string.BadgesOverviewFragment__featured_badge,
layoutId = R.layout.select_featured_badge_fragment,
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
) {
private val viewModel: SelectFeaturedBadgeViewModel by viewModels(factoryProducer = { SelectFeaturedBadgeViewModel.Factory(BadgeRepository(requireContext())) })
private val lifecycleDisposable = LifecycleDisposable()
private lateinit var scrollShadow: View
private lateinit var save: View
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
scrollShadow = view.findViewById(R.id.scroll_shadow)
super.onViewCreated(view, savedInstanceState)
save = view.findViewById(R.id.save)
save.setOnClickListener {
viewModel.save()
}
}
override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
return ToolbarShadowAnimationHelper(scrollShadow)
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
Badge.register(adapter) { badge, isSelected ->
if (!isSelected) {
viewModel.setSelectedBadge(badge)
}
}
val previewView: View = requireView().findViewById(R.id.preview)
val previewViewHolder = FeaturedBadgePreview.ViewHolder(previewView)
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
lifecycleDisposable += viewModel.events.subscribe { event: SelectFeaturedBadgeEvent ->
when (event) {
SelectFeaturedBadgeEvent.NO_BADGE_SELECTED -> Toast.makeText(requireContext(), R.string.SelectFeaturedBadgeFragment__you_must_select_a_badge, Toast.LENGTH_LONG).show()
SelectFeaturedBadgeEvent.FAILED_TO_UPDATE_PROFILE -> Toast.makeText(requireContext(), R.string.SelectFeaturedBadgeFragment__failed_to_update_profile, Toast.LENGTH_LONG).show()
SelectFeaturedBadgeEvent.SAVE_SUCCESSFUL -> findNavController().popBackStack()
}
}
viewModel.state.observe(viewLifecycleOwner) { state ->
save.isEnabled = state.stage == SelectFeaturedBadgeState.Stage.READY
previewViewHolder.bind(FeaturedBadgePreview.Model(state.selectedBadge))
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun getConfiguration(state: SelectFeaturedBadgeState): DSLConfiguration {
return configure {
sectionHeaderPref(R.string.SelectFeaturedBadgeFragment__select_a_badge)
displayBadges(state.allUnlockedBadges, state.selectedBadge)
}
}
}

View File

@@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.badges.self.featured
import org.thoughtcrime.securesms.badges.models.Badge
data class SelectFeaturedBadgeState(
val stage: Stage = Stage.INIT,
val selectedBadge: Badge? = null,
val allUnlockedBadges: List<Badge> = listOf()
) {
enum class Stage {
INIT,
READY,
SAVING
}
}

View File

@@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.badges.self.featured
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.livedata.Store
private val TAG = Log.tag(SelectFeaturedBadgeViewModel::class.java)
class SelectFeaturedBadgeViewModel(private val repository: BadgeRepository) : ViewModel() {
private val store = Store(SelectFeaturedBadgeState())
private val eventSubject = PublishSubject.create<SelectFeaturedBadgeEvent>()
val state: LiveData<SelectFeaturedBadgeState> = store.stateLiveData
val events: Observable<SelectFeaturedBadgeEvent> = eventSubject.observeOn(AndroidSchedulers.mainThread())
private val disposables = CompositeDisposable()
init {
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
state.copy(
stage = if (state.stage == SelectFeaturedBadgeState.Stage.INIT) SelectFeaturedBadgeState.Stage.READY else state.stage,
selectedBadge = recipient.badges.firstOrNull(),
allUnlockedBadges = recipient.badges
)
}
}
fun setSelectedBadge(badge: Badge) {
store.update { it.copy(selectedBadge = badge) }
}
fun save() {
val snapshot = store.state
if (snapshot.selectedBadge == null) {
eventSubject.onNext(SelectFeaturedBadgeEvent.NO_BADGE_SELECTED)
return
}
store.update { it.copy(stage = SelectFeaturedBadgeState.Stage.SAVING) }
disposables += repository.setFeaturedBadge(snapshot.selectedBadge).subscribeBy(
onComplete = {
eventSubject.onNext(SelectFeaturedBadgeEvent.SAVE_SUCCESSFUL)
},
onError = { error ->
Log.e(TAG, "Failed to update profile.", error)
eventSubject.onNext(SelectFeaturedBadgeEvent.FAILED_TO_UPDATE_PROFILE)
}
)
}
override fun onCleared() {
disposables.clear()
}
class Factory(private val badgeRepository: BadgeRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(SelectFeaturedBadgeViewModel(badgeRepository)))
}
}
}

View File

@@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.badges.self.overview
enum class BadgesOverviewEvent {
FAILED_TO_UPDATE_PROFILE
}

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.badges.self.overview
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.Badges.displayBadges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.LifecycleDisposable
/**
* Fragment to allow user to manage options related to the badges they've unlocked.
*/
class BadgesOverviewFragment : DSLSettingsFragment(
titleId = R.string.ManageProfileFragment_badges,
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
) {
private val lifecycleDisposable = LifecycleDisposable()
private val viewModel: BadgesOverviewViewModel by viewModels(factoryProducer = { BadgesOverviewViewModel.Factory(BadgeRepository(requireContext())) })
override fun bindAdapter(adapter: DSLSettingsAdapter) {
Badge.register(adapter) { badge, _ ->
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
}
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
lifecycleDisposable.add(
viewModel.events.subscribe { event: BadgesOverviewEvent ->
when (event) {
BadgesOverviewEvent.FAILED_TO_UPDATE_PROFILE -> Toast.makeText(requireContext(), R.string.BadgesOverviewFragment__failed_to_update_profile, Toast.LENGTH_LONG).show()
}
}
)
}
private fun getConfiguration(state: BadgesOverviewState): DSLConfiguration {
return configure {
sectionHeaderPref(R.string.BadgesOverviewFragment__my_badges)
displayBadges(state.allUnlockedBadges)
switchPref(
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile),
isChecked = state.displayBadgesOnProfile,
onClick = {
viewModel.setDisplayBadgesOnProfile(!state.displayBadgesOnProfile)
}
)
clickPref(
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__featured_badge),
summary = state.featuredBadge?.name?.let { DSLSettingsText.from(it) },
isEnabled = state.stage == BadgesOverviewState.Stage.READY,
onClick = {
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
}
)
}
}
}

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.badges.self.overview
import org.thoughtcrime.securesms.badges.models.Badge
data class BadgesOverviewState(
val stage: Stage = Stage.INIT,
val allUnlockedBadges: List<Badge> = listOf(),
val featuredBadge: Badge? = null,
val displayBadgesOnProfile: Boolean = false
) {
enum class Stage {
INIT,
READY,
UPDATING
}
}

View File

@@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.badges.self.overview
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.livedata.Store
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
class BadgesOverviewViewModel(private val badgeRepository: BadgeRepository) : ViewModel() {
private val store = Store(BadgesOverviewState())
private val eventSubject = PublishSubject.create<BadgesOverviewEvent>()
val state: LiveData<BadgesOverviewState> = store.stateLiveData
val events: Observable<BadgesOverviewEvent> = eventSubject.observeOn(AndroidSchedulers.mainThread())
val disposables = CompositeDisposable()
init {
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
state.copy(
stage = if (state.stage == BadgesOverviewState.Stage.INIT) BadgesOverviewState.Stage.READY else state.stage,
allUnlockedBadges = recipient.badges,
displayBadgesOnProfile = recipient.badges.firstOrNull()?.visible == true
)
}
}
fun setDisplayBadgesOnProfile(displayBadgesOnProfile: Boolean) {
disposables += badgeRepository.setVisibilityForAllBadges(displayBadgesOnProfile)
.subscribe(
{
store.update { it.copy(stage = BadgesOverviewState.Stage.READY) }
},
{ error ->
Log.e(TAG, "Failed to update visibility.", error)
store.update { it.copy(stage = BadgesOverviewState.Stage.READY) }
eventSubject.onNext(BadgesOverviewEvent.FAILED_TO_UPDATE_PROFILE)
}
)
}
override fun onCleared() {
disposables.clear()
}
class Factory(private val badgeRepository: BadgeRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository)))
}
}
}

View File

@@ -0,0 +1,109 @@
package org.thoughtcrime.securesms.badges.view
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.button.MaterialButton
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.LargeBadge
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.visible
class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
private val viewModel: ViewBadgeViewModel by viewModels(factoryProducer = { ViewBadgeViewModel.Factory(getStartBadge(), getRecipientId(), BadgeRepository(requireContext())) })
override val peekHeightPercentage: Float = 1f
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.view_badge_bottom_sheet_dialog_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
postponeEnterTransition()
val pager: ViewPager2 = view.findViewById(R.id.pager)
val tabs: TabLayout = view.findViewById(R.id.tab_layout)
val action: MaterialButton = view.findViewById(R.id.action)
if (getRecipientId() == Recipient.self().id) {
action.visible = false
}
val adapter = MappingAdapter()
LargeBadge.register(adapter)
pager.adapter = adapter
adapter.submitList(listOf(LargeBadge.EmptyModel()))
TabLayoutMediator(tabs, pager) { _, _ ->
}.attach()
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
if (adapter.getModel(position).map { it is LargeBadge.Model }.orElse(false)) {
viewModel.onPageSelected(position)
}
}
})
viewModel.state.observe(viewLifecycleOwner) { state ->
if (state.recipient == null || state.badgeLoadState == ViewBadgeState.LoadState.INIT) {
return@observe
}
if (state.allBadgesVisibleOnProfile.isEmpty()) {
dismissAllowingStateLoss()
}
adapter.submitList(
state.allBadgesVisibleOnProfile.map {
LargeBadge.Model(LargeBadge(it), state.recipient.getShortDisplayName(requireContext()))
}
) {
val stateSelectedIndex = state.allBadgesVisibleOnProfile.indexOf(state.selectedBadge)
if (state.selectedBadge != null && pager.currentItem != stateSelectedIndex) {
pager.currentItem = stateSelectedIndex
}
}
}
}
private fun getStartBadge(): Badge? = requireArguments().getParcelable(ARG_START_BADGE)
private fun getRecipientId(): RecipientId = requireNotNull(requireArguments().getParcelable(ARG_RECIPIENT_ID))
companion object {
private const val ARG_START_BADGE = "start_badge"
private const val ARG_RECIPIENT_ID = "recipient_id"
@JvmStatic
fun show(
fragmentManager: FragmentManager,
recipientId: RecipientId,
startBadge: Badge? = null
) {
ViewBadgeBottomSheetDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_START_BADGE, startBadge)
putParcelable(ARG_RECIPIENT_ID, recipientId)
}
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}
}

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.badges.view
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.recipients.Recipient
data class ViewBadgeState(
val allBadgesVisibleOnProfile: List<Badge> = listOf(),
val badgeLoadState: LoadState = LoadState.INIT,
val selectedBadge: Badge? = null,
val recipient: Recipient? = null
) {
enum class LoadState {
INIT,
LOADED
}
}

View File

@@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.badges.view
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.livedata.Store
class ViewBadgeViewModel(
private val startBadge: Badge?,
private val recipientId: RecipientId,
private val repository: BadgeRepository
) : ViewModel() {
private val disposables = CompositeDisposable()
private val store = Store(ViewBadgeState())
val state: LiveData<ViewBadgeState> = store.stateLiveData
init {
store.update(Recipient.live(recipientId).liveData) { recipient, state ->
state.copy(
recipient = recipient,
allBadgesVisibleOnProfile = recipient.badges,
selectedBadge = startBadge ?: recipient.badges.firstOrNull(),
badgeLoadState = ViewBadgeState.LoadState.LOADED
)
}
}
override fun onCleared() {
disposables.clear()
}
fun onPageSelected(position: Int) {
if (position > store.state.allBadgesVisibleOnProfile.size - 1 || position < 0) {
return
}
store.update {
it.copy(selectedBadge = it.allBadgesVisibleOnProfile[position])
}
}
class Factory(
private val startBadge: Badge?,
private val recipientId: RecipientId,
private val repository: BadgeRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(ViewBadgeViewModel(startBadge, recipientId, repository)))
}
}
}

View File

@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BlurTransformation;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
import java.util.List;
@@ -207,8 +208,8 @@ public final class AvatarImageView extends AppCompatImageView {
this.chatColors = chatColors;
recipientContactPhoto = photo;
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider, ViewUtil.getWidth(this))
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider, ViewUtil.getWidth(this));
if (fixedSizeTarget != null) {
requestManager.clear(fixedSizeTarget);

View File

@@ -5,10 +5,14 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.CharacterStyle;
import android.text.style.MetricAffectingSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
@@ -17,15 +21,20 @@ import androidx.core.content.ContextCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
public class FromTextView extends EmojiTextView {
public class FromTextView extends SimpleEmojiTextView {
private static final String TAG = Log.tag(FromTextView.class);
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
public FromTextView(Context context) {
super(context);
}
@@ -45,20 +54,9 @@ public class FromTextView extends EmojiTextView {
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
String fromString = recipient.getDisplayName(getContext());
int typeface;
if (!read) {
typeface = Typeface.BOLD;
} else {
typeface = Typeface.NORMAL;
}
SpannableStringBuilder builder = new SpannableStringBuilder();
SpannableString fromSpan = new SpannableString(fromString);
fromSpan.setSpan(new StyleSpan(typeface), 0, builder.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
SpannableStringBuilder builder = new SpannableStringBuilder();
SpannableString fromSpan = new SpannableString(fromString);
fromSpan.setSpan(getFontSpan(!read), 0, fromSpan.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
if (recipient.isSelf()) {
builder.append(getContext().getString(R.string.note_to_self));
@@ -85,4 +83,8 @@ public class FromTextView extends EmojiTextView {
return mutedDrawable;
}
private CharacterStyle getFontSpan(boolean isBold) {
return isBold ? SpanUtil.getBoldSpan() : SpanUtil.getNormalSpan();
}
}

View File

@@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.components
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.annotation.LayoutRes
import androidx.fragment.app.DialogFragment
import org.thoughtcrime.securesms.R
/**
* Fullscreen Dialog Fragment which will dismiss itself when the keyboard is closed
*/
abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
DialogFragment(contentLayoutId),
KeyboardAwareLinearLayout.OnKeyboardShownListener,
KeyboardAwareLinearLayout.OnKeyboardHiddenListener {
private var hasShown = false
override fun onCreate(savedInstanceState: Bundle?) {
setStyle(STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet)
super.onCreate(savedInstanceState)
}
@Suppress("DEPRECATION")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setDimAmount(0f)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
return dialog
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
hasShown = false
val view = super.onCreateView(inflater, container, savedInstanceState)
return if (view is KeyboardAwareLinearLayout) {
view.addOnKeyboardShownListener(this)
view.addOnKeyboardHiddenListener(this)
view
} else {
throw IllegalStateException("Expected parent of view hierarchy to be keyboard aware.")
}
}
override fun onKeyboardShown() {
hasShown = true
}
override fun onKeyboardHidden() {
if (hasShown) {
dismissAllowingStateLoss()
}
}
}

View File

@@ -1,152 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class MaskView extends View {
private MaskTarget maskTarget;
private ViewGroup activityContentView;
private Paint maskPaint;
private Rect drawingRect = new Rect();
private float targetParentTranslationY;
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
public MaskView(@NonNull Context context) {
super(context);
}
public MaskView(@NonNull Context context, @Nullable AttributeSet attributeSet) {
super(context, attributeSet);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
setLayerType(LAYER_TYPE_HARDWARE, maskPaint);
maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
activityContentView = getRootView().findViewById(android.R.id.content);
}
public void setTarget(@Nullable MaskTarget maskTarget) {
if (this.maskTarget != null) {
removeOnDrawListener(this.maskTarget, onDrawListener);
}
this.maskTarget = maskTarget;
if (this.maskTarget != null) {
addOnDrawListener(maskTarget, onDrawListener);
}
invalidate();
}
public void setTargetParentTranslationY(float targetParentTranslationY) {
this.targetParentTranslationY = targetParentTranslationY;
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
if (nothingToMask(maskTarget)) {
return;
}
maskTarget.getPrimaryTarget().getDrawingRect(drawingRect);
activityContentView.offsetDescendantRectToMyCoords(maskTarget.getPrimaryTarget(), drawingRect);
drawingRect.top += targetParentTranslationY;
drawingRect.bottom += targetParentTranslationY;
Bitmap mask = Bitmap.createBitmap(maskTarget.getPrimaryTarget().getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
Canvas maskCanvas = new Canvas(mask);
maskTarget.draw(maskCanvas);
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) maskTarget.getPrimaryTarget().getLayoutParams();
canvas.drawBitmap(mask, params.leftMargin, drawingRect.top, maskPaint);
mask.recycle();
}
private static void removeOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) {
for (View view : maskTarget.getAllTargets()) {
if (view != null) {
view.getViewTreeObserver().removeOnDrawListener(onDrawListener);
}
}
}
private static void addOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) {
for (View view : maskTarget.getAllTargets()) {
if (view != null) {
view.getViewTreeObserver().addOnDrawListener(onDrawListener);
}
}
}
private static boolean nothingToMask(@Nullable MaskTarget maskTarget) {
if (maskTarget == null) {
return true;
}
for (View view : maskTarget.getAllTargets()) {
if (view == null || !view.isAttachedToWindow()) {
return true;
}
}
return false;
}
public static class MaskTarget {
private final View primaryTarget;
public MaskTarget(@NonNull View primaryTarget) {
this.primaryTarget = primaryTarget;
}
final @NonNull View getPrimaryTarget() {
return primaryTarget;
}
protected @NonNull List<View> getAllTargets() {
return Collections.singletonList(primaryTarget);
}
protected void draw(@NonNull Canvas canvas) {
primaryTarget.draw(canvas);
}
}
}

View File

@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.ThemeUtil;
@@ -228,55 +229,63 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
bodyView.setVisibility(GONE);
mediaDescriptionText.setVisibility(VISIBLE);
List<Slide> audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
List<Slide> videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
List<Slide> stickerSlides = Stream.of(attachments.getSlides()).filter(Slide::hasSticker).limit(1).toList();
List<Slide> viewOnceSlides = Stream.of(attachments.getSlides()).filter(Slide::hasViewOnce).limit(1).toList();
Slide audioSlide = attachments.getSlides().stream().filter(Slide::hasAudio).findFirst().orElse(null);
Slide documentSlide = attachments.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
Slide imageSlide = attachments.getSlides().stream().filter(Slide::hasImage).findFirst().orElse(null);
Slide videoSlide = attachments.getSlides().stream().filter(Slide::hasVideo).findFirst().orElse(null);
Slide stickerSlide = attachments.getSlides().stream().filter(Slide::hasSticker).findFirst().orElse(null);
Slide viewOnceSlide = attachments.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
// Given that most types have images, we specifically check images last
if (!viewOnceSlides.isEmpty()) {
if (viewOnceSlide != null) {
mediaDescriptionText.setText(R.string.QuoteView_view_once_media);
} else if (!audioSlides.isEmpty()) {
} else if (audioSlide != null) {
mediaDescriptionText.setText(R.string.QuoteView_audio);
} else if (!documentSlides.isEmpty()) {
} else if (documentSlide != null) {
mediaDescriptionText.setVisibility(GONE);
} else if (!videoSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_video);
} else if (!stickerSlides.isEmpty()) {
} else if (videoSlide != null) {
if (videoSlide.isVideoGif()) {
mediaDescriptionText.setText(R.string.QuoteView_gif);
} else {
mediaDescriptionText.setText(R.string.QuoteView_video);
}
} else if (stickerSlide != null) {
mediaDescriptionText.setText(R.string.QuoteView_sticker);
} else if (!imageSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_photo);
} else if (imageSlide != null) {
if (MediaUtil.isGif(imageSlide.getContentType())) {
mediaDescriptionText.setText(R.string.QuoteView_gif);
} else {
mediaDescriptionText.setText(R.string.QuoteView_photo);
}
}
}
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
List<Slide> viewOnceSlides = Stream.of(attachments.getSlides()).filter(Slide::hasViewOnce).limit(1).toList();
Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null);
Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
attachmentVideoOverlayView.setVisibility(GONE);
if (!viewOnceSlides.isEmpty()) {
if (viewOnceSlide != null) {
thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(GONE);
} else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getUri() != null) {
} else if (imageVideoSlide != null && imageVideoSlide.getUri() != null) {
thumbnailView.setVisibility(VISIBLE);
attachmentContainerView.setVisibility(GONE);
dismissView.setBackgroundResource(R.drawable.dismiss_background);
if (imageVideoSlides.get(0).hasVideo()) {
if (imageVideoSlide.hasVideo() && !imageVideoSlide.isVideoGif()) {
attachmentVideoOverlayView.setVisibility(VISIBLE);
}
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getUri()))
glideRequests.load(new DecryptableUri(imageVideoSlide.getUri()))
.centerCrop()
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(thumbnailView);
} else if (!documentSlides.isEmpty()){
} else if (documentSlide != null){
thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(VISIBLE);
attachmentNameView.setText(documentSlides.get(0).getFileName().or(""));
attachmentNameView.setText(documentSlide.getFileName().or(""));
} else {
thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(GONE);

View File

@@ -8,6 +8,7 @@ import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Xfermode;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
@@ -100,6 +101,10 @@ public final class RotatableGradientDrawable extends Drawable {
fillPaint.setShader(new LinearGradient(fillRect.left, fillRect.top, fillRect.right, fillRect.bottom, colors, positions, Shader.TileMode.CLAMP));
}
public void setXfermode(@NonNull Xfermode xfermode) {
fillPaint.setXfermode(xfermode);
}
private static Point cornerPrime(@NonNull Point origin, @NonNull Point corner, float degrees) {
return new Point(xPrime(origin, corner, Math.toRadians(degrees)), yPrime(origin, corner, Math.toRadians(degrees)));
}

View File

@@ -23,9 +23,12 @@ public class ShapeScrim extends View {
private final Paint eraser;
private final ShapeType shape;
private final float radius;
private final int canvasColor;
private Bitmap scrim;
private Canvas scrimCanvas;
private int scrimWidth;
private int scrimHeight;
public ShapeScrim(Context context) {
this(context, null);
@@ -57,13 +60,30 @@ public class ShapeScrim extends View {
this.eraser = new Paint();
this.eraser.setColor(0xFFFFFFFF);
this.eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
this.canvasColor = Color.parseColor("#55BDBDBD");
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
int shortDimension = Math.min(getWidth(), getHeight());
float drawRadius = shortDimension * radius;
float left = (getMeasuredWidth() / 2 ) - drawRadius;
float top = (getMeasuredHeight() / 2) - drawRadius;
float right = left + (drawRadius * 2);
float bottom = top + (drawRadius * 2);
scrimWidth = (int) (right - left);
scrimHeight = (int) (bottom - top);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
int shortDimension = getWidth() < getHeight() ? getWidth() : getHeight();
int shortDimension = Math.min(getWidth(), getHeight());
float drawRadius = shortDimension * radius;
if (scrimCanvas == null) {
@@ -72,7 +92,7 @@ public class ShapeScrim extends View {
}
scrim.eraseColor(Color.TRANSPARENT);
scrimCanvas.drawColor(Color.parseColor("#55BDBDBD"));
scrimCanvas.drawColor(canvasColor);
if (shape == ShapeType.CIRCLE) drawCircle(scrimCanvas, drawRadius, eraser);
else drawSquare(scrimCanvas, drawRadius, eraser);
@@ -104,4 +124,12 @@ public class ShapeScrim extends View {
canvas.drawRoundRect(square, 25, 25, eraser);
}
public int getScrimWidth() {
return scrimWidth;
}
public int getScrimHeight() {
return scrimHeight;
}
}

View File

@@ -10,6 +10,7 @@ import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.graphics.drawable.shapes.Shape;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
@@ -438,8 +439,10 @@ public class ThumbnailView extends FrameLayout {
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transition(withCrossFade()), fit);
if (slide.isInProgress()) return request;
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
if (slide.isInProgress() || doNotShowMissingThumbnailImage) return request;
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
}
private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {

View File

@@ -66,6 +66,7 @@ public class ZoomingImageView extends FrameLayout {
this.photoView = findViewById(R.id.image_view);
this.subsamplingImageView = findViewById(R.id.subsampling_image_view);
this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF);
this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF);
this.photoView.setZoomTransitionDuration(ZOOM_TRANSITION_DURATION);

View File

@@ -26,13 +26,15 @@ import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
class EmojiProvider {
public class EmojiProvider {
private static final String TAG = Log.tag(EmojiProvider.class);
private static final Paint PAINT = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
static @Nullable EmojiParser.CandidateList getCandidates(@Nullable CharSequence text) {
public static @Nullable EmojiParser.CandidateList getCandidates(@Nullable CharSequence text) {
if (text == null) return null;
return new EmojiParser(EmojiSource.getLatest().getEmojiTree()).findCandidates(text);
}
@@ -64,6 +66,32 @@ class EmojiProvider {
return builder;
}
public static @Nullable Spannable emojify(@NonNull Context context,
@Nullable EmojiParser.CandidateList matches,
@Nullable CharSequence text,
@NonNull Paint paint,
boolean synchronous)
{
if (matches == null || text == null) return null;
SpannableStringBuilder builder = new SpannableStringBuilder(text);
for (EmojiParser.Candidate candidate : matches) {
Drawable drawable;
if (synchronous) {
drawable = getEmojiDrawableSync(context, candidate.getDrawInfo());
} else {
drawable = getEmojiDrawable(context, candidate.getDrawInfo(), null);
}
if (drawable != null) {
builder.setSpan(new EmojiSpan(context, drawable, paint), candidate.getStartIndex(), candidate.getEndIndex(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return builder;
}
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji) {
EmojiDrawInfo drawInfo = EmojiSource.getLatest().getEmojiTree().getEmoji(emoji, 0, emoji.length());
return getEmojiDrawable(context, drawInfo, null);
@@ -113,6 +141,43 @@ class EmojiProvider {
return drawable;
}
/**
* Gets an EmojiDrawable from the Page Cache synchronously
*
* @param context Context object used in reading and writing from disk
* @param drawInfo Information about the emoji being displayed
*/
private static @Nullable Drawable getEmojiDrawableSync(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo) {
ThreadUtil.assertNotMainThread();
if (drawInfo == null) {
return null;
}
final int lowMemoryDecodeScale = DeviceProperties.isLowMemoryDevice(context) ? 2 : 1;
final EmojiSource source = EmojiSource.getLatest();
final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale);
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
Bitmap bitmap = null;
if (loadResult instanceof EmojiPageCache.LoadResult.Immediate) {
Log.d(TAG, "Cached emoji page: " + drawInfo.getPage().getUri().toString());
bitmap = ((EmojiPageCache.LoadResult.Immediate) loadResult).getBitmap();
} else if (loadResult instanceof EmojiPageCache.LoadResult.Async) {
Log.d(TAG, "Loading emoji page: " + drawInfo.getPage().getUri().toString());
try {
bitmap = ((EmojiPageCache.LoadResult.Async) loadResult).getTask().get(2, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
}
} else {
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
}
drawable.setBitmap(bitmap);
return drawable;
}
static final class EmojiDrawable extends Drawable {
private final float intrinsicWidth;
private final float intrinsicHeight;
@@ -160,7 +225,6 @@ class EmojiProvider {
}
public void setBitmap(Bitmap bitmap) {
ThreadUtil.assertMainThread();
if (bmp == null || !bmp.sameAs(bitmap)) {
bmp = bitmap;
invalidateSelf();

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
@@ -25,6 +26,15 @@ public class EmojiSpan extends AnimatingImageSpan {
getDrawable().setBounds(0, 0, size, size);
}
public EmojiSpan(@NonNull Context context, @NonNull Drawable drawable, @NonNull Paint paint) {
super(drawable, null);
fontMetrics = paint.getFontMetricsInt();
size = fontMetrics != null ? Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent)
: context.getResources().getDimensionPixelSize(R.dimen.conversation_item_body_text_size);
getDrawable().setBounds(0, 0, size, size);
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
if (fm != null && this.fontMetrics != null) {
@@ -48,6 +58,7 @@ public class EmojiSpan extends AnimatingImageSpan {
int height = bottom - top;
int centeringMargin = (height - size) / 2;
int adjustedMargin = (int) (centeringMargin * SHIFT_FACTOR);
int adjustedBottom = bottom - adjustedMargin;
super.draw(canvas, text, start, end, x, top, y, bottom - adjustedMargin, paint);
}
}

View File

@@ -11,6 +11,7 @@ import android.text.Spanned;
import android.text.TextDirectionHeuristic;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
import android.text.method.TransformationMethod;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.ViewGroup;
@@ -42,6 +43,7 @@ public class EmojiTextView extends AppCompatTextView {
private boolean forceCustom;
private CharSequence previousText;
private BufferType previousBufferType;
private TransformationMethod previousTransformationMethod;
private float originalFontSize;
private boolean useSystemEmoji;
private boolean sizeChangeInProgress;
@@ -124,10 +126,11 @@ public class EmojiTextView extends AppCompatTextView {
return;
}
previousText = text;
previousOverflowText = overflowText;
previousBufferType = type;
useSystemEmoji = useSystemEmoji();
previousText = text;
previousOverflowText = overflowText;
previousBufferType = type;
useSystemEmoji = useSystemEmoji();
previousTransformationMethod = getTransformationMethod();
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")), BufferType.NORMAL);
@@ -158,12 +161,13 @@ public class EmojiTextView extends AppCompatTextView {
lastLineWidth = -1;
} else {
Layout layout = getLayout();
int lines = layout.getLineCount();
int start = layout.getLineStart(lines - 1);
int count = text.length() - start;
text = layout.getText();
if ((getLayoutDirection() == LAYOUT_DIRECTION_LTR && textDirection.isRtl(text, start, count)) ||
(getLayoutDirection() == LAYOUT_DIRECTION_RTL && !textDirection.isRtl(text, start, count))) {
int lines = layout.getLineCount();
int start = layout.getLineStart(lines - 1);
if ((getLayoutDirection() == LAYOUT_DIRECTION_LTR && textDirection.isRtl(text, 0, text.length())) ||
(getLayoutDirection() == LAYOUT_DIRECTION_RTL && !textDirection.isRtl(text, 0, text.length()))) {
lastLineWidth = getMeasuredWidth();
} else {
lastLineWidth = (int) getPaint().measureText(text, start, text.length());
@@ -260,7 +264,8 @@ public class EmojiTextView extends AppCompatTextView {
Util.equals(previousOverflowText, overflowText) &&
Util.equals(previousBufferType, bufferType) &&
useSystemEmoji == useSystemEmoji() &&
!sizeChangeInProgress;
!sizeChangeInProgress &&
previousTransformationMethod == getTransformationMethod();
}
private boolean useSystemEmoji() {

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.FrameLayout;
@@ -20,6 +21,8 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPage;
import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment;
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
import java.util.Objects;
public class MediaKeyboard extends FrameLayout implements InputView {
private static final String TAG = Log.tag(MediaKeyboard.class);
@@ -40,6 +43,10 @@ public class MediaKeyboard extends FrameLayout implements InputView {
super(context, attrs);
}
public void setFragmentManager(@NonNull FragmentManager fragmentManager) {
this.fragmentManager = fragmentManager;
}
public void setKeyboardListener(@Nullable MediaKeyboardListener listener) {
this.keyboardListener = listener;
}
@@ -125,13 +132,32 @@ public class MediaKeyboard extends FrameLayout implements InputView {
private void initView() {
if (!isInitialised) {
LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
if (fragmentManager == null) {
FragmentActivity activity = resolveActivity(getContext());
fragmentManager = activity.getSupportFragmentManager();
}
keyboardPagerFragment = new KeyboardPagerFragment();
fragmentManager.beginTransaction()
.replace(R.id.media_keyboard_fragment_container, keyboardPagerFragment)
.commitNowAllowingStateLoss();
keyboardState = State.NORMAL;
latestKeyboardHeight = -1;
isInitialised = true;
fragmentManager = ((FragmentActivity) getContext()).getSupportFragmentManager();
keyboardPagerFragment = (KeyboardPagerFragment) fragmentManager.findFragmentById(R.id.media_keyboard_fragment_container);
}
}
private static FragmentActivity resolveActivity(@Nullable Context context) {
if (context instanceof FragmentActivity) {
return (FragmentActivity) context;
} else if (context instanceof ContextThemeWrapper) {
return resolveActivity(((ContextThemeWrapper) context).getBaseContext());
} else {
throw new IllegalStateException("Could not locate FragmentActivity");
}
}

View File

@@ -7,7 +7,7 @@ import androidx.appcompat.widget.AppCompatTextView
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.libsignal.util.guava.Optional
open class SingleLineEmojiTextView @JvmOverloads constructor(
open class SimpleEmojiTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
@@ -15,20 +15,16 @@ open class SingleLineEmojiTextView @JvmOverloads constructor(
private var bufferType: BufferType? = null
init {
maxLines = 1
}
override fun setText(text: CharSequence?, type: BufferType?) {
bufferType = type
val candidates = if (isInEditMode) null else EmojiProvider.getCandidates(text)
if (SignalStore.settings().isPreferSystemEmoji || candidates == null || candidates.size() == 0) {
super.setText(Optional.fromNullable(text).or(""), BufferType.NORMAL)
} else {
val newContent = if (width == 0) {
val newContent = if (width == 0 || maxLines == -1) {
text
} else {
TextUtils.ellipsize(text, paint, width.toFloat(), TextUtils.TruncateAt.END, false, null)
TextUtils.ellipsize(text, paint, (width * maxLines).toFloat(), TextUtils.TruncateAt.END, false, null)
}
val newCandidates = if (isInEditMode) null else EmojiProvider.getCandidates(newContent)
@@ -37,6 +33,7 @@ open class SingleLineEmojiTextView @JvmOverloads constructor(
} else {
EmojiProvider.emojify(newCandidates, newContent, this)
}
bufferType = BufferType.SPANNABLE
super.setText(newText, type)
}
}
@@ -47,9 +44,4 @@ open class SingleLineEmojiTextView @JvmOverloads constructor(
setText(text, bufferType ?: BufferType.NORMAL)
}
}
override fun setMaxLines(maxLines: Int) {
check(maxLines == 1) { "setMaxLines: $maxLines != 1" }
super.setMaxLines(maxLines)
}
}

View File

@@ -9,9 +9,11 @@ import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.SignalSessionLock;
@@ -40,12 +42,12 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
@Override
public void onClick(DialogInterface dialog, int which) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
final TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
SimpleTask.run(() -> {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
identityStore.setApproval(identityRecord.getRecipientId(), true);
}
}

View File

@@ -16,7 +16,7 @@ import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import java.util.List;

View File

@@ -11,7 +11,9 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.SignalSessionLock;
import java.util.List;
@@ -39,27 +41,16 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
@Override
public void onClick(DialogInterface dialog, int which) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
}
SimpleTask.run(() -> {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : untrustedRecords) {
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
resendListener.onResendMessage();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return null;
}, nothing -> resendListener.onResendMessage());
}
public interface ResendListener {

View File

@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.util.visible
class DSLSettingsAdapter : MappingAdapter() {
init {
registerFactory(ClickPreference::class.java, LayoutFactory(::ClickPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(LongClickPreference::class.java, LayoutFactory(::LongClickPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(TextPreference::class.java, LayoutFactory(::TextPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(RadioListPreference::class.java, LayoutFactory(::RadioListPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(MultiSelectListPreference::class.java, LayoutFactory(::MultiSelectListPreferenceViewHolder, R.layout.dsl_preference_item))
@@ -82,6 +83,16 @@ class ClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ClickPref
}
}
class LongClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<LongClickPreference>(itemView) {
override fun bind(model: LongClickPreference) {
super.bind(model)
itemView.setOnLongClickListener() {
model.onLongClick()
true
}
}
}
class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<RadioListPreference>(itemView) {
override fun bind(model: RadioListPreference) {
super.bind(model)

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.view.View
@@ -10,6 +11,7 @@ import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
@@ -18,7 +20,8 @@ import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimation
abstract class DSLSettingsFragment(
@StringRes private val titleId: Int = -1,
@MenuRes private val menuId: Int = -1,
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment,
val layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
) : Fragment(layoutId) {
private lateinit var recyclerView: RecyclerView
@@ -46,6 +49,7 @@ abstract class DSLSettingsFragment(
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
val adapter = DSLSettingsAdapter()
recyclerView.layoutManager = layoutManagerProducer(requireContext())
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(scrollAnimationHelper)

View File

@@ -39,6 +39,7 @@ class AppSettingsActivity : DSLSettingsActivity() {
.setStartCategoryIndex(intent.getIntExtra(HelpFragment.START_CATEGORY_INDEX, 0))
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
}
}
@@ -98,6 +99,9 @@ class AppSettingsActivity : DSLSettingsActivity() {
@JvmStatic
fun notifications(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATIONS)
@JvmStatic
fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CHANGE_NUMBER)
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
return Intent(context, AppSettingsActivity::class.java)
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
@@ -110,7 +114,8 @@ class AppSettingsActivity : DSLSettingsActivity() {
BACKUPS(1),
HELP(2),
PROXY(3),
NOTIFICATIONS(4);
NOTIFICATIONS(4),
CHANGE_NUMBER(5);
companion object {
fun fromCode(code: Int?): StartLocation {

View File

@@ -2,9 +2,10 @@ package org.thoughtcrime.securesms.components.settings.app
import android.view.View
import android.widget.TextView
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
@@ -27,7 +28,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
adapter.registerFactory(BioPreference::class.java, MappingAdapter.LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
adapter.registerFactory(PaymentsPreference::class.java, MappingAdapter.LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
val viewModel = ViewModelProviders.of(this)[AppSettingsViewModel::class.java]
val viewModel = ViewModelProvider(this)[AppSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
@@ -162,6 +163,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
private val avatarView: AvatarImageView = itemView.findViewById(R.id.icon)
private val aboutView: TextView = itemView.findViewById(R.id.about)
private val badgeView: BadgeImageView = itemView.findViewById(R.id.badge)
override fun bind(model: BioPreference) {
super.bind(model)
@@ -171,6 +173,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
titleView.text = model.recipient.profileName.toString()
summaryView.text = PhoneNumberFormatter.prettyPrint(model.recipient.requireE164())
avatarView.setRecipient(Recipient.self())
badgeView.setBadgeFromRecipient(Recipient.self())
titleView.visibility = View.VISIBLE
summaryView.visibility = View.VISIBLE

View File

@@ -14,7 +14,7 @@ import androidx.autofill.HintConstants
import androidx.core.app.DialogCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import com.google.android.material.snackbar.Snackbar
import org.thoughtcrime.securesms.R
@@ -30,6 +30,8 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity
import org.thoughtcrime.securesms.lock.v2.KbsConstants
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.ThemeUtil
@@ -49,7 +51,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
viewModel = ViewModelProviders.of(this)[AccountSettingsViewModel::class.java]
viewModel = ViewModelProvider(this)[AccountSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
@@ -61,6 +63,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
sectionHeaderPref(R.string.preferences_app_protection__signal_pin)
@Suppress("DEPRECATION")
clickPref(
title = DSLSettingsText.from(if (state.hasPin) R.string.preferences_app_protection__change_your_pin else R.string.preferences_app_protection__create_a_pin),
onClick = {
@@ -103,6 +106,15 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
sectionHeaderPref(R.string.AccountSettingsFragment__account)
if (FeatureFlags.changeNumber() && Recipient.self().changeNumberCapability == Recipient.Capability.SUPPORTED) {
clickPref(
title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
}
)
}
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),

View File

@@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.appearance
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
@@ -23,7 +23,7 @@ class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__app
private val languageValues by lazy { resources.getStringArray(R.array.language_values) }
override fun bindAdapter(adapter: DSLSettingsAdapter) {
viewModel = ViewModelProviders.of(this)[AppearanceSettingsViewModel::class.java]
viewModel = ViewModelProvider(this)[AppearanceSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())

View File

@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.registration.fragments.BaseAccountLockedFragment
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
class ChangeNumberAccountLockedFragment : BaseAccountLockedFragment(R.layout.fragment_change_number_account_locked) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
}
override fun getViewModel(): BaseRegistrationViewModel {
return ChangeNumberUtil.getViewModel(this)
}
override fun onNext() {
findNavController().navigateUp()
}
}

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_number_confirm) {
private lateinit var viewModel: ChangeNumberViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel = ChangeNumberUtil.getViewModel(this)
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
val confirmMessage: TextView = view.findViewById(R.id.change_number_confirm_new_number_message)
confirmMessage.text = getString(R.string.ChangeNumberConfirmFragment__you_are_about_to_change_your_phone_number_from_s_to_s, viewModel.oldNumberState.fullFormattedNumber, viewModel.number.fullFormattedNumber)
val newNumber: TextView = view.findViewById(R.id.change_number_confirm_new_number)
newNumber.text = viewModel.number.fullFormattedNumber
val editNumber: View = view.findViewById(R.id.change_number_confirm_edit_number)
editNumber.setOnClickListener { findNavController().navigateUp() }
val changeNumber: View = view.findViewById(R.id.change_number_confirm_change_number)
changeNumber.setOnClickListener { findNavController().navigate(R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment) }
}
}

View File

@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.fragments.BaseEnterCodeFragment
class ChangeNumberEnterCodeFragment : BaseEnterCodeFragment<ChangeNumberViewModel>(R.layout.fragment_change_number_enter_code) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
toolbar.title = viewModel.number.fullFormattedNumber
toolbar.setNavigationOnClickListener { navigateUp() }
view.findViewById<View>(R.id.verify_header).setOnClickListener(null)
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateUp()
}
}
)
}
private fun navigateUp() {
if (SignalStore.misc().isChangeNumberLocked) {
startActivity(ChangeNumberLockActivity.createIntent(requireContext()))
} else {
findNavController().navigateUp()
}
}
override fun getViewModel(): ChangeNumberViewModel {
return getViewModel(this)
}
override fun handleSuccessfulVerify() {
displaySuccess { changeNumberSuccess() }
}
override fun navigateToCaptcha() {
findNavController().navigate(R.id.action_changeNumberEnterCodeFragment_to_captchaFragment, getCaptchaArguments())
}
override fun navigateToRegistrationLock(timeRemaining: Long) {
findNavController().navigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
}
override fun navigateToKbsAccountLocked() {
findNavController().navigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
}
}

View File

@@ -0,0 +1,174 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.widget.ScrollView
import android.widget.Spinner
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.LabeledEditText
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel.ContinueStatus
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment
import org.thoughtcrime.securesms.registration.fragments.CountryPickerFragmentArgs
import org.thoughtcrime.securesms.registration.util.RegistrationNumberInputController
import org.thoughtcrime.securesms.util.Dialogs
private const val OLD_NUMBER_COUNTRY_SELECT = "old_number_country"
private const val NEW_NUMBER_COUNTRY_SELECT = "new_number_country"
class ChangeNumberEnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_change_number_enter_phone_number) {
private lateinit var scrollView: ScrollView
private lateinit var oldNumberCountrySpinner: Spinner
private lateinit var oldNumberCountryCode: LabeledEditText
private lateinit var oldNumber: LabeledEditText
private lateinit var newNumberCountrySpinner: Spinner
private lateinit var newNumberCountryCode: LabeledEditText
private lateinit var newNumber: LabeledEditText
private lateinit var viewModel: ChangeNumberViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel = getViewModel(this)
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
toolbar.setTitle(R.string.ChangeNumberEnterPhoneNumberFragment__change_number)
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
view.findViewById<View>(R.id.change_number_enter_phone_number_continue).setOnClickListener {
onContinue()
}
scrollView = view.findViewById(R.id.change_number_enter_phone_number_scroll)
oldNumberCountrySpinner = view.findViewById(R.id.change_number_enter_phone_number_old_number_spinner)
oldNumberCountryCode = view.findViewById(R.id.change_number_enter_phone_number_old_number_country_code)
oldNumber = view.findViewById(R.id.change_number_enter_phone_number_old_number_number)
val oldController = RegistrationNumberInputController(
requireContext(),
oldNumberCountryCode,
oldNumber,
oldNumberCountrySpinner,
false,
object : RegistrationNumberInputController.Callbacks {
override fun onNumberFocused() {
scrollView.postDelayed({ scrollView.smoothScrollTo(0, oldNumber.bottom) }, 250)
}
override fun onNumberInputNext(view: View) {
newNumberCountryCode.requestFocus()
}
override fun onNumberInputDone(view: View) = Unit
override fun onPickCountry(view: View) {
val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(OLD_NUMBER_COUNTRY_SELECT).build()
findNavController().navigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
}
override fun setNationalNumber(number: String) {
viewModel.setOldNationalNumber(number)
}
override fun setCountry(countryCode: Int) {
viewModel.setOldCountry(countryCode)
}
}
)
newNumberCountrySpinner = view.findViewById(R.id.change_number_enter_phone_number_new_number_spinner)
newNumberCountryCode = view.findViewById(R.id.change_number_enter_phone_number_new_number_country_code)
newNumber = view.findViewById(R.id.change_number_enter_phone_number_new_number_number)
val newController = RegistrationNumberInputController(
requireContext(),
newNumberCountryCode,
newNumber,
newNumberCountrySpinner,
true,
object : RegistrationNumberInputController.Callbacks {
override fun onNumberFocused() {
scrollView.postDelayed({ scrollView.smoothScrollTo(0, newNumber.bottom) }, 250)
}
override fun onNumberInputNext(view: View) = Unit
override fun onNumberInputDone(view: View) {
onContinue()
}
override fun onPickCountry(view: View) {
val arguments: CountryPickerFragmentArgs = CountryPickerFragmentArgs.Builder().setResultKey(NEW_NUMBER_COUNTRY_SELECT).build()
findNavController().navigate(R.id.action_enterPhoneNumberChangeFragment_to_countryPickerFragment, arguments.toBundle())
}
override fun setNationalNumber(number: String) {
viewModel.setNewNationalNumber(number)
}
override fun setCountry(countryCode: Int) {
viewModel.setNewCountry(countryCode)
}
}
)
parentFragmentManager.setFragmentResultListener(OLD_NUMBER_COUNTRY_SELECT, this) { _, bundle ->
viewModel.setOldCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
}
parentFragmentManager.setFragmentResultListener(NEW_NUMBER_COUNTRY_SELECT, this) { _, bundle ->
viewModel.setNewCountry(bundle.getInt(CountryPickerFragment.KEY_COUNTRY_CODE), bundle.getString(CountryPickerFragment.KEY_COUNTRY))
}
viewModel.getLiveOldNumber().observe(viewLifecycleOwner, oldController::updateNumber)
viewModel.getLiveNewNumber().observe(viewLifecycleOwner, newController::updateNumber)
}
private fun onContinue() {
if (TextUtils.isEmpty(oldNumberCountryCode.text)) {
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_number_country_code), Toast.LENGTH_LONG).show()
return
}
if (TextUtils.isEmpty(oldNumber.text)) {
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_old_phone_number), Toast.LENGTH_LONG).show()
return
}
if (TextUtils.isEmpty(newNumberCountryCode.text)) {
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_number_country_code), Toast.LENGTH_LONG).show()
return
}
if (TextUtils.isEmpty(newNumber.text)) {
Toast.makeText(context, getString(R.string.ChangeNumberEnterPhoneNumberFragment__you_must_specify_your_new_phone_number), Toast.LENGTH_LONG).show()
return
}
when (viewModel.canContinue()) {
ContinueStatus.CAN_CONTINUE -> findNavController().navigate(R.id.action_enterPhoneNumberChangeFragment_to_changePhoneNumberConfirmFragment)
ContinueStatus.INVALID_NUMBER -> {
Dialogs.showAlertDialog(
context, getString(R.string.RegistrationActivity_invalid_number), String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.number.e164Number)
)
}
ContinueStatus.OLD_NUMBER_DOESNT_MATCH -> {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.ChangeNumberEnterPhoneNumberFragment__the_phone_number_you_entered_doesnt_match_your_accounts)
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
}
}

View File

@@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
class ChangeNumberFragment : LoggingFragment(R.layout.fragment_change_phone_number) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
view.findViewById<View>(R.id.change_phone_number_continue).setOnClickListener {
findNavController().navigate(R.id.action_changePhoneNumberFragment_to_enterPhoneNumberChangeFragment)
}
}
}

View File

@@ -0,0 +1,108 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.TextSecurePreferences
import java.util.Objects
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
/**
* A captive activity that can determine if an interrupted/erred change number request
* caused a disparity between the server and our locally stored number.
*/
class ChangeNumberLockActivity : PassphraseRequiredActivity() {
private val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
private val disposables: LifecycleDisposable = LifecycleDisposable()
private lateinit var changeNumberRepository: ChangeNumberRepository
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
dynamicTheme.onCreate(this)
disposables.bindTo(lifecycle)
setContentView(R.layout.activity_change_number_lock)
changeNumberRepository = ChangeNumberRepository(applicationContext)
checkWhoAmI()
}
override fun onResume() {
super.onResume()
dynamicTheme.onResume(this)
}
override fun onBackPressed() = Unit
private fun checkWhoAmI() {
disposables.add(
changeNumberRepository.whoAmI()
.flatMap { whoAmI ->
if (Objects.equals(whoAmI.number, TextSecurePreferences.getLocalNumber(this))) {
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
Single.just(false)
} else {
Log.i(TAG, "Local (${TextSecurePreferences.getLocalNumber(this)}) and remote (${whoAmI.number}) numbers do not match, updating local.")
changeNumberRepository.changeLocalNumber(whoAmI.number)
.map { true }
}
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus)
)
}
private fun onChangeStatusConfirmed() {
SignalStore.misc().unlockChangeNumber()
MaterialAlertDialogBuilder(this)
.setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed)
.setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(TextSecurePreferences.getLocalNumber(this))))
.setPositiveButton(android.R.string.ok) { _, _ ->
startActivity(MainActivity.clearTop(this))
finish()
}
.setCancelable(false)
.show()
}
private fun onFailedToGetChangeNumberStatus(error: Throwable) {
Log.w(TAG, "Unable to determine status of change number", error)
MaterialAlertDialogBuilder(this)
.setTitle(R.string.ChangeNumberLockActivity__change_status_unconfirmed)
.setMessage(getString(R.string.ChangeNumberLockActivity__we_could_not_determine_the_status_of_your_change_number_request, error.javaClass.simpleName))
.setPositiveButton(R.string.ChangeNumberLockActivity__retry) { _, _ -> checkWhoAmI() }
.setNegativeButton(R.string.ChangeNumberLockActivity__leave) { _, _ -> finish() }
.setNeutralButton(R.string.ChangeNumberLockActivity__submit_debug_log) { _, _ ->
startActivity(Intent(this, SubmitDebugLogActivity::class.java))
finish()
}
.setCancelable(false)
.show()
}
companion object {
@JvmStatic
fun createIntent(context: Context): Intent {
return Intent(context, ChangeNumberLockActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
}
}
}

View File

@@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity
class ChangeNumberPinDiffersFragment : LoggingFragment(R.layout.fragment_change_number_pin_differs) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<View>(R.id.change_number_pin_differs_keep_old_pin).setOnClickListener {
changeNumberSuccess()
}
val changePin = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == CreateKbsPinActivity.RESULT_OK) {
changeNumberSuccess()
}
}
view.findViewById<View>(R.id.change_number_pin_differs_update_pin).setOnClickListener {
changePin.launch(CreateKbsPinActivity.getIntentForPinChangeFromSettings(requireContext()))
}
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.ChangeNumberPinDiffersFragment__keep_old_pin_question)
.setPositiveButton(android.R.string.ok) { _, _ -> changeNumberSuccess() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
)
}
}

View File

@@ -0,0 +1,81 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.PinHashing
import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
import org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
class ChangeNumberRegistrationLockFragment : BaseRegistrationLockFragment(R.layout.fragment_change_number_registration_lock) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
toolbar.setNavigationOnClickListener { navigateUp() }
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateUp()
}
}
)
}
override fun getViewModel(): BaseRegistrationViewModel {
return ChangeNumberUtil.getViewModel(this)
}
override fun navigateToAccountLocked() {
findNavController().navigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked())
}
override fun handleSuccessfulPinEntry(pin: String) {
val pinsDiffer: Boolean = SignalStore.kbsValues().localPinHash?.let { !PinHashing.verifyLocalPinHash(it, pin) } ?: false
cancelSpinning(pinButton)
if (pinsDiffer) {
findNavController().navigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers())
} else {
changeNumberSuccess()
}
}
override fun sendEmailToSupport() {
val subject = R.string.ChangeNumberRegistrationLockFragment__signal_change_number_need_help_with_pin_for_android_v2_pin
val body: String = SupportEmailUtil.generateSupportEmailBody(
requireContext(),
subject,
null,
null
)
CommunicationActions.openEmail(
requireContext(),
SupportEmailUtil.getSupportEmailAddress(requireContext()),
getString(subject),
body
)
}
private fun navigateUp() {
if (SignalStore.misc().isChangeNumberLocked) {
startActivity(ChangeNumberLockActivity.createIntent(requireContext()))
} else {
findNavController().navigateUp()
}
}
}

View File

@@ -0,0 +1,93 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.content.Context
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.CertificateType
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
import org.thoughtcrime.securesms.pin.TokenData
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.KbsPinData
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
class ChangeNumberRepository(private val context: Context) {
private val accountManager = ApplicationDependencies.getSignalServiceAccountManager()
fun changeNumber(code: String, newE164: String): Single<ServiceResponse<VerifyAccountResponse>> {
return Single.fromCallable { accountManager.changeNumber(code, newE164, null) }
.subscribeOn(Schedulers.io())
}
fun changeNumber(
code: String,
newE164: String,
pin: String,
tokenData: TokenData
): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> {
return Single.fromCallable {
try {
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
val registrationLock: String = kbsData.masterKey.deriveRegistrationLock()
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(code, newE164, registrationLock)
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
} catch (e: KeyBackupSystemWrongPinException) {
ServiceResponse.forExecutionError(e)
} catch (e: KeyBackupSystemNoDataException) {
ServiceResponse.forExecutionError(e)
}
}.subscribeOn(Schedulers.io())
}
@Suppress("UsePropertyAccessSyntax")
fun whoAmI(): Single<WhoAmIResponse> {
return Single.fromCallable { ApplicationDependencies.getSignalServiceAccountManager().getWhoAmI() }
.subscribeOn(Schedulers.io())
}
@WorkerThread
fun changeLocalNumber(e164: String): Single<Unit> {
DatabaseFactory.getRecipientDatabase(context).updateSelfPhone(e164)
TextSecurePreferences.setLocalNumber(context, e164)
ApplicationDependencies.closeConnections()
ApplicationDependencies.getIncomingMessageObserver()
return rotateCertificates()
}
@Suppress("UsePropertyAccessSyntax")
private fun rotateCertificates(): Single<Unit> {
val certificateTypes = SignalStore.phoneNumberPrivacy().allCertificateTypes
Log.i(TAG, "Rotating these certificates $certificateTypes")
return Single.fromCallable {
for (certificateType in certificateTypes) {
val certificate: ByteArray? = when (certificateType) {
CertificateType.UUID_AND_E164 -> accountManager.getSenderCertificate()
CertificateType.UUID_ONLY -> accountManager.getSenderCertificateForPhoneNumberPrivacy()
else -> throw AssertionError()
}
Log.i(TAG, "Successfully got $certificateType certificate")
SignalStore.certificateValues().setUnidentifiedAccessCertificate(certificateType, certificate)
}
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.registration.fragments.CaptchaFragment
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
/**
* Helpers for various aspects of the change number flow.
*/
object ChangeNumberUtil {
@JvmStatic
fun getViewModel(fragment: Fragment): ChangeNumberViewModel {
val navController = NavHostFragment.findNavController(fragment)
return ViewModelProvider(
navController.getViewModelStoreOwner(R.id.app_settings_change_number),
ChangeNumberViewModel.Factory(navController.getBackStackEntry(R.id.app_settings_change_number))
).get(ChangeNumberViewModel::class.java)
}
fun getCaptchaArguments(): Bundle {
return Bundle().apply {
putSerializable(
CaptchaFragment.EXTRA_VIEW_MODEL_PROVIDER,
object : CaptchaFragment.CaptchaViewModelProvider {
override fun get(fragment: CaptchaFragment): BaseRegistrationViewModel = getViewModel(fragment)
}
)
}
}
fun Fragment.changeNumberSuccess() {
findNavController().navigate(R.id.action_pop_app_settings_change_number)
Toast.makeText(requireContext(), R.string.ChangeNumber__your_phone_number_has_been_changed, Toast.LENGTH_SHORT).show()
}
}

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.os.Bundle
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.util.LifecycleDisposable
private val TAG: String = Log.tag(ChangeNumberVerifyFragment::class.java)
class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phone_number_verify) {
private lateinit var viewModel: ChangeNumberViewModel
private var requestingCaptcha: Boolean = false
private val lifecycleDisposable: LifecycleDisposable = LifecycleDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleDisposable.bindTo(lifecycle)
viewModel = getViewModel(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
toolbar.setTitle(R.string.ChangeNumberVerifyFragment__change_number)
toolbar.setNavigationOnClickListener { findNavController().navigateUp() }
val status: TextView = view.findViewById(R.id.change_phone_number_verify_status)
status.text = getString(R.string.ChangeNumberVerifyFragment__verifying_s, viewModel.number.fullFormattedNumber)
if (!requestingCaptcha || viewModel.hasCaptchaToken()) {
requestCode()
} else {
Toast.makeText(requireContext(), R.string.ChangeNumberVerifyFragment__captcha_required, Toast.LENGTH_SHORT).show()
findNavController().navigateUp()
}
}
private fun requestCode() {
lifecycleDisposable.add(
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { processor ->
if (processor.hasResult()) {
findNavController().navigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
} else if (processor.localRateLimit()) {
Log.i(TAG, "Unable to request sms code due to local rate limit")
findNavController().navigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
} else if (processor.captchaRequired()) {
Log.i(TAG, "Unable to request sms code due to captcha required")
findNavController().navigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
requestingCaptcha = true
} else if (processor.rateLimit()) {
Log.i(TAG, "Unable to request sms code due to rate limit")
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show()
findNavController().navigateUp()
} else {
Log.w(TAG, "Unable to request sms code", processor.error)
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show()
findNavController().navigateUp()
}
}
)
}
}

View File

@@ -0,0 +1,191 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.app.Application
import androidx.annotation.WorkerThread
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.savedstate.SavedStateRegistryOwner
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository
import org.thoughtcrime.securesms.pin.TokenData
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs
import org.thoughtcrime.securesms.registration.VerifyCodeWithRegistrationLockResponseProcessor
import org.thoughtcrime.securesms.registration.VerifyProcessor
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
import org.thoughtcrime.securesms.util.DefaultValueLiveData
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.util.Objects
private val TAG: String = Log.tag(ChangeNumberViewModel::class.java)
class ChangeNumberViewModel(
private val localNumber: String,
private val changeNumberRepository: ChangeNumberRepository,
savedState: SavedStateHandle,
password: String,
verifyAccountRepository: VerifyAccountRepository,
kbsRepository: KbsRepository,
) : BaseRegistrationViewModel(savedState, verifyAccountRepository, kbsRepository, password) {
var oldNumberState: NumberViewState = NumberViewState.Builder().build()
private set
private val liveOldNumberState = DefaultValueLiveData(oldNumberState)
private val liveNewNumberState = DefaultValueLiveData(number)
init {
try {
val countryCode: Int = PhoneNumberUtil.getInstance()
.parse(localNumber, null)
.countryCode
setOldCountry(countryCode)
setNewCountry(countryCode)
} catch (e: NumberParseException) {
Log.i(TAG, "Unable to parse number for default country code")
}
}
fun getLiveOldNumber(): LiveData<NumberViewState> {
return liveOldNumberState
}
fun getLiveNewNumber(): LiveData<NumberViewState> {
return liveNewNumberState
}
fun setOldNationalNumber(number: String) {
oldNumberState = oldNumberState.toBuilder()
.nationalNumber(number)
.build()
liveOldNumberState.value = oldNumberState
}
fun setOldCountry(countryCode: Int, country: String? = null) {
oldNumberState = oldNumberState.toBuilder()
.selectedCountryDisplayName(country)
.countryCode(countryCode)
.build()
liveOldNumberState.value = oldNumberState
}
fun setNewNationalNumber(number: String) {
setNationalNumber(number)
liveNewNumberState.value = this.number
}
fun setNewCountry(countryCode: Int, country: String? = null) {
onCountrySelected(country, countryCode)
liveNewNumberState.value = this.number
}
fun canContinue(): ContinueStatus {
return if (oldNumberState.e164Number == localNumber) {
if (number.isValid) {
ContinueStatus.CAN_CONTINUE
} else {
ContinueStatus.INVALID_NUMBER
}
} else {
ContinueStatus.OLD_NUMBER_DOESNT_MATCH
}
}
override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyAccountResponseProcessor> {
return super.verifyCodeWithoutRegistrationLock(code)
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
.flatMap(this::attemptToUnlockChangeNumber)
}
override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> {
return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
.flatMap(this::attemptToUnlockChangeNumber)
}
private fun <T : VerifyProcessor> attemptToUnlockChangeNumber(processor: T): Single<T> {
return if (processor.hasResult() || processor.isServerSentError()) {
SignalStore.misc().unlockChangeNumber()
Single.just(processor)
} else {
changeNumberRepository.whoAmI()
.map { whoAmI ->
if (Objects.equals(whoAmI.number, localNumber)) {
Log.i(TAG, "Local and remote numbers match, we can unlock.")
SignalStore.misc().unlockChangeNumber()
}
processor
}
.onErrorReturn { processor }
}
}
override fun verifyAccountWithoutRegistrationLock(): Single<ServiceResponse<VerifyAccountResponse>> {
return changeNumberRepository.changeNumber(textCodeEntered, number.e164Number)
}
override fun verifyAccountWithRegistrationLock(pin: String, kbsTokenData: TokenData): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> {
return changeNumberRepository.changeNumber(textCodeEntered, number.e164Number, pin, kbsTokenData)
}
@WorkerThread
override fun onVerifySuccess(processor: VerifyAccountResponseProcessor): Single<VerifyAccountResponseProcessor> {
return changeNumberRepository.changeLocalNumber(number.e164Number)
.map { processor }
.onErrorReturn { t ->
Log.w(TAG, "Error attempting to change local number", t)
VerifyAccountResponseWithoutKbs(ServiceResponse.forUnknownError(t))
}
}
override fun onVerifySuccessWithRegistrationLock(processor: VerifyCodeWithRegistrationLockResponseProcessor, pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> {
return changeNumberRepository.changeLocalNumber(number.e164Number)
.map { processor }
.onErrorReturn { t ->
Log.w(TAG, "Error attempting to change local number", t)
VerifyCodeWithRegistrationLockResponseProcessor(ServiceResponse.forUnknownError(t), processor.token)
}
}
class Factory(owner: SavedStateRegistryOwner) : AbstractSavedStateViewModelFactory(owner, null) {
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
val context: Application = ApplicationDependencies.getApplication()
val localNumber: String = TextSecurePreferences.getLocalNumber(context)
val password: String = TextSecurePreferences.getPushServerPassword(context)
val viewModel = ChangeNumberViewModel(
localNumber = localNumber,
changeNumberRepository = ChangeNumberRepository(context),
savedState = handle,
password = password,
verifyAccountRepository = VerifyAccountRepository(context),
kbsRepository = KbsRepository()
)
return requireNotNull(modelClass.cast(viewModel))
}
}
enum class ContinueStatus {
CAN_CONTINUE,
INVALID_NUMBER,
OLD_NUMBER_DOESNT_MATCH
}
}

View File

@@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
@@ -16,7 +16,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
override fun bindAdapter(adapter: DSLSettingsAdapter) {
val repository = ChatsSettingsRepository()
val factory = ChatsSettingsViewModel.Factory(repository)
viewModel = ViewModelProviders.of(this, factory)[ChatsSettingsViewModel::class.java]
viewModel = ViewModelProvider(this, factory)[ChatsSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())

View File

@@ -4,7 +4,7 @@ import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
@@ -28,7 +28,7 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
viewModel = ViewModelProviders.of(this)[SmsSettingsViewModel::class.java]
viewModel = ViewModelProvider(this)[SmsSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
@@ -41,6 +41,7 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
private fun getConfiguration(state: SmsSettingsState): DSLConfiguration {
return configure {
@Suppress("DEPRECATION")
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__use_as_default_sms_app),
summary = DSLSettingsText.from(if (state.useAsDefaultSmsApp) R.string.arrays__enabled else R.string.arrays__disabled),
@@ -84,6 +85,7 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
// Linter isn't smart enough to figure out the else only happens if API >= 24
@SuppressLint("InlinedApi")
@Suppress("DEPRECATION")
private fun startDefaultAppSelectionIntent() {
val intent: Intent = when {
Build.VERSION.SDK_INT < 23 -> Intent(Settings.ACTION_WIRELESS_SETTINGS)

View File

@@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.data
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import androidx.preference.PreferenceManager
import org.thoughtcrime.securesms.R
@@ -31,7 +31,7 @@ class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences_
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val repository = DataAndStorageSettingsRepository()
val factory = DataAndStorageSettingsViewModel.Factory(preferences, repository)
viewModel = ViewModelProviders.of(this, factory)[DataAndStorageSettingsViewModel::class.java]
viewModel = ViewModelProvider(this, factory)[DataAndStorageSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())

View File

@@ -5,7 +5,7 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.widget.Toast
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.BuildConfig
@@ -34,7 +34,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
override fun bindAdapter(adapter: DSLSettingsAdapter) {
val repository = InternalSettingsRepository(requireContext())
val factory = InternalSettingsViewModel.Factory(repository)
viewModel = ViewModelProviders.of(this, factory)[InternalSettingsViewModel::class.java]
viewModel = ViewModelProvider(this, factory)[InternalSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
@@ -107,6 +107,15 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
sectionHeaderPref(R.string.preferences__internal_storage_service)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_disable_storage_service),
summary = DSLSettingsText.from(R.string.preferences__internal_disable_storage_service_description),
isChecked = state.disableStorageService,
onClick = {
viewModel.setDisableStorageService(!state.disableStorageService)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_force_storage_service_sync),
summary = DSLSettingsText.from(R.string.preferences__internal_force_storage_service_sync_description),
@@ -221,7 +230,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
summary = DSLSettingsText.from(emojiSummary),
isChecked = state.useBuiltInEmojiSet,
onClick = {
viewModel.setDisableAutoMigrationNotification(!state.useBuiltInEmojiSet)
viewModel.setUseBuiltInEmoji(!state.useBuiltInEmojiSet)
}
)
@@ -314,12 +323,12 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
.setPositiveButton(
"Copy"
) { _: DialogInterface?, _: Int ->
val context: Context = ApplicationDependencies.getApplication()
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
SimpleTask.run<Any?>(
SignalExecutors.UNBOUNDED,
{
val context: Context = ApplicationDependencies.getApplication()
val clipboard =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val tsv = DataExportUtil.createTsv()
val clip = ClipData.newPlainText(context.getString(R.string.app_name), tsv)
clipboard.setPrimaryClip(clip)

View File

@@ -17,4 +17,5 @@ data class InternalSettingsState(
val emojiVersion: EmojiFiles.Version?,
val removeSenderKeyMinimium: Boolean,
val delayResends: Boolean,
val disableStorageService: Boolean,
)

View File

@@ -30,6 +30,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun setDisableStorageService(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.DISABLE_STORAGE_SERVICE, enabled)
refresh()
}
fun setGv2DoNotCreateGv2Groups(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_DO_NOT_CREATE_GV2, enabled)
refresh()
@@ -103,7 +108,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
useBuiltInEmojiSet = SignalStore.internalValues().forceBuiltInEmoji(),
emojiVersion = null,
removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum(),
delayResends = SignalStore.internalValues().delayResends()
delayResends = SignalStore.internalValues().delayResends(),
disableStorageService = SignalStore.internalValues().storageServiceDisabled()
)
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {

View File

@@ -12,7 +12,7 @@ import android.text.TextUtils
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
@@ -71,7 +71,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val factory = NotificationsSettingsViewModel.Factory(sharedPreferences)
viewModel = ViewModelProviders.of(this, factory)[NotificationsSettingsViewModel::class.java]
viewModel = ViewModelProvider(this, factory)[NotificationsSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
@@ -241,6 +241,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
}
}
@Suppress("DEPRECATION")
private fun launchMessageSoundSelectionIntent() {
val current = SignalStore.settings().messageNotificationSound
@@ -268,6 +269,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
startActivity(intent)
}
@Suppress("DEPRECATION")
private fun launchCallRingtoneSelectionIntent() {
val current = SignalStore.settings().callRingtone

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